All files / src/lib fullscreen.ts

100% Statements 42/42
100% Branches 16/16
100% Functions 14/14
100% Lines 36/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114                            6x 1903x               6x 16x 14x               6x                                                     6x       36x 36x   33x 33x   1x   32x 32x 32x 32x 51x 50x 4x 4x   46x 46x 46x     45x   1x     46x 46x 1x 1x       32x 1x 1x       32x 32x     27x 27x        
// SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
 
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { ChangeTracker } from "@/lib/change-tracker";
import { isMobilePlatform } from "@/lib/platform";
import type { LayoutMode } from "@/lib/types";
 
/**
 * Whether the player shows a full screen button. The phone layout already
 * plays full screen. Every other layout has the sidebar next to the video,
 * including an iPad's, so it needs the button. A device without a touch
 * screen, like an Android TV box, keeps it for its remote in any layout.
 */
export const hasFullscreenButton = (touch: boolean, layoutMode: LayoutMode): boolean =>
	!touch || layoutMode !== "mobile";
 
/**
 * Puts the window in or out of full screen. On iOS and Android the window
 * already fills its screen or its Split View pane, and tao's iOS version
 * would resize it to the whole screen, so there full screen only hides the
 * sidebar.
 */
export const setWindowFullscreen = async (on: boolean): Promise<void> => {
	if (isMobilePlatform()) return;
	await getCurrentWindow().setFullscreen(on);
};
 
/**
 * How long after one of the app's own full screen changes to read the window
 * again, in milliseconds. It's longer than the macOS animation, so the change
 * has landed or been turned down by then.
 */
export const RECHECK_MS = 1000;
 
/** Follows the window's full screen state. See `followWindowFullscreen`. */
export interface FullscreenFollower {
	/**
	 * Reads the window again. Call it a while after one of the app's own
	 * changes, since macOS can turn a change down without resizing the window.
	 */
	recheck: () => void;
	/** Stops following, and passes nothing more on. */
	stop: () => void;
}
 
/**
 * Reads whether the window is full screen once it can follow its resizes,
 * then after each resize, and passes it to `onChange` until `stop` is
 * called. Going in or out of full screen resizes the window, so this also
 * catches the ways the app doesn't start itself, such as the green button
 * on macOS.
 *
 * A read sent before one of the app's own changes in `changes` is dropped,
 * since it may come back with the state from before the change. Dragging the
 * window's edge resizes it many times a second, so only one read runs at a
 * time, with one more after it if the window resized meanwhile. Does nothing
 * on iOS and Android, where the window never goes full screen, or outside
 * Tauri, where there's no window to follow.
 */
export const followWindowFullscreen = (
	onChange: (on: boolean) => void,
	changes: ChangeTracker
): FullscreenFollower => {
	const idle = { recheck: () => {}, stop: () => {} };
	if (isMobilePlatform()) return idle;
	let win: ReturnType<typeof getCurrentWindow>;
	try {
		win = getCurrentWindow();
	} catch {
		return idle;
	}
	let stopped = false;
	let reading = false;
	let resizedMeanwhile = false;
	const read = () => {
		if (stopped) return;
		if (reading) {
			resizedMeanwhile = true;
			return;
		}
		reading = true;
		const mark = changes.mark();
		win.isFullscreen()
			.then(
				(on) => {
					if (!stopped && !changes.isStale(mark)) onChange(on);
				},
				(e) => console.error("[fullscreen] isFullscreen failed:", e)
			)
			.finally(() => {
				reading = false;
				if (resizedMeanwhile) {
					resizedMeanwhile = false;
					read();
				}
			});
	};
	const unlisten = win.onResized(read).catch((e) => {
		console.error("[fullscreen] onResized failed:", e);
		return () => {};
	});
	// Reading only once the listener is in place means a change made while
	// it was being set up isn't missed.
	unlisten.then(() => read());
	return {
		recheck: read,
		stop: () => {
			stopped = true;
			unlisten.then((fn) => fn());
		},
	};
};