Skip to content

feat(epg): add vertical list view for the live EPG panel#1115

Merged
4gray merged 3 commits into
masterfrom
claude/nifty-lederberg-82aed1
Jul 4, 2026
Merged

feat(epg): add vertical list view for the live EPG panel#1115
4gray merged 3 commits into
masterfrom
claude/nifty-lederberg-82aed1

Conversation

@4gray

@4gray 4gray commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds an EPG List View — a vertical, single-day programme list — as an alternative to the horizontal timeline ribbon from feat(epg): rework live EPG panel into a horizontal timeline #1102, selectable via a new Settings → EPG → Guide view toggle.
  • New setting epgViewMode: 'timeline' | 'list' (default 'timeline' — existing users see no change after updating; missing key in stored settings falls back to the default on load).
  • EpgListViewComponent (app-epg-list-view) mirrors EpgTimelineComponent's input/output contract 1:1, so every live host swaps the panel with a plain @if and identical bindings.

Changes

  • New libs/ui/epg/src/lib/epg-list-view/: list component + dumb row component + pure buildEpgListRows row-model builder + EpgListScrollController (auto-focus of the on-air row, collapse/expand remount restore, sticky in-flow "On now" strip). Reuses the shared view-agnostic modules (classifyTimelineWhen, hasProgramsForDateKey, epg-archive.util, epg-summary.util, epg-date, EpgProgrammeDialogService, app-epg-timeline-empty-state) — no duplicated logic.
  • Hosts: M3U video player, unified live tab, Xtream and Stalker live layouts branch on epgViewMode from SettingsStore; list mode raises only the inline panel height via an epg--list modifier (--epg-inline-height), timeline/collapsed heights unchanged.
  • Settings: end-to-end pipeline (interface → DEFAULT_SETTINGSSettingsStore/StorageMap → segmented control in the EPG section, persisted immediately). Electron-only UI; PWA stays on the timeline default.
  • i18n: SETTINGS.EPG_VIEW_MODE* keys in all 18 locales.
  • Docs: docs/architecture/m3u-playlist-module.md EPG section rewritten for the two view modes.

Applicable findings from the #1102 bot reviews were mapped onto the list view and handled (nested-button keyboard guard, scroll-position restore across collapse/expand, gap-day/auto-focus semantics), plus a multi-lens adversarial self-review (viewport-yank on programme rollover, rect-based scroll maths, stale now-strip on non-scrollable lists, 56px collapsed-summary clipping).

Testing

  • Unit: ui-epg 122 tests (component render states, activation semantics, row keyboard guard, scroll-controller focus/remount/rollover/strip maths, day filter/dedup utils)
  • Unit: settings default + immediate persistence; swap tests in all four host specs (incl. epg--list class)
  • Electron E2E: settings round-trip across app restart; list view renders with the on-air row highlighted (xtream-epg.e2e.ts)
  • nx build web, lint clean across touched projects
  • Manual verification over CDP in the dev app (auto-focus, now-strip appear/return, taller panel)

🤖 Generated with Claude Code

Add an EPG list view — a vertical, single-day programme list — as an
alternative rendering of the live EPG panel, selectable via a new
Settings → EPG → "Guide view" toggle (epgViewMode: 'timeline' | 'list',
default 'timeline' so existing users see no change).

- New EpgListViewComponent (app-epg-list-view) mirrors
  EpgTimelineComponent's input/output contract 1:1, so all four live
  hosts (M3U player, unified live tab, Xtream, Stalker) swap the panel
  with a plain @if and identical bindings.
- Reuses the shared view-agnostic EPG modules (classifyTimelineWhen,
  hasProgramsForDateKey, epg-archive.util catch-up gating,
  epg-summary.util collapsed-summary maths, epg-date helpers,
  EpgProgrammeDialogService, app-epg-timeline-empty-state) — no
  duplicated logic.
- Rows show time range, title, optional description, live progress on
  the on-air row, catch-up "Watch" on past rows when archive playback
  is available, and a details dialog; keyboard activation guards
  nested buttons (target === currentTarget).
- Auto-focuses the on-air row on channel select, restores it across
  collapse/expand remounts, and shows a sticky in-flow "On now" strip
  (never overlaying rows) when the current programme is scrolled away;
  all scroll maths is rect-based relative to the scroller.
