1. Player.js — Added optional chaining (gamepad?.on/gamepad?.off) to prevent null crash
2. GamepadProvider.tsx — Toast messages now use t('GAMEPAD_CONNECTED') / t('GAMEPAD_DISCONNECTED') via i18next, added keys to en-US.json
3. useVerticalNavGamepadNavigation.tsx — Fixed event.nativeEvent?.spatialNavigationPrevented → event.spatialNavigationPrevented (native DOM events don't have nativeEvent)
4. GamepadProvider.tsx — Changed connectedGamepads from useState to useRef to avoid rAF effect restart cycle, removed it from effect deps, simplified the enabled guard
shortcuts.json already declares fullscreen -> F, but the provider was
listening for KeyF on its own keydown handler in parallel — both fired
on every F press, with the canonical shortcuts dispatch going nowhere.
Subscribe via onShortcut('fullscreen', toggleFullscreen, ...) and drop
the KeyF branch (plus the now-unused inputFocused check). Escape stays
local because its action is gated on the escExitFullscreen profile
setting; F11 stays local because it's shell-only and not in
shortcuts.json.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous NewState listener fired on every change to the ctx model —
notifications, search history, library sync, streaming-server URL — and
each fire triggered a getState('ctx') round-trip to the worker just to
re-read escExitFullscreen.
Switch to the CoreEvent / SettingsUpdated channel (same pattern App.js
uses for interfaceLanguage/quitOnClose), reading the new value straight
from the event payload. Initial seed still uses getState('ctx') once
on mount.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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
The C shortcut tracked enabled/disabled via a ref that was never reset
on stream change and was not updated when the user picked tracks via
the menu, so the toggle could become inverted or no-op until pressed
twice. Read selectedSubtitlesTrackId / selectedExtraSubtitlesTrackId
directly instead.
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