mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
feat: unify live epg toolbar
Entire-Checkpoint: 978eaff2e478
This commit is contained in:
1 parent
d163a1fbf2
commit
31258624b5
23 files changed
+648
-123
No files matched your search
@@ -1,5 +1,6 @@
|
||||
export * from './lib/epg-list/epg-list.component';
|
||||
export * from './lib/epg-list/epg-item-description/epg-item-description.component';
|
||||
export * from './lib/epg-date';
|
||||
export * from './lib/epg-progress-panel/epg-progress-panel.component';
|
||||
export * from './lib/epg-source-status/epg-source-status.component';
|
||||
export * from './lib/multi-epg/multi-epg-container.component';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { addDays, format, subDays } from 'date-fns';
|
||||
|
||||
export const EPG_DATE_KEY_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export type EpgDateNavigationDirection = 'next' | 'prev';
|
||||
|
||||
export function getTodayEpgDateKey(): string {
|
||||
return format(new Date(), EPG_DATE_KEY_FORMAT);
|
||||
}
|
||||
|
||||
export function parseEpgDateKey(
|
||||
dateKey: string | null | undefined,
|
||||
fallbackDateKey = getTodayEpgDateKey()
|
||||
): Date {
|
||||
const normalizedDateKey =
|
||||
typeof dateKey === 'string' && dateKey.trim().length > 0
|
||||
? dateKey
|
||||
: fallbackDateKey;
|
||||
const parsedDate = new Date(`${normalizedDateKey}T00:00:00`);
|
||||
|
||||
if (Number.isFinite(parsedDate.getTime())) {
|
||||
return parsedDate;
|
||||
}
|
||||
|
||||
return new Date(`${fallbackDateKey}T00:00:00`);
|
||||
}
|
||||
|
||||
export function shiftEpgDateKey(
|
||||
dateKey: string | null | undefined,
|
||||
direction: EpgDateNavigationDirection
|
||||
): string {
|
||||
const selectedDate = parseEpgDateKey(dateKey);
|
||||
return format(
|
||||
direction === 'next'
|
||||
? addDays(selectedDate, 1)
|
||||
: subDays(selectedDate, 1),
|
||||
EPG_DATE_KEY_FORMAT
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,35 @@
|
||||
@let items = filteredItems();
|
||||
@let timeshiftLimit = timeshiftUntil();
|
||||
|
||||
<!-- channel info panel -->
|
||||
<div id="channel-header" color="primary">
|
||||
<div id="date-navigator">
|
||||
<button
|
||||
class="previous-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('prev')"
|
||||
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<span class="selected-date">
|
||||
{{
|
||||
selectedDate()
|
||||
| date: 'MMMM d, EEEE' : undefined : currentLocale()
|
||||
}}</span
|
||||
>
|
||||
<button
|
||||
class="next-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('next')"
|
||||
[matTooltip]="'EPG.NEXT_DAY' | translate"
|
||||
>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
@if (showDateNavigator()) {
|
||||
<!-- channel info panel -->
|
||||
<div id="channel-header" color="primary">
|
||||
<div id="date-navigator">
|
||||
<button
|
||||
class="previous-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('prev')"
|
||||
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<span class="selected-date">
|
||||
{{
|
||||
selectedDateKey()
|
||||
| date: 'MMMM d, EEEE' : undefined : currentLocale()
|
||||
}}</span
|
||||
>
|
||||
<button
|
||||
class="next-day"
|
||||
mat-icon-button
|
||||
(click)="changeDate('next')"
|
||||
[matTooltip]="'EPG.NEXT_DAY' | translate"
|
||||
>
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (timeshiftLimit) {
|
||||
<!-- program list -->
|
||||
|
||||
@@ -243,6 +243,34 @@ describe('EpgListComponent', () => {
|
||||
.textContent.trim()
|
||||
).toContain('Sonntag');
|
||||
});
|
||||
|
||||
it('can hide its internal date navigator for a parent-owned toolbar', () => {
|
||||
fixture.componentRef.setInput('controlledPrograms', buildPrograms());
|
||||
fixture.componentRef.setInput('showDateNavigator', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('#channel-header')
|
||||
).toBeNull();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('#program-list')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('emits date changes when the selected date is controlled', () => {
|
||||
const emittedDates: string[] = [];
|
||||
component.selectedDateChange.subscribe((date) =>
|
||||
emittedDates.push(date)
|
||||
);
|
||||
fixture.componentRef.setInput('selectedDate', '2026-04-04');
|
||||
fixture.componentRef.setInput('controlledPrograms', buildPrograms());
|
||||
fixture.detectChanges();
|
||||
|
||||
component.changeDate('next');
|
||||
|
||||
expect(emittedDates).toEqual(['2026-04-05']);
|
||||
expect(component.selectedDateKey()).toBe('2026-04-04');
|
||||
});
|
||||
});
|
||||
|
||||
function buildPrograms(overrides?: {
|
||||
@@ -253,11 +281,9 @@ function buildPrograms(overrides?: {
|
||||
}): EpgProgram[] {
|
||||
const pastStart = '2026-04-05T09:00:00.000Z';
|
||||
const pastStop = '2026-04-05T10:00:00.000Z';
|
||||
const currentStart =
|
||||
overrides?.currentStart ?? '2026-04-05T11:30:00.000Z';
|
||||
const currentStart = overrides?.currentStart ?? '2026-04-05T11:30:00.000Z';
|
||||
const currentStop = overrides?.currentStop ?? '2026-04-05T12:30:00.000Z';
|
||||
const futureStart =
|
||||
overrides?.futureStart ?? '2026-04-05T12:30:00.000Z';
|
||||
const futureStart = overrides?.futureStart ?? '2026-04-05T12:30:00.000Z';
|
||||
const futureStop = overrides?.futureStop ?? '2026-04-05T13:30:00.000Z';
|
||||
|
||||
return [
|
||||
|
||||
@@ -20,13 +20,17 @@ import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { EpgActions, selectActive } from 'm3u-state';
|
||||
import { addDays, format, subDays } from 'date-fns';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import { startWith } from 'rxjs';
|
||||
import { Channel, EpgChannel, EpgProgram } from 'shared-interfaces';
|
||||
import {
|
||||
EPG_DATE_KEY_FORMAT,
|
||||
EpgDateNavigationDirection,
|
||||
getTodayEpgDateKey,
|
||||
shiftEpgDateKey,
|
||||
} from '../epg-date';
|
||||
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export interface EpgProgramActivationEvent {
|
||||
program: EpgProgram;
|
||||
type: 'live' | 'timeshift';
|
||||
@@ -51,7 +55,12 @@ export class EpgListComponent {
|
||||
readonly controlledPrograms = input<EpgProgram[] | null>(null);
|
||||
readonly controlledArchiveDays = input<number | null>(null);
|
||||
readonly archivePlaybackAvailable = input<boolean | null>(null);
|
||||
readonly selectedDateInput = input<string | null>(null, {
|
||||
alias: 'selectedDate',
|
||||
});
|
||||
readonly showDateNavigator = input(true);
|
||||
readonly programActivated = output<EpgProgramActivationEvent>();
|
||||
readonly selectedDateChange = output<string>();
|
||||
|
||||
private readonly store = inject(Store);
|
||||
private readonly epgService = inject(EpgService);
|
||||
@@ -70,7 +79,10 @@ export class EpgListComponent {
|
||||
);
|
||||
|
||||
readonly programList = viewChild<ElementRef<HTMLElement>>('programList');
|
||||
readonly selectedDate = signal(format(new Date(), DATE_FORMAT));
|
||||
readonly internalSelectedDate = signal(getTodayEpgDateKey());
|
||||
readonly selectedDateKey = computed(
|
||||
() => this.selectedDateInput() ?? this.internalSelectedDate()
|
||||
);
|
||||
readonly timeNow = signal(new Date().toISOString());
|
||||
readonly currentTimeMs = computed(() => Date.parse(this.timeNow()));
|
||||
readonly currentLocale = computed(() => {
|
||||
@@ -85,8 +97,8 @@ export class EpgListComponent {
|
||||
this.controlledChannel() !== null ||
|
||||
this.controlledPrograms() !== null
|
||||
);
|
||||
readonly displayChannel = computed(
|
||||
() => this.controlledChannel() ?? this.activeChannel()
|
||||
readonly displayChannel = computed<Channel | null>(
|
||||
() => this.controlledChannel() ?? this.activeChannel() ?? null
|
||||
);
|
||||
readonly channel = computed<EpgChannel | null>(() => {
|
||||
const channel = this.displayChannel();
|
||||
@@ -96,7 +108,9 @@ export class EpgListComponent {
|
||||
|
||||
return {
|
||||
id: channel.tvg?.id || '',
|
||||
displayName: channel.name ? [{ lang: '', value: channel.name }] : [],
|
||||
displayName: channel.name
|
||||
? [{ lang: '', value: channel.name }]
|
||||
: [],
|
||||
url: channel.url ? [channel.url] : [],
|
||||
icon: channel.tvg?.logo ? [{ src: channel.tvg.logo }] : [],
|
||||
};
|
||||
@@ -119,14 +133,14 @@ export class EpgListComponent {
|
||||
subDays(new Date(), this.archiveDays()).toISOString()
|
||||
);
|
||||
readonly archivePlaybackEnabled = computed(
|
||||
() => this.archivePlaybackAvailable() ?? (this.archiveDays() > 0)
|
||||
() => this.archivePlaybackAvailable() ?? this.archiveDays() > 0
|
||||
);
|
||||
readonly filteredItems = computed(() =>
|
||||
[...this.items()]
|
||||
.filter(
|
||||
(item) =>
|
||||
getProgramDateKey(item.start, item.startTimestamp) ===
|
||||
this.selectedDate()
|
||||
this.selectedDateKey()
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
@@ -185,16 +199,14 @@ export class EpgListComponent {
|
||||
});
|
||||
}
|
||||
|
||||
changeDate(direction: 'next' | 'prev'): void {
|
||||
const selectedDate = new Date(`${this.selectedDate()}T00:00:00`);
|
||||
this.selectedDate.set(
|
||||
format(
|
||||
direction === 'next'
|
||||
? addDays(selectedDate, 1)
|
||||
: subDays(selectedDate, 1),
|
||||
DATE_FORMAT
|
||||
)
|
||||
);
|
||||
changeDate(direction: EpgDateNavigationDirection): void {
|
||||
const nextDate = shiftEpgDateKey(this.selectedDateKey(), direction);
|
||||
|
||||
if (this.selectedDateInput() == null) {
|
||||
this.internalSelectedDate.set(nextDate);
|
||||
}
|
||||
|
||||
this.selectedDateChange.emit(nextDate);
|
||||
}
|
||||
|
||||
activateProgram(program: EpgProgram): void {
|
||||
@@ -225,7 +237,8 @@ export class EpgListComponent {
|
||||
|
||||
canActivateProgram(program: EpgProgram): boolean {
|
||||
return (
|
||||
this.isProgramPlaying(program) || this.canPlayArchivedProgram(program)
|
||||
this.isProgramPlaying(program) ||
|
||||
this.canPlayArchivedProgram(program)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +253,9 @@ export class EpgListComponent {
|
||||
const total = stop - start;
|
||||
const elapsed = now - start;
|
||||
|
||||
return total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 0;
|
||||
return total > 0
|
||||
? Math.min(100, Math.max(0, (elapsed / total) * 100))
|
||||
: 0;
|
||||
}
|
||||
|
||||
isProgramPlaying(program: EpgProgram): boolean {
|
||||
@@ -289,7 +304,7 @@ export class EpgListComponent {
|
||||
}
|
||||
|
||||
private scheduleScrollToCurrentProgram(): void {
|
||||
if (this.selectedDate() !== format(new Date(), DATE_FORMAT)) {
|
||||
if (this.selectedDateKey() !== getTodayEpgDateKey()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -385,5 +400,5 @@ function getProgramDateKey(
|
||||
return '';
|
||||
}
|
||||
|
||||
return format(new Date(programTimeMs), DATE_FORMAT);
|
||||
return format(new Date(programTimeMs), EPG_DATE_KEY_FORMAT);
|
||||
}
|
||||
Reference in new issue
Block a user