diff --git a/CLAUDE.md b/CLAUDE.md index 851eb439e..2419bb1f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -618,6 +618,14 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use - External players: MPV, VLC (via IPC to Electron backend) - Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`. +**VOD/Series Detail Pages (two-state layout)**: + +- Xtream and Stalker detail pages use the shared `PortalDetailShellComponent` (`libs/ui/components/src/lib/portal-detail-shell/`) with two states: **Browse** (hero with poster/metadata/actions, episodes below) and **Watch** (hero collapses with a ~300ms morph, the inline player takes the full content width, metadata moves to an About block below the episodes) +- Watch state derives from `inlinePlayback() !== null` only; external MPV/VLC playback keeps the browse layout. Esc and the now-playing back button close the inline player back to browse without route navigation +- Hosts pass hero chips/meta/actions as `*detailTags`/`*detailMeta`/`*detailActions` templates; the shell stamps them into both the hero and the About block +- Seasons are tabs (`SeasonTabsComponent`, dropdown beyond 6 seasons) with auto-selection (playing episode's season → resume season → first) that fires the same `seasonSelected` lazy-load/enrichment hooks as manual clicks; grid/list episode view toggle persists to localStorage; season descriptions come from `get_series_info` (Xtream) or TMDB (Stalker) +- See `docs/architecture/embedded-inline-playback.md` ("Two-State Detail Layout") + **Radio Player**: - Dedicated audio player for channels with `radio="true"` M3U attribute diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 8d53a7051..88ef2a9d8 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -82,8 +82,60 @@ Current limitation: - because of that, the setting is auto-sanitized back to the default inline player unless support detection reports that the experimental runtime is available - this follows the rollout gate: keep the native work in-tree, but do not leave it user-facing until playback, resize, focus, and packaging are stable +## Two-State Detail Layout (Browse ↔ Watch) + +Portal VOD/series detail pages are hosted by the shared +`PortalDetailShellComponent` +(`libs/ui/components/src/lib/portal-detail-shell/portal-detail-shell.component.ts`), +which owns the page scroll container and two layout states: + +- **Browse** (`playbackActive=false`): the hero (`ContentHeroComponent`, + now a natural-height, non-scrolling block) renders poster, title, chips, + description, credits, and actions. Episodes render below. +- **Watch** (`playbackActive=true`, bound by hosts to + `inlinePlayback() !== null`): the hero collapses (~300ms CSS morph, + disabled under `prefers-reduced-motion`), the host-projected + `[detail-player]` slot renders the inline player at full content width, + and the shell renders an About block (`ContentAboutComponent`) below the + `[detail-episodes]` slot so the hero metadata stays reachable. + +Contracts: + +- Hosts provide hero chips/meta/actions as `*detailTags` / `*detailMeta` / + `*detailActions` templates; the shell stamps them into the hero and again + into the About block. Degradation stays "missing → not rendered". +- The shell never wraps the `[detail-player]` slot in a conditional; the + host's `@if (inlinePlayback())` is the only creator/destroyer of the + player subtree, so shell state changes cannot recreate the `