mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
feat(epg): add vertical list view for the live EPG panel
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>
This commit is contained in:
1 parent
401012eac4
commit
c4890748e0
57 files changed
+2981
-90
No files matched your search
@@ -231,6 +231,38 @@ test.describe('Electron Settings', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@settings @persistence @electron persists the EPG view mode across app restart', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const firstLaunch = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await openSettings(firstLaunch.mainWindow);
|
||||
const listToggle = firstLaunch.mainWindow.locator(
|
||||
'[data-test-id="epg-view-mode-list"]'
|
||||
);
|
||||
await expect(listToggle).toBeVisible();
|
||||
await listToggle.click();
|
||||
await expect(listToggle).toHaveAttribute('aria-checked', 'true');
|
||||
await saveSettings(firstLaunch.mainWindow);
|
||||
} finally {
|
||||
await closeElectronApp(firstLaunch);
|
||||
}
|
||||
|
||||
const secondLaunch = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await openSettings(secondLaunch.mainWindow);
|
||||
await expect(
|
||||
secondLaunch.mainWindow.locator(
|
||||
'[data-test-id="epg-view-mode-list"]'
|
||||
)
|
||||
).toHaveAttribute('aria-checked', 'true');
|
||||
} finally {
|
||||
await closeElectronApp(secondLaunch);
|
||||
}
|
||||
});
|
||||
|
||||
test('@settings @electron starts on sources when dashboard is disabled', async ({ dataDir }) => {
|
||||
const firstLaunch = await launchElectronApp(dataDir);
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@ import {
|
||||
clickCategoryByNameExact,
|
||||
closeElectronApp,
|
||||
expect,
|
||||
goToDashboard,
|
||||
launchElectronApp,
|
||||
openSettings,
|
||||
openWorkspaceSection,
|
||||
resetMockServers,
|
||||
saveSettings,
|
||||
test,
|
||||
waitForXtreamWorkspaceReady,
|
||||
} from './electron-test-fixtures';
|
||||
@@ -121,6 +124,64 @@ for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
|
||||
});
|
||||
}
|
||||
|
||||
test('@epg @xtream @electron renders the vertical list view when the setting is "list"', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const fixture = await fetchXtreamEpgFixture(request, epgCredentials);
|
||||
const currentProgram = fixture.shortEpg[0];
|
||||
if (!currentProgram) {
|
||||
throw new Error(
|
||||
'Expected the Xtream EPG fixture to include a current program.'
|
||||
);
|
||||
}
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
// Opt into the list view first (from the fresh workspace) so the portal
|
||||
// → Live TV → channel flow afterwards mirrors the timeline test exactly.
|
||||
await openSettings(app.mainWindow);
|
||||
await app.mainWindow
|
||||
.locator('[data-test-id="epg-view-mode-list"]')
|
||||
.click();
|
||||
await saveSettings(app.mainWindow);
|
||||
await goToDashboard(app.mainWindow);
|
||||
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
name: `${epgPortalName} List`,
|
||||
username: epgCredentials.username,
|
||||
password: epgCredentials.password,
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
await openWorkspaceSection(app.mainWindow, 'Live TV');
|
||||
await clickCategoryByNameExact(app.mainWindow, fixture.categoryName);
|
||||
const channelRow = channelItemByTitle(
|
||||
app.mainWindow,
|
||||
fixture.stream.name ?? ''
|
||||
).first();
|
||||
await expect(channelRow).toBeVisible({ timeout: 20000 });
|
||||
await channelRow.click();
|
||||
|
||||
// The list view renders instead of the timeline.
|
||||
await expect(app.mainWindow.locator('app-epg-list-view')).toBeVisible({
|
||||
timeout: 20000,
|
||||
});
|
||||
await expect(
|
||||
app.mainWindow.locator('app-epg-timeline')
|
||||
).toHaveCount(0);
|
||||
|
||||
// The on-air programme is the highlighted "now" row.
|
||||
await expect(
|
||||
app.mainWindow
|
||||
.locator('app-epg-list-view .g-row[data-when="now"] .title')
|
||||
.first()
|
||||
).toHaveText(currentProgram.title);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
function formatTimeInZone(timestampSeconds: number, timeZone: string): string {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
timeZone,
|
||||
|
||||
Reference in new issue
Block a user