- List mode raises only the inline panel height via an epg--list
  modifier (--epg-inline-height clamp); timeline and collapsed heights
  are unchanged.
- Setting flows end-to-end (Settings interface → DEFAULT_SETTINGS →
  SettingsStore/StorageMap → segmented control in the EPG section);
  Electron-only UI, PWA stays on the timeline default. i18n keys added
  to all 18 locales.
- Tests: new component/row/utils/scroll-controller specs, settings
  persistence spec, swap tests in all four host specs, and Electron
  E2E for the settings round-trip and the rendered list view. Docs
  updated (m3u-playlist-module.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4890748e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +38 to +40
if (!list || !today) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset list view to today on new channels

If a user has navigated the EPG to a non-today date and then selects another channel, the host still passes that old selectedLiveEpgDate; list rows are built only for that off-today date, and this early return prevents the list from ever looking for the new channel's on-air row or emitting a date reset. The timeline controller resets new programme sets back to today when today has data, so list mode can strand users on an empty/old day until they manually press the Now control.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6fd0d0: maybeAutoScroll now keys by the full programme-set identity (programsFocusKey) and commits today (when today has data) before focusing, so a channel switch while parked on another day returns the list to today — timeline parity. Covered by the new controller specs (return-to-today, no-takeover when today is empty, day navigation left alone).

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

Greptile Summary

Adds EpgListViewComponent as a vertical, single-day EPG programme list selectable via a new Settings → EPG → Guide view toggle (epgViewMode: 'timeline' | 'list'), defaulting to 'timeline' so existing users see no change. The centralized resolvedEpgViewMode computed signal on SettingsStore — added in direct response to the previous review's feedback — is the single fallback point consumed by all four live hosts (M3U, Xtream, Stalker, Unified), eliminating the duplicated inline fallback that was present before.

  • New EPG list view library (libs/ui/epg/src/lib/epg-list-view/): EpgListViewComponent (290 lines, within the 300-line guidance), a dumb row sub-component, a pure buildEpgListRows utility, and EpgListScrollController for auto-focus of the on-air row, sticky now-strip, and collapse/expand scroll restoration.
  • Host integration: All four live layouts branch on epgViewMode() via an @if/@else guard; the epg--list CSS modifier raises the inline panel height via a CSS variable so the collapsed-panel override still wins.
  • Settings pipeline: EpgViewMode type → DEFAULT_SETTINGSSettingsStore/withComputedcreateSettingsForm (Electron/supportsEpg gated) → segmented control → immediate persistence via updateSettings; all 18 i18n locales updated.

Confidence Score: 5/5

Safe to merge. The new list view is a purely additive feature behind a settings toggle defaulting to the existing timeline, so all existing users see no change on update.

All four live host components are updated consistently, the fallback default is applied at a single point in the store, backward compatibility with stored settings is preserved via optional interface field and triple-fallback chains, and the feature is gated behind the Electron-only supportsEpg condition in the settings form. Test coverage is comprehensive across the utility, scroll controller, row component, and all four host specs.

No files require special attention. The new epg-list-view library files are well-structured with thorough unit tests, and the host template changes are mechanical @if/else swaps with identical bindings on both branches.

Important Files Changed

Filename Overview
libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.ts New 290-line component implementing the vertical EPG list — within the 300-line soft limit after extracting effects to epg-list-view.effects.ts. Input/output contract mirrors EpgTimelineComponent exactly. Signal-based state (linkedSignal, computed, viewChild) follows Angular coding standards throughout.
libs/ui/epg/src/lib/epg-list-view/epg-list-scroll.controller.ts Extracts all scroll behaviour (auto-focus, now-strip, collapse/expand restore) into a plain class. Uses rect-based scroll maths to avoid offsetTop measuring against the wrong positioned ancestor. Well unit-tested across all code paths.
libs/ui/epg/src/lib/epg-list-view/epg-list-view.effects.ts Houses the two Angular effects (30s tick + auto-scroll) extracted from the component to keep it under the line-count guideline. Effects are properly cleaned up and use untracked to prevent feedback loops.
libs/ui/epg/src/lib/epg-list-view/epg-list-view.utils.ts Pure buildEpgListRows function with overlap-based day filtering, dedup, and pre-computed catch-up gating. Correctly handles cross-midnight programmes and malformed (zero/negative duration) entries. Comprehensively tested.
libs/services/src/lib/settings-store.service.ts Adds epgViewMode to DEFAULT_SETTINGS and exposes resolvedEpgViewMode as a withComputed signal — the single fallback point for all four live hosts. Eliminates the previously duplicated inline fallback pattern across host components.
libs/ui/epg/src/lib/epg-list-view/epg-list-view-row/epg-list-view-row.component.ts Dumb presentational row component using host bindings for data-when, class.sel, and keyboard guards. Correctly prevents nested watch/info button clicks from bubbling to the row activation handler.
apps/web/src/app/settings/settings.component.ts Adds selectEpgViewMode handler following the established pattern (patchValue + markAsDirty + immediate updateSettings), consistent with selectCoverSize and selectTheme. EPG section inputs/outputs are correctly extended.
libs/shared/interfaces/src/lib/settings.interface.ts Adds optional EpgViewMode type and epgViewMode field to Settings interface. Optional field ensures backward compatibility with existing stored settings that lack the key.
libs/ui/styles/_portal-layout.scss Adds epg--list modifier that overrides --epg-inline-height via a CSS variable, so the epg-collapsed fixed-height rule still wins when the panel is collapsed. Also extends flex rules to cover app-epg-list-view alongside app-epg-timeline.
libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html Swaps the single app-epg-timeline for an @if/else block on epgViewMode(). Bindings are 1:1 identical between the two branches, matching the explicit design goal of the list view's input/output contract.
apps/web/src/app/settings/settings-form.utils.ts epgViewMode is correctly gated behind supportsEpg (same as preferUploadedEpgOverXtream) for Electron-only behaviour. createSettingsFromFormValue maps epgViewMode with a triple-fallback chain safe for PWA where the form control is absent.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Settings: EPG Guide View toggle] -->|selectEpgViewMode| B[SettingsStore.updateSettings]
    B -->|patchState| C[store.epgViewMode signal]
    C --> D[resolvedEpgViewMode computed]
    D --> E1[VideoPlayerComponent]
    D --> E2[LiveStreamLayoutComponent]
    D --> E3[StalkerLiveStreamLayout]
    D --> E4[UnifiedLiveTabComponent]
    E1 -->|if list| F1[app-epg-list-view]
    E1 -->|else| G1[app-epg-timeline]
    E2 -->|if list| F2[app-epg-list-view]
    E3 -->|if list| F3[app-epg-list-view]
    E4 -->|if list| F4[app-epg-list-view]

    subgraph EpgListView[EpgListViewComponent]
        H[programs input] --> I[buildEpgListRows]
        K[nowMs 30s tick] --> I
        I --> J[EpgListViewRowComponent rows]
        L[EpgListScrollController] -->|auto-focus on-air row| J
        L -->|now-strip visibility| M[g-nowstrip button]
    end

    F1 --> EpgListView
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Settings: EPG Guide View toggle] -->|selectEpgViewMode| B[SettingsStore.updateSettings]
    B -->|patchState| C[store.epgViewMode signal]
    C --> D[resolvedEpgViewMode computed]
    D --> E1[VideoPlayerComponent]
    D --> E2[LiveStreamLayoutComponent]
    D --> E3[StalkerLiveStreamLayout]
    D --> E4[UnifiedLiveTabComponent]
    E1 -->|if list| F1[app-epg-list-view]
    E1 -->|else| G1[app-epg-timeline]
    E2 -->|if list| F2[app-epg-list-view]
    E3 -->|if list| F3[app-epg-list-view]
    E4 -->|if list| F4[app-epg-list-view]

    subgraph EpgListView[EpgListViewComponent]
        H[programs input] --> I[buildEpgListRows]
        K[nowMs 30s tick] --> I
        I --> J[EpgListViewRowComponent rows]
        L[EpgListScrollController] -->|auto-focus on-air row| J
        L -->|now-strip visibility| M[g-nowstrip button]
    end

    F1 --> EpgListView
