useServices is already typed via src/services/ServicesContext/useServices.d.ts,
so the @ts-expect-error suppression on the import was unnecessary and
masked two real type holes that surfaced once it was removed:
- core.transport.getState('ctx') returns Promise<object>; cast to the
ambient Ctx type so escExitFullscreen is read through a typed path.
- CoreTransport.on/off types listeners as () => void, but the 'NewState'
event actually emits a string[]. Use a (...args: unknown[]) wrapper +
Array.isArray narrowing so the call site stays type-safe without
weakening the ambient transport signature.
No behavior change.
Made-with: Cursor
FullscreenProvider sits above the router, but useSettings() ->
useProfile() -> useModelState() requires CoreSuspenderContext which is
only provided by withCoreSuspender below the router. Mounting the
provider therefore crashed with "Cannot read properties of null
(reading 'getState')".
Switch the provider to read profile.settings.escExitFullscreen directly
from core.transport.getState('ctx') and refresh on the 'NewState' event
when 'ctx' changes. core is available via useServices(), whose provider
sits at the very top of the tree and is always reachable here.
Behavior is preserved: ESC still exits fullscreen iff the user has the
escExitFullscreen setting enabled, and updates to that setting from the
Settings tab take effect on the next ctx NewState push.
Made-with: Cursor
useFullscreen is now a thin useContext consumer of FullscreenProvider,
so all callers share a single fullscreen state owned by the app root.
Why this fixes the desync bug:
stremio-router keeps multiple route layers mounted at once, and each
top-level route (Board, Discover, Library, Calendar, Addons, Settings,
Search) renders its own MainNavBars -> HorizontalNavBar -> useFullscreen.
The previous hook held local useState plus its own listeners, so each
route had an independent boolean. Entering fullscreen, then navigating
to another tab, mounted a fresh hook initialized to false; the icon
flipped back to "enter fullscreen" and clicking it re-requested
fullscreen on top of the existing one, leaving the UI unresponsive
until a route remount happened to coincide with reality.
With one provider above the router, state outlives route remounts and
listeners are attached exactly once. The hook's return tuple shape
([fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]) is
preserved, so all three call sites (HorizontalNavBar, NavMenuContent,
Player) keep working with no API change.
Also removes the legacy src/common/useFullscreen.ts and routes its
imports through stremio/common/Fullscreen (and the stremio/common
barrel for App.js / Player).
Note: MainNavBars is still rendered per-route. Lifting it to a single
app-level layout above the router is a worthwhile follow-up (eliminates
6+ duplicate mounts) but carries non-trivial CSS / useRouteFocused /
stacked-route risk and is out of scope for this PR; tracking separately.
Made-with: Cursor
Wrap the router with <FullscreenProvider> so a single provider instance
spans the whole app lifetime. The legacy useFullscreen hook is still
intact and continues to drive consumers; the provider is in place but
not yet consumed. Splitting this from the consumer cutover keeps each
commit independently buildable.
Made-with: Cursor
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
The 'next-track' media-key handler called video.setTime(0) before
checking whether a next video existed. onNextVideoRequested() no-ops
when player.nextVideo is null, but the unconditional setTime(0) had
already rewound the current stream — causing movies (which have no
next video) to restart from the beginning when the Windows next-track
media key was pressed.
Guard both calls with the same player.nextVideo check that the
navigator.mediaSession 'nexttrack' handler already uses.