diff --git a/.changes/playback-fullscreen-panel-hover.md b/.changes/playback-fullscreen-panel-hover.md new file mode 100644 index 000000000..36b9c34f1 --- /dev/null +++ b/.changes/playback-fullscreen-panel-hover.md @@ -0,0 +1,6 @@ +--- +type: fix +area: playback +--- + +The fullscreen channel panel stays open under the pointer when its opening animation is delayed, instead of closing before the list appears. diff --git a/.changes/playlists-refresh-catalog-read.md b/.changes/playlists-refresh-catalog-read.md new file mode 100644 index 000000000..aaa852144 --- /dev/null +++ b/.changes/playlists-refresh-catalog-read.md @@ -0,0 +1,6 @@ +--- +type: fix +area: playlists +--- + +Opening an M3U playlist immediately after refreshing now waits for its pending save, so the channel list does not revert to the previous catalog. diff --git a/AGENTS.md b/AGENTS.md index eaf34a452..6cce0d3ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -406,6 +406,9 @@ Key files: ## M3U URL User-Agent +- `PlaylistsService.getPlaylist()` joins the per-playlist mutation queue so a + route opened during refresh reads after its pending save. Mutation-internal + reads keep using `getPlaylistById()` directly to avoid queue re-entry. - The URL import form accepts an optional User-Agent and stores it as `Playlist.userAgent`. Electron sends it on initial download, manual refresh, and startup auto-update. The self-hosted PWA sends it through the registered @@ -553,7 +556,9 @@ Key files: first unknown probe and confirmed native/unsupported results withhold it. A live host provides `FULLSCREEN_CHANNEL_PANEL` (`panelTemplate` + optional `panelTitle`) and the panel slides that list over the video: left-edge hover - dwell, a touch tap on that edge, or `C`. Nothing is drawn while it is closed + dwell, a touch tap on that edge, or `C`. The hot zone stays mounted above the + scrim and below the panel during opening, so a delayed paint cannot turn + stationary hover into a synthetic leave. Nothing is drawn while it is closed (no handle), the hot zone stops above the controls bar, and scrim/Escape/ mouse-leave close it — while a CDK overlay opened from the list counts as the panel, so hover keeps it open and Escape closes the overlay first. The diff --git a/CLAUDE.md b/CLAUDE.md index 3cb2744a9..e527f9e96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -855,6 +855,9 @@ panel's clock, not the viewer's"). #### M3U URL User-Agent +- `PlaylistsService.getPlaylist()` joins the per-playlist mutation queue so a + route opened during refresh reads after its pending save. Mutation-internal + reads keep using `getPlaylistById()` directly to avoid queue re-entry. - The URL import form accepts an optional User-Agent and stores it as `Playlist.userAgent`. Electron sends it on initial download, manual refresh, and startup auto-update. The self-hosted PWA sends it through the registered @@ -1274,7 +1277,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 `