Loading

Reviews (3): Last reviewed commit: "fix(epg): drop malformed programmes from..." | Re-trigger Greptile

Comment thread libs/ui/epg/src/lib/epg-list-view/epg-list-view.component.ts
Comment thread libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts Outdated
- Reset the list view to today when a new channel's programme set
  arrives while the user is parked on another day (timeline parity):
  the scroll controller now keys by the full programme-set identity
  (programsFocusKey) and commits today before focusing, instead of
  silently stranding the new channel on the stale day. (Codex P2)
- Centralise the 'timeline' fallback as a resolvedEpgViewMode computed
  on SettingsStore; the four live hosts consume the derived signal
  instead of duplicating the `?? 'timeline'` expression. (Greptile P2)
- Extract the component's reactive plumbing into
  registerEpgListViewEffects(), bringing the component back under the
  300-line guideline (290). (Greptile P2)
- Controller spec rewritten around programme-set fixtures with new
  coverage: return-to-today on channel switch, day navigation left
  alone, no-takeover when today has no data, empty-set no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@4gray

4gray commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@greptileai

Pushed e6fd0d08 addressing all three review findings:

  • Codex P2 (stale day on channel switch): the scroll controller now keys by the full programme-set identity (programsFocusKey) and commits today before focusing when a new channel arrives while another day is viewed — timeline parity.
  • Greptile P2 (duplicated fallback): added a resolvedEpgViewMode computed on SettingsStore; all four hosts consume the derived signal.
  • Greptile P2 (304 lines): extracted registerEpgListViewEffects() — the component is now 290 lines.

