All files / src/lib fullscreen-context.tsx

100% Statements 24/24
100% Branches 0/0
100% Functions 10/10
100% Lines 21/21

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                                                              5x         5x 69x     69x 69x 69x   12x 12x 12x   3x     12x     12x                 69x 24x 24x 24x 24x 24x     69x 69x     3898x  
// SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
 
import {
	createContext,
	useCallback,
	useContext,
	useEffect,
	useMemo,
	useRef,
	useState,
} from "react";
import { createChangeTracker } from "@/lib/change-tracker";
import {
	RECHECK_MS,
	followWindowFullscreen,
	setWindowFullscreen,
	type FullscreenFollower,
} from "@/lib/fullscreen";
 
interface FullscreenContextValue {
	isFullscreen: boolean;
	/**
	 * Puts the player in or out of full screen. This hides or shows the
	 * sidebar and sets the window to match, so every way in or out, such as
	 * the button, Escape or closing the player, leaves the two in step. The
	 * state also follows the window when it changes some other way.
	 */
	setFullscreen: (on: boolean) => void;
}
 
export const FullscreenContext = createContext<FullscreenContextValue>({
	isFullscreen: false,
	setFullscreen: () => {},
});
 
export const FullscreenProvider = ({ children }: { children: React.ReactNode }) => {
	const [isFullscreen, setIsFullscreen] = useState(false);
	// The app's own changes, so that a read of the window sent before one
	// can't undo it.
	const [changes] = useState(createChangeTracker);
	const followerRef = useRef<FullscreenFollower | null>(null);
	const setFullscreen = useCallback(
		(on: boolean) => {
			setIsFullscreen(on);
			const done = changes.begin();
			setWindowFullscreen(on)
				.catch((e) => {
					console.error(`[fullscreen] setFullscreen(${on}) failed:`, e);
				})
				.finally(() => {
					done();
					// macOS can turn the change down, and then the window
					// doesn't resize, so nothing else would read it.
					setTimeout(() => followerRef.current?.recheck(), RECHECK_MS);
				});
		},
		[changes]
	);
	// The window can go in or out of full screen without the app, such as
	// with the green button on macOS. A change asked for during the animation
	// of another one only lands when that ends. Following the window keeps
	// the sidebar, the button and Escape in step with what's on screen.
	useEffect(() => {
		const follower = followWindowFullscreen(setIsFullscreen, changes);
		followerRef.current = follower;
		return () => {
			follower.stop();
			followerRef.current = null;
		};
	}, [changes]);
	const value = useMemo(() => ({ isFullscreen, setFullscreen }), [isFullscreen, setFullscreen]);
	return <FullscreenContext.Provider value={value}>{children}</FullscreenContext.Provider>;
};
 
export const useFullscreen = () => useContext(FullscreenContext);