mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -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>
* fix(epg): address list-view review findings from Codex and Greptile
- 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>
* fix(epg): drop malformed programmes from the list-view day filter
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 e6fd0d08)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
144 lines
5.5 KiB
HTML
144 lines
5.5 KiB
HTML
@if (isDialog) {
|
|
<h2 mat-dialog-title>{{ 'SETTINGS.GENERAL' | translate }}</h2>
|
|
}
|
|
|
|
<div
|
|
class="settings-container"
|
|
data-test-id="settings-container"
|
|
[class.dialog-mode]="isDialog"
|
|
appSettingsSectionScroll
|
|
>
|
|
<form
|
|
[formGroup]="settingsForm"
|
|
(ngSubmit)="onSubmit()"
|
|
class="settings-layout"
|
|
novalidate
|
|
>
|
|
<!-- Page-level "Settings" h1 intentionally removed: the left rail
|
|
already labels the page "Settings" (sg-nav-title), and each
|
|
section below provides its own h3 + subtitle. Stacking another
|
|
h1 in the right pane reproduced the audit's "vertical ladder
|
|
of identical labels" complaint. -->
|
|
@if (!isDialog) {
|
|
<!-- Reserved hook so the page test selector still resolves,
|
|
even though there's no visible chrome here. -->
|
|
<span
|
|
class="visually-hidden"
|
|
data-test-id="settings-page-header"
|
|
>
|
|
{{ 'SETTINGS.TITLE' | translate }}
|
|
</span>
|
|
}
|
|
|
|
<div class="settings-content">
|
|
<app-settings-general-section
|
|
[form]="settingsForm"
|
|
[activeSection]="activeSection()"
|
|
[languageEnum]="languageEnum"
|
|
[themeOptions]="themeOptions"
|
|
[coverSizeOptions]="coverSizeOptions"
|
|
[startupBehaviorOptions]="startupBehaviorOptions"
|
|
(selectTheme)="selectTheme($event)"
|
|
(selectCoverSize)="selectCoverSize($event)"
|
|
/>
|
|
|
|
<app-settings-playback-section
|
|
[form]="settingsForm"
|
|
[activeSection]="activeSection()"
|
|
[players]="players()"
|
|
[streamFormatEnum]="streamFormatEnum"
|
|
[isDesktop]="isDesktop"
|
|
[supportsManagedExternalPlayers]="supportsManagedExternalPlayers"
|
|
[supportsExternalPlayerPathSettings]="supportsExternalPlayerPathSettings"
|
|
(selectRecordingFolder)="selectRecordingFolder()"
|
|
/>
|
|
|
|
@if (supportsEpg) {
|
|
<app-settings-epg-section
|
|
[form]="settingsForm"
|
|
[activeSection]="activeSection()"
|
|
[epgUrl]="epgUrl"
|
|
[isClearingEpgData]="isClearingEpgData()"
|
|
[epgViewModeOptions]="epgViewModeOptions"
|
|
(refreshEpg)="refreshEpg($event)"
|
|
(removeEpgSource)="removeEpgSource($event)"
|
|
(addEpgSource)="addEpgSource()"
|
|
(refreshAllEpg)="refreshAllEpg()"
|
|
(clearEpgData)="clearEpgData()"
|
|
(selectEpgViewMode)="selectEpgViewMode($event)"
|
|
/>
|
|
}
|
|
|
|
<app-settings-dashboard-section
|
|
[form]="settingsForm"
|
|
[activeSection]="activeSection()"
|
|
/>
|
|
|
|
@if (supportsRemoteControl) {
|
|
<app-settings-remote-control-section
|
|
[form]="settingsForm"
|
|
[activeSection]="activeSection()"
|
|
[localIpAddresses]="localIpAddresses()"
|
|
[visibleQrCodeIp]="visibleQrCodeIp()"
|
|
(toggleQrCode)="toggleQrCode($event)"
|
|
/>
|
|
}
|
|
|
|
<app-settings-backup-section
|
|
[activeSection]="activeSection()"
|
|
[isPwa]="isPwa"
|
|
[isRemovingAllPlaylists]="isRemovingAllPlaylists()"
|
|
[isExportingData]="isExportingData()"
|
|
(importData)="importData()"
|
|
(exportData)="exportData()"
|
|
/>
|
|
|
|
<app-settings-reset-section
|
|
[activeSection]="activeSection()"
|
|
[isRemovingAllPlaylists]="isRemovingAllPlaylists()"
|
|
[canRemoveAllPlaylists]="canRemoveAllPlaylists()"
|
|
[playlistDeleteSummary]="playlistDeleteSummary()"
|
|
[removeAllProgressLabel]="removeAllProgressLabel()"
|
|
(removeAll)="removeAll()"
|
|
/>
|
|
|
|
<app-settings-about-section
|
|
[activeSection]="activeSection()"
|
|
[isDesktop]="isDesktop"
|
|
[version]="version"
|
|
[updateMessage]="updateMessage"
|
|
[appUpdateStatus]="appUpdateStatus()"
|
|
(checkForAppUpdate)="checkForAppUpdate()"
|
|
(downloadAppUpdate)="downloadAppUpdate()"
|
|
(installAppUpdate)="installAppUpdate()"
|
|
(openManualAppUpdate)="openManualAppUpdate()"
|
|
(openAppUpdateReleaseNotes)="openAppUpdateReleaseNotes()"
|
|
/>
|
|
|
|
<footer class="action-bar" data-test-id="settings-action-bar">
|
|
@if (isDialog) {
|
|
<button
|
|
mat-button
|
|
type="button"
|
|
(click)="backToHome()"
|
|
data-test-id="back-to-home"
|
|
>
|
|
<mat-icon>close</mat-icon>
|
|
{{ 'CLOSE' | translate }}
|
|
</button>
|
|
}
|
|
<button
|
|
mat-flat-button
|
|
type="submit"
|
|
class="settings-save-button"
|
|
[disabled]="settingsForm.pristine || !settingsForm.valid"
|
|
data-test-id="save-settings"
|
|
>
|
|
<mat-icon>save</mat-icon>
|
|
{{ 'SETTINGS.SAVE_CHANGES' | translate }}
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</form>
|
|
</div>
|