diff --git a/src/App/App.js b/src/App/App.js index eecca48ea..375c1f5db 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common'); +const { FileDropProvider, FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); @@ -231,21 +231,23 @@ const App = () => { - { - shortcutModalOpen && - } - { - gamepadModalOpen && - } - - - - - + + { + shortcutModalOpen && + } + { + gamepadModalOpen && + } + + + + + + diff --git a/src/common/Fullscreen/FullscreenContext.ts b/src/common/Fullscreen/FullscreenContext.ts new file mode 100644 index 000000000..1c9599ffb --- /dev/null +++ b/src/common/Fullscreen/FullscreenContext.ts @@ -0,0 +1,16 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import { createContext } from 'react'; + +export type FullscreenContextValue = readonly [ + fullscreen: boolean, + requestFullscreen: () => Promise | void, + exitFullscreen: () => void, + toggleFullscreen: () => void, +]; + +const FullscreenContext = createContext(null); + +FullscreenContext.displayName = 'FullscreenContext'; + +export default FullscreenContext; diff --git a/src/common/Fullscreen/FullscreenProvider.tsx b/src/common/Fullscreen/FullscreenProvider.tsx new file mode 100644 index 000000000..2300602c5 --- /dev/null +++ b/src/common/Fullscreen/FullscreenProvider.tsx @@ -0,0 +1,109 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { withCoreSuspender } from '../CoreSuspender'; +import onShortcut from '../Shortcuts/onShortcut'; +import useSettings from '../useSettings'; +import useShell, { type WindowVisibility } from '../useShell'; +import FullscreenContext, { type FullscreenContextValue } from './FullscreenContext'; + +type Props = { + children: React.ReactNode, +}; + +const isTextInputFocused = () => { + const activeElement = document.activeElement; + + return activeElement instanceof HTMLElement && + (activeElement.tagName === 'INPUT' || + activeElement.tagName === 'TEXTAREA' || + activeElement.tagName === 'SELECT' || + activeElement.isContentEditable); +}; + +const FullscreenProvider = ({ children }: Props) => { + const shell = useShell(); + const [settings] = useSettings(); + const escExitFullscreen = settings.escExitFullscreen; + + const [fullscreen, setFullscreen] = useState(() => { + 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]); + + const toggleFullscreenFromShortcut = useCallback(() => { + if (isTextInputFocused()) return; + toggleFullscreen(); + }, [toggleFullscreen]); + + onShortcut('fullscreen', toggleFullscreenFromShortcut, [toggleFullscreenFromShortcut]); + + useEffect(() => { + const onWindowVisibilityChanged = (state: WindowVisibility) => { + setFullscreen(state.isFullscreen === true); + }; + + const onFullscreenChange = () => { + setFullscreen(document.fullscreenElement === document.documentElement); + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.code === 'Escape' && escExitFullscreen) { + exitFullscreen(); + } + + 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, toggleFullscreen, exitFullscreen, escExitFullscreen]); + + const value = useMemo( + () => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen], + [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen] + ); + + return ( + + {children} + + ); +}; + +export default withCoreSuspender(FullscreenProvider); diff --git a/src/common/Fullscreen/index.ts b/src/common/Fullscreen/index.ts new file mode 100644 index 000000000..db65974ac --- /dev/null +++ b/src/common/Fullscreen/index.ts @@ -0,0 +1,7 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import FullscreenProvider from './FullscreenProvider'; +import useFullscreen from './useFullscreen'; + +export { FullscreenProvider, useFullscreen }; +export default useFullscreen; diff --git a/src/common/Fullscreen/useFullscreen.ts b/src/common/Fullscreen/useFullscreen.ts new file mode 100644 index 000000000..5cee0a801 --- /dev/null +++ b/src/common/Fullscreen/useFullscreen.ts @@ -0,0 +1,15 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import { useContext } from 'react'; +import FullscreenContext from './FullscreenContext'; + +const useFullscreen = () => { + const value = useContext(FullscreenContext); + if (value === null) { + throw new Error('useFullscreen must be used inside FullscreenProvider'); + } + + return value; +}; + +export default useFullscreen; diff --git a/src/common/index.js b/src/common/index.js index e608e7e23..1963e8995 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const { FileDropProvider, onFileDrop } = require('./FileDrop'); +const { FullscreenProvider, useFullscreen } = require('./Fullscreen'); const { PlatformProvider, usePlatform } = require('./Platform'); const { ToastProvider, useToast } = require('./Toast'); const { TooltipProvider, Tooltip } = require('./Tooltips'); @@ -14,7 +15,6 @@ const languages = require('./languages'); const routesRegexp = require('./routesRegexp'); const useAnimationFrame = require('./useAnimationFrame'); const useBinaryState = require('./useBinaryState'); -const { default: useFullscreen } = require('./useFullscreen'); const { default: useInterval } = require('./useInterval'); const useLiveRef = require('./useLiveRef'); const useModelState = require('./useModelState'); @@ -34,6 +34,7 @@ const { default: useLanguageSorting } = require('./useLanguageSorting'); module.exports = { FileDropProvider, onFileDrop, + FullscreenProvider, PlatformProvider, usePlatform, ShortcutsProvider, diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts deleted file mode 100644 index 8a1692254..000000000 --- a/src/common/useFullscreen.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -import { useCallback, useEffect, useState } from 'react'; -import useShell, { type WindowVisibility } from './useShell'; -import useSettings from './useSettings'; - -const useFullscreen = () => { - const shell = useShell(); - const [settings] = useSettings(); - - const [fullscreen, setFullscreen] = useState(false); - - 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); - } - } - }, []); - - const exitFullscreen = useCallback(() => { - if (shell.active) { - shell.send('win-set-visibility', { fullscreen: false }); - } else { - if (document.fullscreenElement === document.documentElement) { - document.exitFullscreen(); - } - } - }, []); - - const toggleFullscreen = useCallback(() => { - fullscreen ? exitFullscreen() : requestFullscreen(); - }, [fullscreen]); - - 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); - }; - }, [settings.escExitFullscreen, toggleFullscreen]); - - return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]; -}; - -export default useFullscreen; diff --git a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js index c6c02ab46..b1644c2b3 100644 --- a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js +++ b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js @@ -5,7 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button, Image } = require('stremio/components'); -const { default: useFullscreen } = require('stremio/common/useFullscreen'); +const { useFullscreen } = require('stremio/common/Fullscreen'); const usePWA = require('stremio/common/usePWA'); const { useHorizontalNavGamepadNavigation } = require('stremio/services/GamepadNavigation'); const SearchBar = require('./SearchBar'); diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index b6309a44c..6615e5b76 100644 --- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -7,7 +7,7 @@ const { useTranslation } = require('react-i18next'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { useServices } = require('stremio/services'); const { Button } = require('stremio/components'); -const { default: useFullscreen } = require('stremio/common/useFullscreen'); +const { useFullscreen } = require('stremio/common/Fullscreen'); const useProfile = require('stremio/common/useProfile'); const usePWA = require('stremio/common/usePWA'); const { default: usePlayUrl } = require('stremio/common/usePlayUrl'); diff --git a/src/services/GamepadNavigation/useHorizontalNavGamepadNavigation.tsx b/src/services/GamepadNavigation/useHorizontalNavGamepadNavigation.tsx index 422489e3e..0d65a3fa1 100644 --- a/src/services/GamepadNavigation/useHorizontalNavGamepadNavigation.tsx +++ b/src/services/GamepadNavigation/useHorizontalNavGamepadNavigation.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useGamepad } from '../GamepadContext'; -import useFullscreen from 'stremio/common/useFullscreen'; +import useFullscreen from 'stremio/common/Fullscreen'; const useHorizontalNavGamepadNavigation = (gamepadHandlerId: string, enableGoBack: boolean) => { const gamepad = useGamepad();