@4gray

4gray commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6fd0d0825

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!Number.isFinite(startMs) || !Number.isFinite(stopMs)) {
return false;
}
return startMs < dayEndMs && stopMs > dayStartMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-positive programme durations

When an EPG provider returns a malformed programme whose stop time is equal to or before its start time, this overlap test still accepts it as long as both timestamps fall within the selected day. The timeline path drops those entries in buildTimelineBlocks, but list mode will render impossible ranges and can even mark them catch-up playable, so list mode can show bogus rows for bad provider data.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c6dbedd: buildEpgListRows now rejects programmes whose stop is not after their start, matching the timeline's buildTimelineBlocks. Regression test added (zero and negative durations are dropped).

Reject programmes whose stop is not after their start in
buildEpgListRows — same as the timeline's buildTimelineBlocks. Bad
provider data would otherwise render impossible time ranges and could
even be offered as catch-up playable. (Codex P2 on e6fd0d0)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@4gray

4gray commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@greptileai

@4gray

4gray commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: c6dbedd035

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@4gray 4gray merged commit 0b526b1 into master Jul 4, 2026
16 checks passed
4gray added a commit that referenced this pull request Jul 4, 2026
Master added EpgListViewComponent (#1115) written against the old
epg-list/ paths this branch deletes, plus the lint/coverage CI
hardening (#1117). Resolution:

- ui/epg barrel: keep relocated epg-item-description and
  epg-program-activation-event exports, add the three new
  epg-list-view exports, drop the deleted epg-list.component export.
- epg-list-view imports updated to ../epg-item-description,
  ../epg-program-activation-event and ../epg-program.utils.
- Restore deduplicateProgramsByTimeSlot (+ private helpers) in
  epg-program.utils.ts — the new list view makes it live again.
- _portal-layout.scss / unified-live-tab.component.scss: panel
  selectors cover app-epg-timeline and app-epg-list-view only.
- Drop the deleted epg-list.component.spec.ts entry from the
  max-lines baseline.
- m3u-playlist-module.md: add EpgListViewComponent to the EPG
  components table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4gray added a commit that referenced this pull request Jul 4, 2026
Resolves conflicts against the dead-component cleanup (#1116), the
max-lines lint baseline (#1117) and the vertical EPG list view (#1115):

- libs/ui/shared-portals/src/index.ts: keep master's barrel (EpgView and
  LiveEpgPanel components were deleted, LiveEpgPanelSummary extracted)
  plus the new actor-view export from this branch
- settings-form.utils.ts / settings.component.spec.ts: keep both the new
  epgViewMode control from master and the tmdb settings group

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant