diff --git a/components/apps/Photos/index.tsx b/components/apps/Photos/index.tsx index 1d784db4..af5e1468 100644 --- a/components/apps/Photos/index.tsx +++ b/components/apps/Photos/index.tsx @@ -7,7 +7,6 @@ import { ZoomOut, } from "components/apps/Photos/PhotoIcons"; import StyledPhotos from "components/apps/Photos/StyledPhotos"; -import useFullscreen from "components/apps/Photos/useFullscreen"; import usePanZoom, { panZoomConfig } from "components/apps/Photos/usePanZoom"; import type { ComponentProcessProps } from "components/system/Apps/RenderComponent"; import useFileDrop from "components/system/Files/FileManager/useFileDrop"; @@ -33,6 +32,7 @@ import { imgDataToBuffer, label, } from "utils/functions"; +import { useViewport } from "contexts/viewport"; const { maxScale, minScale } = panZoomConfig; @@ -94,7 +94,7 @@ const Photos: FC = ({ id }) => { imageRef.current, imageContainerRef.current ); - const { fullscreen, toggleFullscreen } = useFullscreen(containerRef.current); + const { fullscreenElement, toggleFullscreen } = useViewport(); const loadPhoto = useCallback(async (): Promise => { let fileContents: Buffer | string = await readFile(url); const ext = getExtension(url); @@ -225,10 +225,14 @@ const Photos: FC = ({ id }) => { diff --git a/components/apps/Photos/useFullscreen.ts b/components/apps/Photos/useFullscreen.ts deleted file mode 100644 index 28715fc8..00000000 --- a/components/apps/Photos/useFullscreen.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { useEffect, useState } from "react"; - -type Fullscreen = { - fullscreen: boolean; - toggleFullscreen: (navigationUI?: FullscreenNavigationUI) => void; -}; - -type FullscreenDocument = Document & { - mozCancelFullScreen: () => Promise; - mozFullScreenElement: HTMLElement; - mozFullScreenEnabled: boolean; - webkitExitFullscreen: () => Promise; - webkitFullscreenElement: HTMLElement; - webkitFullscreenEnabled: boolean; -}; - -type FullscreenElement = HTMLElement & { - mozRequestFullScreen?: (options?: FullscreenOptions) => Promise; - webkitRequestFullscreen?: (options?: FullscreenOptions) => Promise; -}; - -const isFullscreen = (): boolean => { - const { fullscreenElement, mozFullScreenElement, webkitFullscreenElement } = - document as FullscreenDocument; - - return Boolean( - fullscreenElement || mozFullScreenElement || webkitFullscreenElement - ); -}; - -const useFullscreen = (element?: HTMLElement | null): Fullscreen => { - const [fullscreen, setFullscreen] = useState(false); - const toggleFullscreen = async ( - navigationUI?: FullscreenNavigationUI - ): Promise => { - if (fullscreen) { - const fullscreenDocument = document as FullscreenDocument; - - try { - if (fullscreenDocument.exitFullscreen) { - await fullscreenDocument.exitFullscreen(); - } else if (fullscreenDocument.mozCancelFullScreen) { - await fullscreenDocument.mozCancelFullScreen(); - } else if (fullscreenDocument.webkitExitFullscreen) { - await fullscreenDocument.webkitExitFullscreen(); - } - } catch { - // Ignore failure while exiting fullscreen - } - } else { - const fullScreenElement = (element || - document.documentElement) as FullscreenElement; - const fullscreenOptions: FullscreenOptions = { - navigationUI: navigationUI || "hide", - }; - - try { - if (fullScreenElement.requestFullscreen) { - await fullScreenElement.requestFullscreen(fullscreenOptions); - } else if (fullScreenElement.mozRequestFullScreen) { - await fullScreenElement.mozRequestFullScreen(fullscreenOptions); - } else if (fullScreenElement.webkitRequestFullscreen) { - await fullScreenElement.webkitRequestFullscreen(fullscreenOptions); - } - } catch { - // Ignore failure while entering fullscreen - } - } - }; - - useEffect(() => { - const monitorFullscreenState = (): void => setFullscreen(isFullscreen()); - - document.addEventListener("fullscreenchange", monitorFullscreenState); - - return () => - document.removeEventListener("fullscreenchange", monitorFullscreenState); - }, []); - - return { - fullscreen, - toggleFullscreen, - }; -}; - -export default useFullscreen; diff --git a/components/system/Taskbar/useTaskbarContextMenu.ts b/components/system/Taskbar/useTaskbarContextMenu.ts index 1bf7fbae..dc6c1cee 100644 --- a/components/system/Taskbar/useTaskbarContextMenu.ts +++ b/components/system/Taskbar/useTaskbarContextMenu.ts @@ -1,5 +1,4 @@ import { useMemo } from "react"; -import useFullscreen from "components/apps/Photos/useFullscreen"; import { useMenu } from "contexts/menu"; import type { ContextMenuCapture, @@ -9,12 +8,13 @@ import { useProcesses } from "contexts/process"; import { useProcessesRef } from "hooks/useProcessesRef"; import { MENU_SEPERATOR } from "utils/constants"; import { toggleShowDesktop } from "utils/functions"; +import { useViewport } from "contexts/viewport"; const useTaskbarContextMenu = (onStartButton = false): ContextMenuCapture => { const { contextMenu } = useMenu(); const { minimize, open } = useProcesses(); const processesRef = useProcessesRef(); - const { fullscreen, toggleFullscreen } = useFullscreen(); + const { fullscreenElement, toggleFullscreen } = useViewport(); return useMemo( () => @@ -54,7 +54,10 @@ const useTaskbarContextMenu = (onStartButton = false): ContextMenuCapture => { menuItems.unshift( { action: () => toggleFullscreen(), - label: fullscreen ? "Exit full screen" : "Enter full screen", + label: + fullscreenElement === document.documentElement + ? "Exit full screen" + : "Enter full screen", }, MENU_SEPERATOR ); @@ -64,7 +67,7 @@ const useTaskbarContextMenu = (onStartButton = false): ContextMenuCapture => { }), [ contextMenu, - fullscreen, + fullscreenElement, minimize, onStartButton, open, diff --git a/contexts/viewport/index.tsx b/contexts/viewport/index.tsx new file mode 100644 index 00000000..7ab1e722 --- /dev/null +++ b/contexts/viewport/index.tsx @@ -0,0 +1,6 @@ +import useViewportContextState from "contexts/viewport/useViewportContextState"; +import contextFactory from "contexts/contextFactory"; + +const { Provider, useContext } = contextFactory(useViewportContextState); + +export { Provider as ViewportProvider, useContext as useViewport }; diff --git a/contexts/viewport/types.ts b/contexts/viewport/types.ts new file mode 100644 index 00000000..e9ea88ad --- /dev/null +++ b/contexts/viewport/types.ts @@ -0,0 +1,26 @@ +export type ViewportContextState = { + fullscreenElement: Element | null; + toggleFullscreen: ( + element?: HTMLElement | null, + navigationUI?: FullscreenNavigationUI + ) => void; +}; + +export type FullscreenDocument = Document & { + mozCancelFullScreen: () => Promise; + mozFullScreenElement: Element | null; + webkitExitFullscreen: () => Promise; + webkitFullscreenElement: Element | null; +}; + +export type FullscreenElement = HTMLElement & { + mozRequestFullScreen?: (options?: FullscreenOptions) => Promise; + webkitRequestFullscreen?: (options?: FullscreenOptions) => Promise; +}; + +export type NavigatorWithKeyboard = Navigator & { + keyboard?: { + lock?: (keys?: string[]) => Promise; + unlock?: () => void; + }; +}; diff --git a/contexts/viewport/useViewportContextState.ts b/contexts/viewport/useViewportContextState.ts new file mode 100644 index 00000000..52f7e794 --- /dev/null +++ b/contexts/viewport/useViewportContextState.ts @@ -0,0 +1,106 @@ +import { useEffect, useState } from "react"; +import type { + ViewportContextState, + FullscreenDocument, + NavigatorWithKeyboard, + FullscreenElement, +} from "contexts/viewport/types"; +import { isFirefox } from "utils/functions"; + +const FULLSCREEN_LOCKED_KEYS = ["MetaLeft", "MetaRight", "Escape"]; + +const enterFullscreen = async ( + element: FullscreenElement, + options: FullscreenOptions +): Promise => { + try { + if (element.requestFullscreen) { + await element.requestFullscreen(options); + } else if (element.mozRequestFullScreen) { + await element.mozRequestFullScreen(options); + } else if (element.webkitRequestFullscreen) { + await element.webkitRequestFullscreen(options); + } + } catch { + // Ignore failure while entering fullscreen + } +}; + +const exitFullscreen = async (): Promise => { + const fullscreenDocument = document as FullscreenDocument; + + try { + if (fullscreenDocument.exitFullscreen) { + await fullscreenDocument.exitFullscreen(); + } else if (fullscreenDocument.mozCancelFullScreen) { + await fullscreenDocument.mozCancelFullScreen(); + } else if (fullscreenDocument.webkitExitFullscreen) { + await fullscreenDocument.webkitExitFullscreen(); + } + } catch { + // Ignore failure while exiting fullscreen + } +}; + +const toggleKeyboardLock = async ( + fullscreenElement: Element | null +): Promise => { + try { + if (fullscreenElement) { + await (navigator as NavigatorWithKeyboard)?.keyboard?.lock?.( + FULLSCREEN_LOCKED_KEYS + ); + } else { + (navigator as NavigatorWithKeyboard)?.keyboard?.unlock?.(); + } + } catch { + // Ignore failure to lock keys + } +}; + +const useViewportContextState = (): ViewportContextState => { + const [fullscreenElement, setFullscreenElement] = useState( + // eslint-disable-next-line unicorn/no-null + null + ); + const toggleFullscreen = async ( + element?: HTMLElement | null, + navigationUI?: FullscreenNavigationUI + ): Promise => { + if (fullscreenElement && (!element || element === fullscreenElement)) { + await exitFullscreen(); + } else { + if (fullscreenElement && isFirefox()) await exitFullscreen(); + + await enterFullscreen(element || document.documentElement, { + navigationUI: navigationUI || "hide", + }); + } + }; + + useEffect(() => { + const onFullscreenChange = (): void => { + const { mozFullScreenElement, webkitFullscreenElement } = + document as FullscreenDocument; + const currentFullscreenElement = + document.fullscreenElement || + mozFullScreenElement || + webkitFullscreenElement; + + toggleKeyboardLock(currentFullscreenElement).then(() => + setFullscreenElement(currentFullscreenElement) + ); + }; + + document.addEventListener("fullscreenchange", onFullscreenChange, { + passive: true, + }); + + return () => + document.removeEventListener("fullscreenchange", onFullscreenChange); + }, []); + + return { fullscreenElement, toggleFullscreen }; +}; + +export default useViewportContextState; diff --git a/hooks/useGlobalKeyboardShortcuts.ts b/hooks/useGlobalKeyboardShortcuts.ts index 86bff2e2..0d96e78e 100644 --- a/hooks/useGlobalKeyboardShortcuts.ts +++ b/hooks/useGlobalKeyboardShortcuts.ts @@ -1,16 +1,9 @@ import { useEffect, useRef } from "react"; -import useFullscreen from "components/apps/Photos/useFullscreen"; import { useProcesses } from "contexts/process"; import { useSession } from "contexts/session"; import { useProcessesRef } from "hooks/useProcessesRef"; import { haltEvent, toggleShowDesktop } from "utils/functions"; - -type NavigatorWithKeyboard = Navigator & { - keyboard?: { - lock?: (keys?: string[]) => void; - unlock?: () => void; - }; -}; +import { useViewport } from "contexts/viewport"; const openStartMenu = (): void => ( @@ -42,7 +35,7 @@ const useGlobalKeyboardShortcuts = (): void => { const { closeWithTransition, maximize, minimize, open } = useProcesses(); const processesRef = useProcessesRef(); const { foregroundId } = useSession(); - const { fullscreen, toggleFullscreen } = useFullscreen(); + const { fullscreenElement, toggleFullscreen } = useViewport(); const altBindingsRef = useRef void>>({}); const shiftBindingsRef = useRef void>>({ E: () => open("FileExplorer"), @@ -82,17 +75,14 @@ const useGlobalKeyboardShortcuts = (): void => { ); } else if (ctrlKey && altKey && altBindingsRef.current?.[keyName]) { altBindingsRef.current?.[keyName]?.(); - } else if (fullscreen) { + } else if (fullscreenElement === document.documentElement) { if (keyName === "META") metaDown = true; else if (altKey && altBindingsRef.current?.[keyName]) { haltEvent(event); altBindingsRef.current?.[keyName]?.(); } else if (keyName === "ESCAPE") { - setTimeout( - // eslint-disable-next-line unicorn/consistent-destructuring - () => !event.defaultPrevented && document.exitFullscreen(), - 0 - ); + if (document.pointerLockElement) document.exitPointerLock(); + else toggleFullscreen(); } else if ( metaDown && metaCombos.has(keyName) && @@ -105,29 +95,16 @@ const useGlobalKeyboardShortcuts = (): void => { } }; const onKeyUp = (event: KeyboardEvent): void => { - if (metaDown && fullscreen && event.key?.toUpperCase() === "META") { + if ( + metaDown && + fullscreenElement === document.documentElement && + event.key?.toUpperCase() === "META" + ) { metaDown = false; if (metaComboUsed) metaComboUsed = false; else openStartMenu(); } }; - const onFullScreen = ({ target }: Event): void => { - if (target === document.documentElement) { - try { - if (fullscreen) { - (navigator as NavigatorWithKeyboard)?.keyboard?.lock?.([ - "MetaLeft", - "MetaRight", - "Escape", - ]); - } else { - (navigator as NavigatorWithKeyboard)?.keyboard?.unlock?.(); - } - } catch { - // Ignore failure to lock keys - } - } - }; document.addEventListener("keydown", onKeyDown, { capture: true, @@ -136,16 +113,16 @@ const useGlobalKeyboardShortcuts = (): void => { capture: true, passive: true, }); - document.addEventListener("fullscreenchange", onFullScreen, { - passive: true, - }); return () => { - document.removeEventListener("keydown", onKeyDown); - document.removeEventListener("keyup", onKeyUp); - document.removeEventListener("fullscreenchange", onFullScreen); + document.removeEventListener("keydown", onKeyDown, { + capture: true, + }); + document.removeEventListener("keyup", onKeyUp, { + capture: true, + }); }; - }, [fullscreen, toggleFullscreen]); + }, [fullscreenElement, toggleFullscreen]); useEffect(() => { altBindingsRef.current = { diff --git a/pages/_app.tsx b/pages/_app.tsx index 3b9d9fec..2478d195 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -6,22 +6,25 @@ import { FileSystemProvider } from "contexts/fileSystem"; import { MenuProvider } from "contexts/menu"; import { ProcessProvider } from "contexts/process"; import { SessionProvider } from "contexts/session"; +import { ViewportProvider } from "contexts/viewport"; const App = ({ Component, pageProps }: AppProps): React.ReactElement => ( - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); export default App;