From 0eecfd1163a36019a7b600290fe5f01ee3bb9677 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 8 Jul 2021 22:37:31 +0200 Subject: [PATCH] feat: add info popup with epg info for active channel This commit closes #51 --- .../html-video-player.component.ts | 4 +- .../info-overlay/info-overlay.component.html | 36 +++++++++ .../info-overlay/info-overlay.component.scss | 73 ++++++++++++++++++ .../info-overlay.component.spec.ts | 29 +++++++ .../info-overlay/info-overlay.component.ts | 76 +++++++++++++++++++ .../video-player/video-player.component.html | 6 ++ .../video-player.component.spec.ts | 2 + .../video-player/video-player.component.ts | 6 ++ src/app/player/player.module.ts | 2 + 9 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/app/player/components/info-overlay/info-overlay.component.html create mode 100644 src/app/player/components/info-overlay/info-overlay.component.scss create mode 100644 src/app/player/components/info-overlay/info-overlay.component.spec.ts create mode 100644 src/app/player/components/info-overlay/info-overlay.component.ts diff --git a/src/app/player/components/html-video-player/html-video-player.component.ts b/src/app/player/components/html-video-player/html-video-player.component.ts index 5e755d045..eba6273f9 100644 --- a/src/app/player/components/html-video-player/html-video-player.component.ts +++ b/src/app/player/components/html-video-player/html-video-player.component.ts @@ -73,6 +73,8 @@ export class HtmlVideoPlayerComponent implements OnChanges, OnDestroy { * Destroy hls instance on component destroy */ ngOnDestroy(): void { - this.hls.destroy(); + if (this.hls) { + this.hls.destroy(); + } } } diff --git a/src/app/player/components/info-overlay/info-overlay.component.html b/src/app/player/components/info-overlay/info-overlay.component.html new file mode 100644 index 000000000..fa4a28f7e --- /dev/null +++ b/src/app/player/components/info-overlay/info-overlay.component.html @@ -0,0 +1,36 @@ +
+ +
+
+ {{ epgProgram?.title[0]?.value }} | {{ channel.name }} +
+
+ {{ epgProgram?.desc[0]?.value }} +
+
+
+
+ {{ start | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm' }} + - + {{ stop | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm' }} +
+
+
+
diff --git a/src/app/player/components/info-overlay/info-overlay.component.scss b/src/app/player/components/info-overlay/info-overlay.component.scss new file mode 100644 index 000000000..6b302e27a --- /dev/null +++ b/src/app/player/components/info-overlay/info-overlay.component.scss @@ -0,0 +1,73 @@ +.channel-overlay { + position: absolute; + bottom: 35px; + width: 100%; + background: #2c2b2fdc; + min-height: 130px; + max-height: 165px; + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + backdrop-filter: blur(10px); +} + +.program-name, +.program-description { + margin: 20px 0; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.channel-logo { + margin-top: 20px; +} + +.program-progress { + width: 96%; + background: #ccc; + height: 20px; + margin-bottom: 8px; +} + +.program-name { + width: 96%; + max-height: 20px; + -webkit-line-clamp: 1; +} + +.program-description { + width: 96%; + max-height: 55px; + -webkit-line-clamp: 3; +} + +.program-finished { + background: #717171; + height: 20px; + font-size: 0.8em; + padding-left: 10px; + color: #000; + line-height: 20px; +} + +.duration { + position: relative; + top: -18px; + font-size: 0.8em; + color: #000; + left: 10px; +} + +.visible { + visibility: visible; + opacity: 1; +} + +.hidden { + visibility: hidden; + opacity: 0; + transition: visibility 0s 2s, opacity 2s linear; +} diff --git a/src/app/player/components/info-overlay/info-overlay.component.spec.ts b/src/app/player/components/info-overlay/info-overlay.component.spec.ts new file mode 100644 index 000000000..422157a8a --- /dev/null +++ b/src/app/player/components/info-overlay/info-overlay.component.spec.ts @@ -0,0 +1,29 @@ +import { MockPipe } from 'ng-mocks'; +import { MomentDatePipe } from './../../../shared/pipes/moment-date.pipe'; +/* tslint:disable:no-unused-variable */ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { InfoOverlayComponent } from './info-overlay.component'; + +describe('InfoOverlayComponent', () => { + let component: InfoOverlayComponent; + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [InfoOverlayComponent, MockPipe(MomentDatePipe)], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(InfoOverlayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/player/components/info-overlay/info-overlay.component.ts b/src/app/player/components/info-overlay/info-overlay.component.ts new file mode 100644 index 000000000..371015b35 --- /dev/null +++ b/src/app/player/components/info-overlay/info-overlay.component.ts @@ -0,0 +1,76 @@ +import { Channel } from './../../../state/channel.model'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { EpgProgram } from '../../models/epg-program.model'; +import * as moment from 'moment'; + +@Component({ + selector: 'app-info-overlay', + templateUrl: './info-overlay.component.html', + styleUrls: ['./info-overlay.component.scss'], +}) +export class InfoOverlayComponent implements OnChanges { + /** Active channel */ + @Input() channel: Channel | undefined; + + /** Current EPG program */ + @Input() epgProgram: EpgProgram | undefined; + + /** Visibility flag of the overlay popup */ + isVisible = false; + + /** Program duration */ + generalDuration!: number; + + /** Finished duration */ + finishedDuration!: number; + + /** Program start time */ + start: moment.Moment; + + /** Program end time */ + stop: moment.Moment; + + /** Timeout for the overlay visibility */ + currentTimeout; + + /** + * Calculates the necessary information for the visualization in the overview popup + * @param changes component input changes + */ + ngOnChanges(changes: SimpleChanges): void { + if (changes.channel && !changes.channel.firstChange) { + clearTimeout(this.currentTimeout); + this.isVisible = true; + this.currentTimeout = setTimeout(() => { + this.isVisible = false; + }, 4000); + } + if ( + changes.epgProgram && + changes.epgProgram.currentValue && + !changes.epgProgram.firstChange + ) { + const { stop, start } = changes.epgProgram.currentValue; + this.setProgramDuration(start, stop); + } + } + + /** + * Calculates and sets the duration of the program for the progress bar visualization + * @param start program start time + * @param stop program stop time + */ + setProgramDuration(start: number, stop: number): void { + this.stop = moment(stop, 'YYYYMMDDHHmm ZZ'); + this.start = moment(start, 'YYYYMMDDHHmm ZZ'); + const timeNow = moment(Date.now()); + + this.generalDuration = moment + .duration(this.stop.diff(this.start)) + .asMilliseconds(); + + this.finishedDuration = moment + .duration(this.stop.diff(timeNow)) + .asMilliseconds(); + } +} 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 5d76a99a0..f3940f120 100644 --- a/src/app/player/components/video-player/video-player.component.html +++ b/src/app/player/components/video-player/video-player.component.html @@ -68,6 +68,12 @@ [channel]="activeChannel$ | async" > + + + { MockComponent(VjsPlayerComponent), MockComponent(VideoPlayerComponent), MockComponent(ChannelListContainerComponent), + MockComponent(InfoOverlayComponent), VideoPlayerComponent, MockPipe(TranslatePipe), ], 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 19f1556cd..2c793b25c 100644 --- a/src/app/player/components/video-player/video-player.component.ts +++ b/src/app/player/components/video-player/video-player.component.ts @@ -1,3 +1,4 @@ +import { EpgProgram } from './../../models/epg-program.model'; import { Component, OnInit, ViewChild } from '@angular/core'; import { ChannelQuery, Channel, ChannelStore } from '../../../state'; import { Observable } from 'rxjs'; @@ -35,6 +36,11 @@ export class VideoPlayerComponent implements OnInit { (store) => store.epgAvailable ); + /** Current epg program */ + epgProgram$: Observable = this.channelQuery.select( + (store) => store.currentEpgProgram + ); + /** Favorites list */ favorites$: Observable = this.channelQuery.select( (store) => store.favorites diff --git a/src/app/player/player.module.ts b/src/app/player/player.module.ts index c30102efe..14f8eda2f 100644 --- a/src/app/player/player.module.ts +++ b/src/app/player/player.module.ts @@ -9,6 +9,7 @@ import { RouterModule, Routes } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { EpgListComponent } from './components/epg-list/epg-list.component'; import { EpgItemDescriptionComponent } from './components/epg-list/epg-item-description/epg-item-description.component'; +import { InfoOverlayComponent } from './components/info-overlay/info-overlay.component'; const routes: Routes = [{ path: '', component: VideoPlayerComponent }]; @@ -19,6 +20,7 @@ const routes: Routes = [{ path: '', component: VideoPlayerComponent }]; EpgItemDescriptionComponent, EpgListComponent, EpgListItemComponent, + InfoOverlayComponent, HtmlVideoPlayerComponent, VideoPlayerComponent, VjsPlayerComponent,