feat(common): add FullscreenProvider + context module

Introduce a single, app-root-owned source of truth for fullscreen state,
mirroring the existing provider pattern (ToastProvider, FileDropProvider).
The provider centralizes the fullscreenchange / win-visibility-changed /
keydown listeners and exposes the same [fullscreen, requestFullscreen,
exitFullscreen, toggleFullscreen] tuple that consumers already destructure.

Not yet wired up - both the legacy src/common/useFullscreen hook and the
new module coexist. Subsequent commits mount the provider in App.js and
switch consumers over.

Made-with: Cursor
This commit is contained in:
AK 2026-04-27 01:50:14 -04:00
parent e2177938d1
commit 60df6860d7
4 changed files with 140 additions and 0 deletions

View file

@ -0,0 +1,20 @@
// Copyright (C) 2017-2023 Smart code 203358507
import { createContext } from 'react';
export type FullscreenContextValue = readonly [
boolean,
() => Promise<void> | void,
() => void,
() => void,
];
const noop = () => { /* no-op */ };
const defaultValue: FullscreenContextValue = [false, noop, noop, noop];
const FullscreenContext = createContext<FullscreenContextValue>(defaultValue);
FullscreenContext.displayName = 'FullscreenContext';
export default FullscreenContext;

View file

@ -0,0 +1,105 @@
// Copyright (C) 2017-2023 Smart code 203358507
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import useShell, { type WindowVisibility } from '../useShell';
import useSettings from '../useSettings';
import FullscreenContext, { type FullscreenContextValue } from './FullscreenContext';
type Props = {
children: React.ReactNode,
};
// Single source of truth for fullscreen state. Mounted once at the app root so
// the value survives route remounts (fixes desync where switching tabs while in
// fullscreen would leave the UI thinking we were still windowed).
const FullscreenProvider = ({ children }: Props) => {
const shell = useShell();
const [settings] = useSettings();
const [fullscreen, setFullscreen] = useState<boolean>(() => {
if (typeof document === 'undefined') return false;
return document.fullscreenElement === document.documentElement;
});
const requestFullscreen = useCallback(async () => {
if (shell.active) {
shell.send('win-set-visibility', { fullscreen: true });
} else {
try {
await document.documentElement.requestFullscreen();
} catch (err) {
console.error('Error enabling fullscreen', err);
}
}
}, [shell]);
const exitFullscreen = useCallback(() => {
if (shell.active) {
shell.send('win-set-visibility', { fullscreen: false });
} else {
if (document.fullscreenElement === document.documentElement) {
document.exitFullscreen();
}
}
}, [shell]);
const toggleFullscreen = useCallback(() => {
fullscreen ? exitFullscreen() : requestFullscreen();
}, [fullscreen, exitFullscreen, requestFullscreen]);
useEffect(() => {
const onWindowVisibilityChanged = (state: WindowVisibility) => {
setFullscreen(state.isFullscreen === true);
};
const onFullscreenChange = () => {
setFullscreen(document.fullscreenElement === document.documentElement);
};
const onKeyDown = (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement;
const inputFocused =
activeElement &&
(activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.tagName === 'SELECT' ||
activeElement.isContentEditable);
if (event.code === 'Escape' && settings.escExitFullscreen) {
exitFullscreen();
}
if (event.code === 'KeyF' && !inputFocused) {
toggleFullscreen();
}
if (event.code === 'F11' && shell.active) {
toggleFullscreen();
}
};
shell.on('win-visibility-changed', onWindowVisibilityChanged);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('fullscreenchange', onFullscreenChange);
return () => {
shell.off('win-visibility-changed', onWindowVisibilityChanged);
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('fullscreenchange', onFullscreenChange);
};
}, [shell, settings.escExitFullscreen, toggleFullscreen, exitFullscreen]);
const value = useMemo<FullscreenContextValue>(
() => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen],
[fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]
);
return (
<FullscreenContext.Provider value={value}>
{children}
</FullscreenContext.Provider>
);
};
export default FullscreenProvider;

View file

@ -0,0 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
import FullscreenProvider from './FullscreenProvider';
import useFullscreen from './useFullscreen';
export { FullscreenProvider, useFullscreen };
export default useFullscreen;

View file

@ -0,0 +1,8 @@
// Copyright (C) 2017-2023 Smart code 203358507
import { useContext } from 'react';
import FullscreenContext from './FullscreenContext';
const useFullscreen = () => useContext(FullscreenContext);
export default useFullscreen;