feat(epg): rework live EPG panel into a horizontal timeline (#1102)

Replaces the vertical EPG list with a shared horizontal `app-epg-timeline`
ribbon across all live surfaces (M3U player, unified live tab, Xtream, Stalker):
zoom, day navigation, short-programme grouping, catch-up/timeshift, and
per-state empty views. Backend gains timezone-aware `datetime()` comparisons,
unscoped source fallback, non-ASCII candidate matching, and chunked candidate
queries.

Timeline split into reusable, view-agnostic modules (archive/summary/dialog
service/render util/scroll controller) for the future EPG list view.

Fixes landed during review:
- honor the controlled `selectedDate` input (seed via linkedSignal)
- restore ribbon position across collapse/expand
- keep the ribbon mounted when scrolling across a gap day
- don't trigger block playback on Enter from nested watch/info buttons
- don't reset timeshift playback on the 30s now-tick during EPG gaps

Greptile 5/5 (safe to merge); Codex clean; CI green.
This commit is contained in:
4gray authored and GitHub committed 2026-07-01 21:09:49 +02:00
1 parent c0e9cd5de3
commit 401012eac4
67 files changed
+6086 -1194

No files matched your search

+4 -5
View File
@@ -238,8 +238,7 @@ test('@stalker PWA hides EPG for ITV channel', async ({ page }) => {
await expect(page.locator('app-web-player-view')).toBeVisible({
timeout: 20_000,
});
await expect(page.locator('app-epg-list')).toHaveCount(0);
await expect(page.locator('app-live-epg-panel')).toHaveCount(0);
await expect(page.locator('app-epg-timeline')).toHaveCount(0);
expect(epgInfoRequests).toHaveLength(0);
expect(shortEpgRequests).toHaveLength(0);
});
@@ -292,7 +291,7 @@ test('@stalker radio — stations use the inline audio player without EPG', asyn
await restoreButton.click();
await expect(stations.first()).toBeVisible();
await expect(page.locator('app-epg-list')).toHaveCount(0);
await expect(page.locator('app-epg-timeline')).toHaveCount(0);
expect(epgRequests).toHaveLength(0);
});
@@ -325,11 +324,11 @@ test('@stalker PWA skips bulk EPG across channel switches', async ({
await channels.first().click();
await expect(channels.first()).toHaveClass(/active/, { timeout: 20_000 });
await expect(page.locator('app-epg-list')).toHaveCount(0);
await expect(page.locator('app-epg-timeline')).toHaveCount(0);
await channels.nth(1).click();
await expect(channels.nth(1)).toHaveClass(/active/, { timeout: 20_000 });
await expect(page.locator('app-epg-list')).toHaveCount(0);
await expect(page.locator('app-epg-timeline')).toHaveCount(0);
expect(epgInfoRequests).toHaveLength(0);
expect(shortEpgRequests).toHaveLength(0);
});