From b4db751fb66353700dbc9e2285232da28ee655cb Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 8 Sep 2022 19:10:41 +0200 Subject: [PATCH] feat: implement multi epg view --- api.ts | 28 ++- epg-worker.ts | 39 ++++ shared/ipc-commands.ts | 3 + .../multi-epg-container.component.html | 142 ++++++++++++ .../multi-epg-container.component.scss | 70 ++++++ .../multi-epg-container.component.ts | 212 ++++++++++++++++++ .../video-player/video-player.component.html | 8 + .../video-player/video-player.component.ts | 39 +++- src/app/player/player.module.ts | 24 +- 9 files changed, 550 insertions(+), 15 deletions(-) create mode 100644 src/app/player/components/multi-epg/multi-epg-container.component.html create mode 100644 src/app/player/components/multi-epg/multi-epg-container.component.scss create mode 100644 src/app/player/components/multi-epg/multi-epg-container.component.ts diff --git a/api.ts b/api.ts index be96de8f0..5a362e6ea 100644 --- a/api.ts +++ b/api.ts @@ -8,6 +8,8 @@ import { EPG_FETCH, EPG_FETCH_DONE, EPG_GET_CHANNELS, + EPG_GET_CHANNELS_BY_RANGE, + EPG_GET_CHANNELS_BY_RANGE_RESPONSE, EPG_GET_CHANNELS_DONE, EPG_GET_PROGRAM, EPG_GET_PROGRAM_DONE, @@ -192,6 +194,19 @@ export class Api { ) .on(EPG_ERROR, (event, arg) => this.mainWindow.webContents.send(EPG_ERROR, arg) + ) + .on(EPG_GET_CHANNELS_BY_RANGE, (event, arg) => { + console.log(JSON.stringify(arg)); + this.workerWindow.webContents.send( + EPG_GET_CHANNELS_BY_RANGE, + arg + ); + }) + .on(EPG_GET_CHANNELS_BY_RANGE_RESPONSE, (event, arg) => + this.mainWindow.webContents.send( + EPG_GET_CHANNELS_BY_RANGE_RESPONSE, + arg + ) ); ipcMain.on( @@ -244,7 +259,7 @@ export class Api { PLAYLIST_UPDATE_POSITIONS, (event, playlists: Partial) => playlists.forEach((list, index) => { - this.updatePlaylistById(list._id, { + this.updatePlaylistById((list as Playlist)._id, { ...list, position: index, }); @@ -369,7 +384,7 @@ export class Api { session.defaultSession.webRequest.onBeforeSendHeaders( (details, callback) => { details.requestHeaders['User-Agent'] = userAgent; - details.requestHeaders['Referer'] = referer; + details.requestHeaders['Referer'] = referer as string; callback({ requestHeaders: details.requestHeaders }); } ); @@ -505,10 +520,11 @@ export class Api { this.updatePlaylistById(id, { updateState: PlaylistUpdateState.NOT_UPDATED, }); - event.sender.send(ERROR, { - message: `File not found. Please check the entered playlist URL again.`, - status: err.response.status, - }); + if (event) + event.sender.send(ERROR, { + message: `File not found. Please check the entered playlist URL again.`, + status: err.response.status, + }); } } diff --git a/epg-worker.ts b/epg-worker.ts index 527f976ec..581a85fa3 100644 --- a/epg-worker.ts +++ b/epg-worker.ts @@ -8,6 +8,8 @@ import { EPG_FETCH, EPG_FETCH_DONE, EPG_GET_CHANNELS, + EPG_GET_CHANNELS_BY_RANGE, + EPG_GET_CHANNELS_BY_RANGE_RESPONSE, EPG_GET_CHANNELS_DONE, EPG_GET_PROGRAM, EPG_GET_PROGRAM_DONE, @@ -17,6 +19,9 @@ import { EpgProgram } from './src/app/player/models/epg-program.model'; // EPG data store let EPG_DATA: { channels: EpgChannel[]; programs: EpgProgram[] }; +let EPG_DATA_MERGED: { + [id: string]: EpgChannel & { programs: EpgProgram[] }; +} = {}; const loggerLabel = '[EPG Worker]'; /** @@ -62,10 +67,36 @@ const fetchEpgDataFromUrl = (epgUrl: string) => { const parseAndSetEpg = (xmlString) => { console.log(loggerLabel, 'start parsing...'); EPG_DATA = parser.parse(xmlString.toString()); + // map programs to channels + EPG_DATA_MERGED = convertEpgData(); ipcRenderer.send(EPG_FETCH_DONE); console.log(loggerLabel, 'done, parsing was finished...'); }; +const convertEpgData = () => { + let result: { + [id: string]: EpgChannel & { programs: EpgProgram[] }; + } = {}; + + EPG_DATA?.programs?.forEach((program) => { + if (!result[program.channel]) { + const channel = EPG_DATA?.channels?.find( + (channel) => channel.id === program.channel + ) as EpgChannel; + result[program.channel] = { + ...channel, + programs: [program], + }; + } else { + result[program.channel] = { + ...result[program.channel], + programs: [...result[program.channel].programs, program], + }; + } + }); + return result; +}; + // fetches epg data from the provided URL ipcRenderer.on(EPG_FETCH, (event, arg) => { console.log(loggerLabel, 'epg fetch command was triggered'); @@ -113,3 +144,11 @@ ipcRenderer.on(EPG_GET_CHANNELS, (event, args) => { payload: EPG_DATA, }); }); + +ipcRenderer.on(EPG_GET_CHANNELS_BY_RANGE, (event, args) => { + ipcRenderer.send(EPG_GET_CHANNELS_BY_RANGE_RESPONSE, { + payload: Object.entries(EPG_DATA_MERGED) + .slice(args.skip, args.limit) + .map((entry) => entry[1]), + }); +}); diff --git a/shared/ipc-commands.ts b/shared/ipc-commands.ts index ad3cc6b73..f8679836f 100644 --- a/shared/ipc-commands.ts +++ b/shared/ipc-commands.ts @@ -6,6 +6,9 @@ export const EPG_GET_PROGRAM = 'EPG:GET_PROGRAM'; export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE'; export const EPG_GET_CHANNELS = 'EPG:GET_CHANNELS'; export const EPG_GET_CHANNELS_DONE = 'EPG:GET_CHANNELS_DONE'; +export const EPG_GET_CHANNELS_BY_RANGE = 'EPG:EPG_GET_CHANNELS_BY_RANGE'; +export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE = + 'EPG:EPG_GET_CHANNELS_BY_RANGE_RESPONSE'; // Playlist related commands export const PLAYLIST_GET_ALL = 'PLAYLIST:GET_ALL'; 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 new file mode 100644 index 000000000..b377a32b4 --- /dev/null +++ b/src/app/player/components/multi-epg/multi-epg-container.component.html @@ -0,0 +1,142 @@ +
+ + + +
+ {{ today | momentDate: 'YYYYMMDD':'MMMM Do, dddd' }} +
+ + + + +
+
+ + + + + + + +
+ {{ item.name[0].value }} +
+
+
+
+
+
+ + + + + + {{ i }}:00 + + + + + + + + + +
+
+
+ +
+ +
+
+
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 new file mode 100644 index 000000000..4a8a8fd38 --- /dev/null +++ b/src/app/player/components/multi-epg/multi-epg-container.component.scss @@ -0,0 +1,70 @@ +.program-item { + cursor: pointer; +} + +rect { + stroke: #676767; + cursor: pointer; +} + +.channel > rect { + stroke: #676767; +} + +.program-item:hover > rect { + fill: #333; +} + +.program-name, +.channel-name { + -webkit-line-clamp: 2; + height: 32px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + padding: 6px 2px; +} + +#epg-container { + width: calc(100vw - 100px); + height: calc(100vh - 73px); + overflow-x: scroll; + overflow-y: hidden; +} + +.parent { + display: flex; + background-color: black; +} + +#channels-column { + width: 100px; + flex: none; + border-right: 2px solid #ccc; +} + +.channel-name { + margin: 0 5px; + text-align: center; +} + +#epg-navigation { + display: flex; + background-color: black; + margin-top: 30px; + border-bottom: 2px solid #fff; +} + +.today-date { + flex: 1; + align-items: center; + line-height: 40px; + height: 40px; + padding: 0 20px; +} + +#current-time-line { + stroke-width: 2px; + stroke: white; +} 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 new file mode 100644 index 000000000..4b0c43b38 --- /dev/null +++ b/src/app/player/components/multi-epg/multi-epg-container.component.ts @@ -0,0 +1,212 @@ +import { OverlayRef } from '@angular/cdk/overlay'; +import { + AfterViewInit, + Component, + ElementRef, + Inject, + NgZone, + OnInit, + ViewChild, +} from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +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 { 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'; + +@Component({ + selector: 'app-multi-epg-container', + templateUrl: './multi-epg-container.component.html', + styleUrls: ['./multi-epg-container.component.scss'], +}) +export class MultiEpgContainerComponent implements OnInit, AfterViewInit { + @ViewChild('epgContainer') epgContainer: ElementRef; + timeHeader = new Array(24); + hourWidth = 150; + barHeight = 50; + originalEpgData: (EpgChannel & { programs: EpgProgram[] })[] = []; + channels: (EpgChannel & { programs: EpgProgram[] })[] = []; + today = format(new Date(), 'yyyyMMdd'); + currentTimeLine = 0; + visibleChannels; + channelsLowerRange = 0; + channelsUpperRange; + + constructor( + private dataService: DataService, + private dialog: MatDialog, + private ngZone: NgZone, + @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 { + this.calculateCurrentTimeBar(); + } + + ngAfterViewInit(): 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); + + 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(); + } + + nextChannels(): void { + this.channelsLowerRange = this.channelsUpperRange; + this.channelsUpperRange = + this.channelsUpperRange + this.visibleChannels; + this.channels = []; + this.requestPrograms(); + } + + previousChannels(): void { + this.channelsUpperRange = + this.channelsUpperRange - this.visibleChannels; + this.channelsLowerRange = + this.channelsUpperRange - this.visibleChannels; + + this.requestPrograms(); + } + + requestPrograms() { + this.dataService.sendIpcEvent(EPG_GET_CHANNELS_BY_RANGE, { + limit: this.channelsUpperRange, + skip: this.channelsLowerRange, + }); + } + + enrichProgramData() { + return this.originalEpgData.map((channel) => { + 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 + ), + }; + }), + }; + }); + } + + 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(); + this.calculateCurrentTimeBar(); + } + + zoomOut(): void { + if (this.hourWidth <= 50) return; + this.hourWidth -= 50; + this.recalculate(); + this.calculateCurrentTimeBar(); + } + + calculateCurrentTimeBar(): void { + const timeNow = new Date(); + this.currentTimeLine = + (timeNow.getHours() + timeNow.getMinutes() / 60) * this.hourWidth; + } + + switchDay(direction: 'prev' | 'next'): void { + this.today = + direction === 'prev' + ? format( + subDays(parse(this.today, 'yyyyMMdd', new Date()), 1), + 'yyyyMMdd' + ) + : format( + addDays(parse(this.today, 'yyyyMMdd', new Date()), 1), + 'yyyyMMdd' + ); + this.calculateCurrentTimeBar(); + this.channels = this.enrichProgramData(); + } + + /** + * Opens the dialog with details about the selected program + * @param program selected epg program + */ + showDescription(program: EpgProgram): void { + this.dialog.open(EpgItemDescriptionComponent, { + data: program, + }); + } + + close() { + this.overlayRef.detach(); + } +} diff --git a/src/app/player/components/video-player/video-player.component.html b/src/app/player/components/video-player/video-player.component.html index e621fe94d..0aa7e89bc 100644 --- a/src/app/player/components/video-player/video-player.component.html +++ b/src/app/player/components/video-player/video-player.component.html @@ -48,6 +48,14 @@ {{ activeChannel?.name }}
+