Merge pull request #1237 from AKnassa/fix/fullscreen-state-desync-on-route-change

App: Correctly display fullscreen status
This commit is contained in:
Timothy Z. 2026-05-01 18:41:19 +03:00 committed by GitHub
commit 6fc21e314d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 170 additions and 106 deletions

View file

@ -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 = () => {
<FileDropProvider className={styles['file-drop-container']}>
<GamepadProvider enabled={gamepadSupportEnabled} onGuide={toggleGamepadModal}>
<ShortcutsProvider onShortcut={onShortcut}>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
{
gamepadModalOpen && <GamepadModal onClose={closeGamepadModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
<FullscreenProvider>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
{
gamepadModalOpen && <GamepadModal onClose={closeGamepadModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</FullscreenProvider>
</ShortcutsProvider>
</GamepadProvider>
</FileDropProvider>

View file

@ -0,0 +1,16 @@
// Copyright (C) 2017-2026 Smart code 203358507
import { createContext } from 'react';
export type FullscreenContextValue = readonly [
fullscreen: boolean,
requestFullscreen: () => Promise<void> | void,
exitFullscreen: () => void,
toggleFullscreen: () => void,
];
const FullscreenContext = createContext<FullscreenContextValue | null>(null);
FullscreenContext.displayName = 'FullscreenContext';
export default FullscreenContext;

View file

@ -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<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]);
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<FullscreenContextValue>(
() => [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen],
[fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]
);
return (
<FullscreenContext.Provider value={value}>
{children}
</FullscreenContext.Provider>
);
};
export default withCoreSuspender(FullscreenProvider);

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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');

View file

@ -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');

View file

@ -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();