From a7a15d252e527e9225ecce28e70e0db1d30d8ef7 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 6 Sep 2026 18:08:57 +0200 Subject: [PATCH] fix(epg): offer the Guide action in the list view too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The EPG list view mirrors the timeline's input/output contract, but the Guide action was bound only in the timeline branch, so Settings → EPG → Guide view = List lost the in-panel entry point. The list toolbar now carries the same icon-only Guide button behind `guideAvailable`/`openGuide`, and the M3U host binds it in both branches. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 2 +- docs/architecture/m3u-playlist-module.md | 23 +++++++++------- .../video-player/video-player.component.html | 2 ++ .../epg-list-view.component.html | 22 +++++++++++++--- .../epg-list-view.component.scss | 24 +++++++++++++++++ .../epg-list-view.component.spec.ts | 26 ++++++++++++++++++- .../epg-list-view/epg-list-view.component.ts | 3 +++ 7 files changed, 88 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 428cbf436..28ab0a4a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1486,7 +1486,7 @@ stream_id`); it drops `series_id`/`movie_id`, so the builder pins the - Background parsing in worker thread - Stored in database for quick lookup - Global display-time offset (`Settings.epgOffsetMinutes`, Settings → EPG, ±720 min, Electron only): display-only, provider data is never rewritten. Two equivalent forms in `libs/shared/interfaces/src/lib/epg-display-offset.util.ts` — `epgDisplayTimeMs` (shift the programme; `ui/epg` rendering via the `offsetMinutes` input, channel rows, dashboard/recording labels; the programme dialog and the programme guide read the store themselves) and `epgProviderClockMs` (shift "now"; every "currently airing" decision: the `GET_CURRENT_PROGRAMS_BATCH` lookup takes an explicit `nowMs` and `EpgService` tags its cache with the offset, Xtream/Stalker/M3U current-programme selection and previews, the unified collection resolver, dashboard progress, recording overlap). A consumer applies exactly one form per comparison. Contract: `docs/architecture/m3u-playlist-module.md` ("EPG display offset") -- Programme guide (Electron, M3U): `app-epg-guide` in `libs/ui/epg` fed by the host-provided `EPG_GUIDE_SOURCE`; the M3U host switches into guide mode (docked player strip, no sidebar/timeline, no remount) from the header action, the palette, the timeline's Guide button or `G`. Data: `EPG_GET_PROGRAMS_FOR_CHANNELS` / `EPG_GET_PROGRAM_COVERAGE` (keys resolved in main; manual mappings honoured). Contract: `docs/architecture/m3u-playlist-module.md` ("Programme guide"). +- Programme guide (Electron, M3U): `app-epg-guide` in `libs/ui/epg` fed by the host-provided `EPG_GUIDE_SOURCE`; the M3U host switches into guide mode (docked player strip, no sidebar/timeline, no remount) from the header action, the palette, the EPG panel's Guide button (timeline or list view) or `G`. Data: `EPG_GET_PROGRAMS_FOR_CHANNELS` / `EPG_GET_PROGRAM_COVERAGE` (keys resolved in main; manual mappings honoured). Contract: `docs/architecture/m3u-playlist-module.md` ("Programme guide"). - Manual EPG mapping (Electron only): right-click a channel in any list (M3U views, Xtream portal list, Stalker ITV sidebar, global favorites) → "Map EPG channel" attaches it to an uploaded-XMLTV channel; stored in `epg_channel_mappings` keyed by the M3U lookup key or a playlist-scoped portal key (`xtream:{playlistId}:{id}` / `stalker:{playlistId}:{id}`, helpers in `libs/shared/interfaces/src/lib/epg-mapping-key.util.ts`); resolved on every EPG path (single + batch IPC lookups, portal detail views, preview queues); dialog: `libs/ui/components/src/lib/channel-list-container/epg-mapping-dialog/` **TMDB Metadata Enrichment** (opt-in): diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index 7e1a5f78d..94c35e3b3 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -764,7 +764,10 @@ matching `hasProgramsForDateKey`), sorts, and deduplicates via a pure `app-epg-list-view-row`; and delegates its own vertical auto-focus + sticky "now" strip to `EpgListScrollController` (`epg-list-scroll.controller.ts`). Render states, the collapsed inline summary, the date stepper, catch-up/timeshift -activation, and the details dialog behave identically to the timeline. +activation, and the details dialog behave identically to the timeline. Both +views also carry the optional `guideAvailable` input and `openGuide` output: +the M3U host binds them in both branches, so the programme guide's Guide +action is reachable whichever view the setting selects. - **One channel, preloaded window.** The panel always shows a single channel. Each provider returns a multi-day window in roughly one call (M3U @@ -1053,11 +1056,11 @@ class EpgService { ### EPG Components -| Component | Purpose | -| ----------------------------- | ------------------------------------ | -| `EpgTimelineComponent` | Horizontal timeline for one channel | -| `EpgListViewComponent` | Vertical single-day list alternative | -| `EpgItemDescriptionComponent` | Program details dialog | +| Component | Purpose | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `EpgTimelineComponent` | Horizontal timeline for one channel | +| `EpgListViewComponent` | Vertical single-day list alternative | +| `EpgItemDescriptionComponent` | Program details dialog | | `EpgGuideComponent` | Multi-channel programme guide grid fed by `EPG_GUIDE_SOURCE` (rows: `EpgGuideRowComponent`, toolbar: `EpgGuideToolbarComponent`, docked strip: `EpgGuideNowPlayingComponent`) | ## Video Player @@ -1175,8 +1178,10 @@ MPV bounds survive. While the guide is open the docked `.video-player` carries `data-player-shortcuts-suspended`, which makes `ControlsShortcuts` (shared and legacy player shortcuts alike) yield ↑/↓, Space, F and M to the guide's own keyboard controller. Entry points: the workspace header action -(`m3u-epg-guide`), the command palette, the Guide button in the timeline -toolbar (`EpgTimelineComponent.openGuide`) and the `G` key on the player page. +(`m3u-epg-guide`), the command palette, the Guide button in the EPG panel's +toolbar (`openGuide` on both `EpgTimelineComponent` and `EpgListViewComponent`, +so the action survives the list-view setting) and the `G` key on the player +page. The header action reports `disabled` whenever the guide cannot open, which greys out the header button and disables its palette command instead of offering a no-op. Player fullscreen, radio, recognised movies, switching to @@ -1206,7 +1211,7 @@ request window is converted with `epgProviderClockMs`. both IPCs from one `guideWindowCondition()` predicate — when the request carries `sourceUrls` (portal hosts only; the M3U host never does), a row qualifies if it belongs to one of those sources OR carries no source at all -(legacy pre-per-source-tracking data), never if it belongs to a *different* +(legacy pre-per-source-tracking data), never if it belongs to a _different_ source. Both reads cap the requested channel-key batch (`EPG_GUIDE_MAX_CHANNELS_PER_REQUEST` = 100 for programmes, `EPG_GUIDE_MAX_COVERAGE_KEYS_PER_REQUEST` = 2000 for the cheaper coverage diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html index b56859c7f..5974fd0bc 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html @@ -191,6 +191,7 @@ liveEpgPanelSummaryLabelKey() " [emptyReason]="liveEpgEmptyReason()" + [guideAvailable]="canOpenGuide()" (programActivated)=" onTimelineProgramActivated($event) " @@ -202,6 +203,7 @@ onLiveEpgPanelCollapsedChange($event) " (openEpgSettings)="openEpgSettings()" + (openGuide)="openGuide()" /> } @else { } + @if (guideAvailable()) { + + } @if (showJump()) {