mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 06:06:18 -08:00
feat(playback): fullscreen episode panel for series playback (#1620)
Series playing in fullscreen get the same slide-in side panel the live channel list has, with season tabs and the episode list: rest the mouse on the left edge, click it, or press C; pick an episode and it plays inline without leaving fullscreen. - Panel contract: `FullscreenChannelPanelHost` gains optional `panelSearchEnabled` and `panelKind`; the template context gains `open`. Pointer/keyboard rules and the four live providers are unchanged. - Series host: `PortalInlinePlayerComponent` provides the token through `createEpisodePanelHost()` and stamps `app-fullscreen-episode-panel` (SeasonTabsComponent over rows with TMDB still or numeral tile, label, runtime, clamped overview, progress, watched check, now-playing marker; playing row centred on open). Episode clicks reuse the Up Next rail's inline path; season tab clicks reach the hosts' `onSeasonSelected` (Xtream TMDB season enrichment, Stalker lazy VOD load with a Retry row after a failed request). - Gates: `Settings.fullscreenChannelPanel` (label now covers both lists in all locales), episode content only, native-view Embedded MPV withheld by the view, external players excluded. - Inline-series e2e moved to `xtream-series-playback.e2e.ts` with shared Xtream helpers in a fixture; adds a fullscreen episode switch through the panel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
c110519412
commit
fa8ce26991
55 files changed
+2961
-570
No files matched your search
@@ -248,6 +248,16 @@ seasons, with per-episode watch-progress bars from playback positions.
|
||||
- Layering: the rail is an opaque panel rendered on top of the stage, so the
|
||||
ambient fill stays behind it and shows in the flexible gap between the
|
||||
docked player and the rail on very wide stages.
|
||||
- Fullscreen: the rail cannot show over a fullscreen video, so the same
|
||||
host offers the whole series — season tabs plus the selected season's
|
||||
episodes — as the slide-in side panel of the player's fullscreen surface.
|
||||
`PortalInlinePlayerComponent` provides `FULLSCREEN_CHANNEL_PANEL` for the
|
||||
nested view from the hosts' `seriesEpisodes` / `episodePlaybackPositions` /
|
||||
`seasonLoadStates` inputs, an episode picked there travels the same
|
||||
`upNextEpisodeSelected` output as a rail click, and a season tab picked
|
||||
there travels `episodePanelSeasonSelected` into the host's
|
||||
`onSeasonSelected`. Contract: "Fullscreen episode panel" in
|
||||
`docs/architecture/player-controls-contract.md`.
|
||||
|
||||
Season navigation inside `SeasonContainerComponent` uses season tabs
|
||||
(`SeasonTabsComponent`; a dropdown beyond 6 seasons) instead of the old
|
||||
|
||||
@@ -399,11 +399,21 @@ is rendered by `WebPlayerViewComponent` as a sibling of the engine and staged
|
||||
on the view's `fullscreenSurface` — the same host element every engine
|
||||
receives as `fullscreenTarget` — so it sits inside the fullscreen element and
|
||||
survives the engine remount a channel switch causes.
|
||||
It injects `FULLSCREEN_CHANNEL_PANEL` optionally: a live host provides
|
||||
`FullscreenChannelPanelHost` (`panelTemplate`, optional `panelTitle`) from its
|
||||
component `providers`, and the panel stamps that template into its body with a
|
||||
`FullscreenChannelPanelContext` of `{ searchTerm: Signal<string>, close }`.
|
||||
Without a provider — VOD detail pages, series playback — nothing renders. The
|
||||
It is a host-agnostic side panel — a channel list for live hosts, an episode
|
||||
list for series playback (see "Fullscreen episode panel" below) — and keeps
|
||||
its `FULLSCREEN_CHANNEL_PANEL` name for history. It injects the token
|
||||
optionally: a host provides `FullscreenChannelPanelHost` from its component
|
||||
`providers` — `panelTemplate`, optional `panelTitle`, optional
|
||||
`panelSearchEnabled` (default true; false drops the header's search field and
|
||||
shows the title as a text row in its place, keeping the header height) and
|
||||
optional `panelKind` (`'channels'` | `'episodes'`, only the accessible name
|
||||
of the list and the close button's label; every pointer and keyboard rule
|
||||
is identical for both) — and the panel stamps that template into its body
|
||||
with a `FullscreenChannelPanelContext` of
|
||||
`{ searchTerm: Signal<string>, open: Signal<boolean>, close }`. `open` lets a
|
||||
body that stays mounted between openings react to the panel coming up (the
|
||||
episode list centres its playing row on it). Without a provider — a movie in
|
||||
a VOD detail page — nothing renders. The
|
||||
host resolves the user preference itself: `Settings.fullscreenChannelPanel`
|
||||
(default on, Settings → Playback, offered for the web players while the
|
||||
shared controls are on and for Embedded MPV — the legacy vendor chrome
|
||||
@@ -527,7 +537,9 @@ the opening animation covers it. A delayed fullscreen paint therefore cannot
|
||||
turn a stationary edge hover into a synthetic mouse-leave that closes the
|
||||
panel. The zone stops above the controls bar (`bottom: max(25%, 140px)`) so
|
||||
the leftmost transport button never loses a click or tap to it. The `C` key
|
||||
opens it too and focuses the search field (hover does not steal focus).
|
||||
opens it too and focuses the search field — or, for a host without one, the
|
||||
panel itself (`tabindex="-1"`), so the next Tab reaches its first control
|
||||
(hover does not steal focus).
|
||||
Touch has neither hover nor a `C` key, so the tap path above is its way in:
|
||||
the handler is bound to `pointerup`, not `pointerdown`, so the hot zone is
|
||||
still the tap's click target and the click that follows dies on it instead
|
||||
@@ -575,6 +587,76 @@ without it every tooltip, sort menu and context menu opened while fullscreen —
|
||||
the panel's view-switcher tooltips and row context menus included — would sit
|
||||
invisibly under the top layer.
|
||||
|
||||
#### Fullscreen episode panel (series)
|
||||
|
||||
Series playback gets the same panel as an **episode list**. The provider is
|
||||
`PortalInlinePlayerComponent` (`libs/ui/playback/src/lib/portal-inline-player/`),
|
||||
the component both series hosts (Xtream `SerialDetailsComponent`, Stalker
|
||||
`StalkerSeriesViewComponent`) render around `app-web-player-view` and that
|
||||
already feeds the Up Next rail — so it is the nearest provider for the nested
|
||||
view, which also shields that view from a page-level channel-list provider
|
||||
(the M3U player's) while its VOD detail hosts the player. The host object
|
||||
itself is built by `createEpisodePanelHost()`
|
||||
(`portal-inline-player-episode-panel.host.ts`) from the component's inputs
|
||||
and exposed as `episodePanel`, so the player's own responsibilities stay
|
||||
readable. It declares `panelKind: 'episodes'` and `panelSearchEnabled:
|
||||
false`: season tabs are the navigation, and the header shows the series
|
||||
title (`seriesTitle`, else the playback title) where the search field would
|
||||
be.
|
||||
|
||||
- Data: the hosts pass their season→episodes map (`seriesEpisodes`, the same
|
||||
`Record<seasonKey, XtreamSerieEpisode[]>` the season container gets, so the
|
||||
TMDB overlay's stills and overviews ride in `info`), the per-episode
|
||||
playback-position map (`episodePlaybackPositions`) and, for Stalker lazy
|
||||
VOD series, per-season load states (`seasonLoadStates`: `loading` while
|
||||
a request is on the wire, `unloaded` while the portal has not answered —
|
||||
after a failed request too; `vodSeasonLoadStates` on the host, computed
|
||||
by `getVodSeasonLoadStates()` in `@iptvnator/portal/stalker/data-access`).
|
||||
`buildFullscreenEpisodePanelSeasons()`
|
||||
(`libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.util.ts`)
|
||||
turns them into `FullscreenEpisodePanelSeason[]` — numeric keys ascending,
|
||||
named keys after, each row a `FullscreenEpisodePanelItem` that extends the
|
||||
Up Next entry with `seasonKey`, `episodeNumber`, `overview`, a "45 min"
|
||||
`durationLabel` (numeric `duration_secs`, else the portals' text forms) and
|
||||
the shared ≥90 % `watched` rule; the playing row is the one whose id equals
|
||||
`contentInfo.contentXtreamId`.
|
||||
- Body: `app-fullscreen-episode-panel` stamps `SeasonTabsComponent` (the
|
||||
detail page's tabs: pills up to six seasons, a dropdown beyond, watched
|
||||
check marks, the "Back to playing episode" chip) over the selected season's
|
||||
rows: a 16:9 still or, without one, a large numeral tile so the no-TMDB
|
||||
case still looks designed; the `S01E03` label, runtime, watched check or
|
||||
"Now playing" marker; the title (label as fallback); a 3-line clamped
|
||||
overview when there is one; a progress bar on the thumbnail. Rows are
|
||||
buttons inside `<li>`s of a `<ul>`, so their button role survives for
|
||||
assistive technology. A loading season shows a spinner row, a loaded empty
|
||||
one the season-empty copy, and an unanswered one a "could not be loaded"
|
||||
row with a Retry button that re-emits the season selection — the tabs
|
||||
never re-emit an already selected key, so a failed lazy load would
|
||||
otherwise be stuck.
|
||||
- Selection: the tab follows the playing episode's season (`linkedSignal`)
|
||||
and resets to it whenever playback moves into another season; a tab the
|
||||
user picks holds until then. Opening the panel (context `open`) centres
|
||||
the playing row inside the list's own scroll box — `scrollTop` math, never
|
||||
`scrollIntoView`, so the stage and page are not scrolled with it — and the
|
||||
effect depends on primitives only (open flag, shown season key, playing
|
||||
season key and episode id), so the season objects a progress tick rebuilds
|
||||
never yank a list the user is scrolling.
|
||||
- Actions: an episode click emits the item through the inline player's
|
||||
`upNextEpisodeSelected` — the Up Next rail's output, so the host plays it
|
||||
through its inline episode flow and the engine remount keeps fullscreen
|
||||
exactly as a "next episode" does — and then calls the context's `close`;
|
||||
a click on the playing row is inert. A season tab click emits
|
||||
`episodePanelSeasonSelected` (as does the retry row), wired by both hosts
|
||||
to the same `onSeasonSelected` their season container uses, so Xtream's
|
||||
TMDB season enrichment and Stalker's lazy VOD season load run for the
|
||||
panel's season too.
|
||||
- Gates: `Settings.fullscreenChannelPanel` (one setting for channels and
|
||||
episodes; its label reads "Channel and episode list in fullscreen"),
|
||||
`contentInfo.contentType === 'episode'` and non-live playback (a movie
|
||||
never gets the panel), and at least one season. Native-view Embedded MPV is
|
||||
withheld by the view's `enabled` input as for channels; external MPV/VLC
|
||||
never mount the inline player, so they are excluded by construction.
|
||||
|
||||
### Keyboard ownership
|
||||
|
||||
Unmodified Space/K, F, arrow keys, and M are playback shortcuts. Playback keys
|
||||
|
||||
Reference in new issue
Block a user