mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(epg): offer the Guide action in the list view too
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
9071bbf19b
commit
a7a15d252e
7 files changed
+88
-14
No files matched your search
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
liveEpgPanelSummaryLabelKey()
|
||||
"
|
||||
[emptyReason]="liveEpgEmptyReason()"
|
||||
[guideAvailable]="canOpenGuide()"
|
||||
(programActivated)="
|
||||
onTimelineProgramActivated($event)
|
||||
"
|
||||
@@ -202,6 +203,7 @@
|
||||
onLiveEpgPanelCollapsedChange($event)
|
||||
"
|
||||
(openEpgSettings)="openEpgSettings()"
|
||||
(openGuide)="openGuide()"
|
||||
/>
|
||||
} @else {
|
||||
<app-epg-timeline
|
||||
|
||||
@@ -79,6 +79,17 @@
|
||||
{{ 'EPG.TIMELINE.RETURN_TO_LIVE' | translate }}
|
||||
</button>
|
||||
}
|
||||
@if (guideAvailable()) {
|
||||
<button
|
||||
type="button"
|
||||
class="g-iconbtn g-guide"
|
||||
(click)="openGuide.emit()"
|
||||
[matTooltip]="'EPG.TIMELINE.OPEN_GUIDE' | translate"
|
||||
[attr.aria-label]="'EPG.TIMELINE.OPEN_GUIDE' | translate"
|
||||
>
|
||||
<mat-icon>grid_view</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (showJump()) {
|
||||
<button
|
||||
type="button"
|
||||
@@ -112,7 +123,8 @@
|
||||
}
|
||||
</span>
|
||||
<small>{{
|
||||
viewDate() | date: 'EEE, d MMM' : '' : currentLocale()
|
||||
viewDate()
|
||||
| date: 'EEE, d MMM' : '' : currentLocale()
|
||||
}}</small>
|
||||
</div>
|
||||
<button
|
||||
@@ -133,7 +145,9 @@
|
||||
@if (state === 'list' && !archivePlaybackAvailable()) {
|
||||
<div class="g-notice" role="note">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>{{ 'EPG.TIMELINE.SCHEDULE_ONLY_NOTICE' | translate }}</span>
|
||||
<span>{{
|
||||
'EPG.TIMELINE.SCHEDULE_ONLY_NOTICE' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -163,7 +177,9 @@
|
||||
<span class="ns-lbl">{{
|
||||
'EPG.TIMELINE.ON_NOW' | translate
|
||||
}}</span>
|
||||
<span class="ns-title">{{ now.program.title }}</span>
|
||||
<span class="ns-title">{{
|
||||
now.program.title
|
||||
}}</span>
|
||||
@if (now.progress !== null) {
|
||||
<span class="ns-bar" aria-hidden="true"
|
||||
><i [style.width.%]="now.progress"></i
|
||||
|
||||
@@ -138,6 +138,30 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* icon-only toolbar action (Guide), same footprint as the timeline's */
|
||||
.g-iconbtn {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
background: $surface-2;
|
||||
border: 1px solid $line-strong;
|
||||
color: $text-secondary;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.g-iconbtn:hover {
|
||||
color: $text-primary;
|
||||
border-color: $accent-blue;
|
||||
}
|
||||
.g-iconbtn mat-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.g-date {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { EpgProgram } from '@iptvnator/shared/interfaces';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { EpgListViewComponent } from './epg-list-view.component';
|
||||
import { EpgListRow } from './epg-list-view.utils';
|
||||
|
||||
@@ -53,6 +53,9 @@ describe('EpgListViewComponent', () => {
|
||||
currentLang: 'en',
|
||||
defaultLang: 'en',
|
||||
onLangChange: new BehaviorSubject(null),
|
||||
onTranslationChange: new BehaviorSubject(null),
|
||||
onDefaultLangChange: new BehaviorSubject(null),
|
||||
get: (key: string) => of(key),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -244,4 +247,25 @@ describe('EpgListViewComponent', () => {
|
||||
expect(component.progress()).toBeLessThan(60);
|
||||
expect(component.minutesLeft()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('offers the Guide action only when the host can open the guide, like the timeline', () => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.g-guide')).toBeNull();
|
||||
|
||||
fixture.componentRef.setInput('guideAvailable', true);
|
||||
fixture.detectChanges();
|
||||
const openGuide = jest.fn();
|
||||
component.openGuide.subscribe(openGuide);
|
||||
const button = fixture.nativeElement.querySelector(
|
||||
'.g-guide'
|
||||
) as HTMLButtonElement | null;
|
||||
expect(button).not.toBeNull();
|
||||
button?.click();
|
||||
expect(openGuide).toHaveBeenCalledTimes(1);
|
||||
|
||||
// The collapsed toolbar row only carries the inline summary.
|
||||
fixture.componentRef.setInput('collapsed', true);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.g-guide')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -91,6 +91,8 @@ export class EpgListViewComponent {
|
||||
readonly summary = input<EpgTimelineSummary | null>(null);
|
||||
readonly summaryLabelKey = input('EPG.CURRENT_PROGRAM');
|
||||
readonly offsetMinutes = input(0);
|
||||
/** Same as the timeline's: renders the Guide action only when the host can open the programme guide. */
|
||||
readonly guideAvailable = input(false);
|
||||
|
||||
readonly programActivated = output<EpgProgramActivationEvent>();
|
||||
readonly returnToLive = output<void>();
|
||||
@@ -98,6 +100,7 @@ export class EpgListViewComponent {
|
||||
readonly openEpgSettings = output<void>();
|
||||
readonly retry = output<void>();
|
||||
readonly collapsedChange = output<boolean>();
|
||||
readonly openGuide = output<void>();
|
||||
|
||||
private readonly programmeDialog = inject(EpgProgrammeDialogService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
Reference in new issue
Block a user