From 4193a67db352ddc1da62c3a3c510c1f2796aabc9 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 7 Dec 2024 16:32:25 +0100 Subject: [PATCH] feat(multi-epg): only show channels from current playlist in multi-epg view --- .../multi-epg-container.component.html | 124 ++++--- .../multi-epg-container.component.scss | 59 +++- .../multi-epg-container.component.ts | 328 +++++++++++------- .../video-player/video-player.component.ts | 36 +- 4 files changed, 344 insertions(+), 203 deletions(-) diff --git a/src/app/player/components/multi-epg/multi-epg-container.component.html b/src/app/player/components/multi-epg/multi-epg-container.component.html index dd1eb4627..38fb4c7e8 100644 --- a/src/app/player/components/multi-epg/multi-epg-container.component.html +++ b/src/app/player/components/multi-epg/multi-epg-container.component.html @@ -24,24 +24,34 @@
{{ today | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}
- - - - +
- + - @if (item.icon) { - - } - @if (!item.icon) { - -
- {{ item.name }} -
-
- } + /> + +
+ + {{ item.name }} +
+
- + - + - {{ i }}:00 + {{ hour }}:00 - + - -
+
{{ program.title }}
-
+ + + [attr.y2]="(channels$ | async)?.length * barHeight + barHeight" + stroke="red" + stroke-width="2" + >
diff --git a/src/app/player/components/multi-epg/multi-epg-container.component.scss b/src/app/player/components/multi-epg/multi-epg-container.component.scss index e787f143a..a33feba11 100644 --- a/src/app/player/components/multi-epg/multi-epg-container.component.scss +++ b/src/app/player/components/multi-epg/multi-epg-container.component.scss @@ -43,12 +43,42 @@ rect { width: 100px; flex: none; border-right: 2px solid #ccc; + + .channel { + cursor: pointer; + + rect { + fill: #2d2d2d; + stroke: #3d3d3d; + } + + &:hover rect { + fill: #3d3d3d; + } + } + + .channel-name { + color: white; + padding: 4px 8px; + font-size: 12px; + display: flex; + align-items: center; + height: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + image { + object-fit: contain; + background: #2d2d2d; + } } -.channel-name { +/* .channel-name { margin: 0 5px; text-align: center; -} +} */ #epg-navigation { display: flex; @@ -57,6 +87,31 @@ rect { align-items: center; } +#epg-svg { + .program { + cursor: pointer; + + rect { + fill: #2d2d2d; + stroke: #3d3d3d; + } + + &:hover rect { + fill: #3d3d3d; + } + } + + .program-title { + color: white; + padding: 4px 8px; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + } +} + .today-date { flex: 1; align-items: center; diff --git a/src/app/player/components/multi-epg/multi-epg-container.component.ts b/src/app/player/components/multi-epg/multi-epg-container.component.ts index 5f7e92900..81cac66f0 100644 --- a/src/app/player/components/multi-epg/multi-epg-container.component.ts +++ b/src/app/player/components/multi-epg/multi-epg-container.component.ts @@ -2,10 +2,13 @@ import { OverlayRef } from '@angular/cdk/overlay'; import { CommonModule } from '@angular/common'; import { AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, Inject, - NgZone, + Input, + OnDestroy, OnInit, ViewChild, } from '@angular/core'; @@ -14,18 +17,27 @@ import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; +import { invoke } from '@tauri-apps/api/core'; import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns'; -import { - EPG_GET_CHANNELS_BY_RANGE, - EPG_GET_CHANNELS_BY_RANGE_RESPONSE, -} from '../../../../../shared/ipc-commands'; -import { DataService } from '../../../services/data.service'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { Channel } from '../../../../../shared/channel.interface'; import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe'; import { EpgChannel } from '../../models/epg-channel.model'; import { EpgProgram } from '../../models/epg-program.model'; import { EpgItemDescriptionComponent } from '../epg-list/epg-item-description/epg-item-description.component'; import { COMPONENT_OVERLAY_REF } from '../video-player/video-player.component'; +interface EnrichedProgram extends EpgProgram { + startDate: Date; + stopDate: Date; + startPosition: number; + width: number; +} + +interface EnrichedChannel extends EpgChannel { + programs: EnrichedProgram[]; +} + @Component({ standalone: true, imports: [ @@ -39,182 +51,242 @@ import { COMPONENT_OVERLAY_REF } from '../video-player/video-player.component'; selector: 'app-multi-epg-container', templateUrl: './multi-epg-container.component.html', styleUrls: ['./multi-epg-container.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MultiEpgContainerComponent implements OnInit, AfterViewInit { +export class MultiEpgContainerComponent + implements OnInit, AfterViewInit, OnDestroy +{ @ViewChild('epgContainer') epgContainer: ElementRef; - timeHeader = new Array(24); - hourWidth = 150; - barHeight = 50; - originalEpgData: (EpgChannel & { programs: EpgProgram[] })[] = []; - channels: (EpgChannel & { programs: EpgProgram[] })[] = []; + + @Input() set playlistChannels(value: Observable) { + if (value) { + value.subscribe((channels) => { + this._playlistChannels = channels; + this.initializeVisibleChannels(); + this.requestPrograms(); + }); + } + } + private _playlistChannels: Channel[] = []; + + readonly timeHeader = Array.from({ length: 24 }, (_, i) => i); + readonly hourWidth$ = new BehaviorSubject(150); + readonly barHeight = 50; + + channels$ = new BehaviorSubject([]); today = format(new Date(), 'yyyyMMdd'); currentTimeLine = 0; - visibleChannels; + visibleChannels = 20; channelsLowerRange = 0; - channelsUpperRange; + channelsUpperRange = this.visibleChannels; + originalEpgData: any[] = []; + + private dateCache = new Map(); + private interval: any; + + isLastPage = false; + totalChannels = 0; constructor( - private dataService: DataService, private dialog: MatDialog, - private ngZone: NgZone, + private cdr: ChangeDetectorRef, @Inject(COMPONENT_OVERLAY_REF) private overlayRef: OverlayRef - ) { - this.dataService.listenOn( - EPG_GET_CHANNELS_BY_RANGE_RESPONSE, - (event, response) => - this.ngZone.run(() => { - if (response) { - this.originalEpgData = response.payload; - this.channels = this.enrichProgramData(); - } - }) - ); - } + ) {} - ngOnInit(): void { + ngOnInit() { this.calculateCurrentTimeBar(); + this.interval = setInterval(() => { + this.calculateCurrentTimeBar(); + }, 60000); } ngAfterViewInit(): void { + this.initializeVisibleChannels(); + this.scrollToCurrentTime(); + } + + private scrollToCurrentTime(): void { const timeNow = new Date(); const scrollPosition = - (timeNow.getHours() + timeNow.getMinutes() / 60) * this.hourWidth; - document - .getElementById('epg-container')! - .scrollTo(scrollPosition < 1000 ? 0 : scrollPosition - 150, 0); + (timeNow.getHours() + timeNow.getMinutes() / 60) * + this.hourWidth$.value; - const borderInPx = - this.epgContainer.nativeElement.offsetHeight / this.barHeight; - this.visibleChannels = Math.floor( - (this.epgContainer.nativeElement.offsetHeight - borderInPx) / - this.barHeight - - 1 - ); - this.channelsUpperRange = this.visibleChannels; - this.requestPrograms(); + requestAnimationFrame(() => { + const container = document.getElementById('epg-container'); + if (container) { + container.scrollTo(scrollPosition < 1000 ? 0 : scrollPosition - 150, 0); + } + }); + } + + private initializeVisibleChannels(): void { + if (this.epgContainer) { + const containerHeight = this.epgContainer.nativeElement.offsetHeight; + const calculatedVisibleChannels = Math.floor((containerHeight - this.barHeight) / this.barHeight); + + this.visibleChannels = Math.max(10, Math.min(calculatedVisibleChannels, 20)); + this.channelsUpperRange = this.visibleChannels; + + console.log('Container height:', containerHeight); + console.log('Calculated visible channels:', this.visibleChannels); + } + } + + ngOnDestroy(): void { + this.hourWidth$.complete(); + this.channels$.complete(); + clearInterval(this.interval); + } + + trackByIndex(index: number): number { + return index; + } + + trackByProgram(_: number, program: EnrichedProgram): string { + return program.start + program.title; + } + + async requestPrograms(): Promise { + const today = new Date(); + const startTime = format(subDays(today, 1), 'yyyyMMddHHmmss +0000'); + const endTime = format(addDays(today, 2), 'yyyyMMddHHmmss +0000'); + + try { + const channelNames = this._playlistChannels + .map((channel) => channel.name?.trim() ?? '') + .filter((name) => name !== ''); + + console.log('Requesting EPG data:'); + console.log('- Skip:', this.channelsLowerRange); + console.log('- Limit:', this.visibleChannels); + console.log('- Channel names count:', channelNames.length); + + const response = await invoke('get_epg_by_range', { + startTime, + endTime, + skip: this.channelsLowerRange, + limit: this.visibleChannels, + playlistChannelNames: channelNames, + }); + + if (response) { + console.log('Received channels:', response.length); + this.originalEpgData = response; + this.channels$.next(this.enrichProgramData()); + + // Update isLastPage based on the number of channels received + this.isLastPage = response.length < this.visibleChannels; + + this.cdr.detectChanges(); + } + } catch (error) { + console.error('Error fetching EPG data:', error); + } } nextChannels(): void { - this.channelsLowerRange = this.channelsUpperRange; - this.channelsUpperRange = - this.channelsUpperRange + this.visibleChannels; - this.channels = []; + console.log('Next channels clicked'); + console.log('Current range:', this.channelsLowerRange, 'to', this.channelsUpperRange); + + this.channelsLowerRange += this.visibleChannels; + this.channelsUpperRange = this.channelsLowerRange + this.visibleChannels; + + console.log('New range:', this.channelsLowerRange, 'to', this.channelsUpperRange); this.requestPrograms(); } previousChannels(): void { - this.channelsUpperRange = - this.channelsUpperRange - this.visibleChannels; - this.channelsLowerRange = - this.channelsUpperRange - this.visibleChannels; - + console.log('Previous channels clicked'); + console.log('Current range:', this.channelsLowerRange, 'to', this.channelsUpperRange); + + this.channelsLowerRange = Math.max(0, this.channelsLowerRange - this.visibleChannels); + this.channelsUpperRange = this.channelsLowerRange + this.visibleChannels; + + console.log('New range:', this.channelsLowerRange, 'to', this.channelsUpperRange); this.requestPrograms(); } - requestPrograms() { - this.dataService.sendIpcEvent(EPG_GET_CHANNELS_BY_RANGE, { - limit: this.channelsUpperRange, - skip: this.channelsLowerRange, - }); + private getCachedDate(dateStr: string): Date { + let date = this.dateCache.get(dateStr); + if (!date) { + date = new Date(dateStr); + this.dateCache.set(dateStr, date); + } + return date; } - enrichProgramData() { + private enrichProgramData(): EnrichedChannel[] { + const hourWidth = this.hourWidth$.value; + return this.originalEpgData.map((channel) => { + const filteredPrograms = channel.programs + .filter((item) => { + const itemDate = format( + this.getCachedDate(item.start), + 'yyyyMMdd' + ); + return itemDate === this.today; + }) + .map((program) => { + const startDate = this.getCachedDate(program.start); + const stopDate = this.getCachedDate(program.stop); + const startPosition = + (startDate.getHours() + startDate.getMinutes() / 60) * + hourWidth; + const duration = differenceInMinutes(stopDate, startDate); + const width = (duration * hourWidth) / 60; + + return { + ...program, + startDate, + stopDate, + startPosition, + width, + }; + }); + return { ...channel, - programs: channel.programs - .filter((item) => item.start.includes(this.today)) - .map((program) => { - const startDate = parse( - program.start, - 'yyyyMMddHHmmss XXXX', - addDays(new Date(), 1) - ); - const stopDate = parse( - program.stop, - 'yyyyMMddHHmmss XXXX', - addDays(new Date(), 1) - ); - return { - ...program, - startDate, - stopDate, - startPosition: this.positionToStartInPx(startDate), - width: this.programDurationInPx( - startDate, - stopDate - ), - }; - }), + programs: filteredPrograms, }; }); } - positionToStartInPx(startDate: Date) { - return ( - (startDate.getHours() + startDate.getMinutes() / 60) * - this.hourWidth - ); - } - - programDurationInPx(startDate: Date, stopDate: Date) { - const duration = differenceInMinutes(stopDate, startDate); - return (duration * this.hourWidth) / 60; - } - - recalculate(): void { - this.channels.forEach((channel) => { - channel.programs = channel.programs.map((program: any) => { - return { - ...program, - startPosition: this.positionToStartInPx(program.startDate), - width: this.programDurationInPx( - program.startDate, - program.stopDate - ), - }; - }); - }); - } - zoomIn(): void { - this.hourWidth += 50; - this.recalculate(); + if (this.hourWidth$.value >= 800) return; + this.hourWidth$.next(this.hourWidth$.value + 50); + this.channels$.next(this.enrichProgramData()); this.calculateCurrentTimeBar(); + this.cdr.detectChanges(); } zoomOut(): void { - if (this.hourWidth <= 50) return; - this.hourWidth -= 50; - this.recalculate(); + if (this.hourWidth$.value <= 50) return; + this.hourWidth$.next(this.hourWidth$.value - 50); + this.channels$.next(this.enrichProgramData()); this.calculateCurrentTimeBar(); + this.cdr.detectChanges(); } calculateCurrentTimeBar(): void { - const timeNow = new Date(); this.currentTimeLine = - (timeNow.getHours() + timeNow.getMinutes() / 60) * this.hourWidth; + (new Date().getHours() + new Date().getMinutes() / 60) * + this.hourWidth$.value; + this.cdr.detectChanges(); } switchDay(direction: 'prev' | 'next'): void { + const currentDate = parse(this.today, 'yyyyMMdd', new Date()); this.today = direction === 'prev' - ? format( - subDays(parse(this.today, 'yyyyMMdd', new Date()), 1), - 'yyyyMMdd' - ) - : format( - addDays(parse(this.today, 'yyyyMMdd', new Date()), 1), - 'yyyyMMdd' - ); + ? format(subDays(currentDate, 1), 'yyyyMMdd') + : format(addDays(currentDate, 1), 'yyyyMMdd'); + this.calculateCurrentTimeBar(); - this.channels = this.enrichProgramData(); + this.channels$.next(this.enrichProgramData()); + this.cdr.detectChanges(); } - /** - * Opens the dialog with details about the selected program - * @param program selected epg program - */ showDescription(program: EpgProgram): void { this.dialog.open(EpgItemDescriptionComponent, { data: program, diff --git a/src/app/player/components/video-player/video-player.component.ts b/src/app/player/components/video-player/video-player.component.ts index 3f0236e4f..f47adc700 100644 --- a/src/app/player/components/video-player/video-player.component.ts +++ b/src/app/player/components/video-player/video-player.component.ts @@ -266,20 +266,42 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { /** * Opens the overlay with multi EPG view */ - openMultiEpgView() { - this.overlayRef = this.overlay.create(); + openMultiEpgView(): void { + const positionStrategy = this.overlay + .position() + .global() + .centerHorizontally() + .centerVertically(); + + this.overlayRef = this.overlay.create({ + hasBackdrop: true, + positionStrategy, + width: '100%', + height: '100%', + }); + const injector = Injector.create({ providers: [ - { provide: COMPONENT_OVERLAY_REF, useValue: this.overlayRef }, + { + provide: COMPONENT_OVERLAY_REF, + useValue: this.overlayRef, + }, ], }); - const componentPortal = new ComponentPortal( + + const portal = new ComponentPortal( MultiEpgContainerComponent, - undefined, + null, injector ); - this.overlayRef.addPanelClass('epg-overlay'); - this.overlayRef.attach(componentPortal); + + const componentRef = this.overlayRef.attach(portal); + componentRef.instance.playlistChannels = + this.store.select(selectChannels); + + this.overlayRef.backdropClick().subscribe(() => { + this.overlayRef.dispose(); + }); } openUrl(url: string) {