diff --git a/.changes/playback-shortcuts-after-button-click.md b/.changes/playback-shortcuts-after-button-click.md new file mode 100644 index 000000000..0067f1a7a --- /dev/null +++ b/.changes/playback-shortcuts-after-button-click.md @@ -0,0 +1,10 @@ +--- +type: fix +area: playback +--- + +Keyboard shortcuts work again right after you click a button in the built-in +player controls. Previously the clicked button kept the keyboard: after +clicking fullscreen, Space left fullscreen instead of pausing, and the seek, +volume and mute keys did nothing until you clicked the video. Applies to the +shared controls in the web players and the Embedded MPV frame-copy engine. diff --git a/AGENTS.md b/AGENTS.md index 73276dca9..c62df3c28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -213,7 +213,18 @@ Key files: discarded on the first bar focus event it is asked about or on any `keydown`, a `pointerdown` inside the bar releases a keyboard pin, and a `keydown` bubbling out of a bar control re-pins it, since operating a - focused control produces no focus event. Contract: + focused control produces no focus event. A completed pointer click then + releases the focus it left on the control (`onBarClick` → + `ControlsSurface.releasePointerFocus`, attributed by `wasPointerClick`: + non-empty click `pointerType`, else a recent press inside the clicked + element), because a focused control captures the keyboard: Space and + Enter re-activated the clicked button and `ControlsShortcuts` yields to + any interactive element in the key's path, so after a click on fullscreen + Space left fullscreen instead of pausing. Keyboard activation (empty + `pointerType`) keeps focus, only buttons and range sliders are released, + Chromium keeps its sequential-focus starting point at the blurred control + so Tab continues from it, and the volume popover ignores the release's + `focusout` (`wasPointerFocusRelease`). Contract: `docs/architecture/player-controls-contract.md` (auto-hide paragraph). - Persisted `Settings.webPlayerSharedControls` is default-ON (absent stored values coerce with `!== false`; only an explicit false opts out to the legacy diff --git a/CLAUDE.md b/CLAUDE.md index 2c768697e..58bdce569 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1130,7 +1130,7 @@ engine` (restart required) or helper: `apps/electron-backend/native/helper/`; canonical packaging/runtime contracts: `docs/architecture/embedded-mpv-native.md` and `tools/embedded-mpv/README.md`. -- Shared player-controls layer: `libs/ui/playback/src/lib/player-controls/` exports the engine-neutral `PlayerController` contract, standalone `app-player-controls`, a generic web-video adapter/helper, and component-scoped `WEB_PLAYER_SHARED_CONTROLS` rollout token. Its subtitle menu carries capability-gated advanced subtitle support (#1408): external subtitle file loading, a ±0.5 s timing-offset row, and size/color styling persisted in the shared `subtitleStyle` localStorage key. HTML5/ArtPlayer implement it through the neutral source bridge (`.srt`/`.vtt` via a DOM file picker with encoding detection, native `TextTrack` rendering, `::cue` styling, delay only while the loaded file is the selected track; picks are source-generation-guarded and engine deselection precedes external track activation); the canonical style shape and clamp/normalize rules are shared with the main process via `@iptvnator/shared/interfaces` (`subtitle-style.util.ts`). Embedded MPV frame-copy implements it through new helper protocol commands (`sub-add`/`sub-delay`/`sub-scale`/`sub-color`, main-process file dialog, ASS supported, delay for all tracks). Video.js shared mode, vendor-chrome paths, native-view, and the Linux out-of-process path advertise no such capability and render no UI. Contract details: `docs/architecture/player-controls-contract.md` ("Advanced subtitle support"). Shared controls include a per-session quality menu (Auto + “1080p”-style levels via `setQualityLevel`; `AUTO_QUALITY_LEVEL_ID` restores ABR): the capability derives from the manifest — advertised only when the source exposes >1 video rendition (multi-variant HLS via hls.js `nextLevel`/`manualLevel`, DASH via Shaka variant tracks pinned to the active variant's exact audio stream (`audioId`, language fallback) with ABR toggled off for manual picks, Video.js via videojs-contrib-quality-levels) — so single-bitrate VOD and raw MPEG-TS never show it, nothing persists to Settings, and Embedded MPV/external players report the capability false. In fullscreen, `app-player-controls` shows a pointer-transparent media-title overlay at the top while controls are revealed (`mediaTitle` input: movie/channel/series name, plus an `S01E03` second line for episodes; series names flow from the detail views through `PortalInlinePlayerComponent.seriesTitle` and `WebPlayerViewComponent.mediaTitle`). Persisted `Settings.webPlayerSharedControls` is default-ON (absent stored values coerce with `!== false` in every normalization site; only an explicit false — the Settings > Playback checkbox — opts out to the legacy vendor chrome), and its checkbox appears only when HTML5, Video.js, or ArtPlayer is selected. The shared surface has explicit touch semantics (`ControlsSurface.wasTouchInteraction`): viewport taps toggle overlay visibility instead of pausing, the volume popover opens on tap instead of hover, coarse pointers get a taller scrub strip, and at container widths ≤640px the bar reflows to two rows (full-width timeline above transport + an end-aligned, wrapping actions cluster with 40px buttons whose panels remain unclipped). Only keyboard-originated focus pins the bar open: Chromium also focuses a clicked `