mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
feat: add info popup with epg info for active channel
This commit closes #51
This commit is contained in:
9 files changed
+233
-1
No files matched your search
@@ -73,6 +73,8 @@ export class HtmlVideoPlayerComponent implements OnChanges, OnDestroy {
|
|||||||
* Destroy hls instance on component destroy
|
* Destroy hls instance on component destroy
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.hls.destroy();
|
if (this.hls) {
|
||||||
|
this.hls.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<div
|
||||||
|
class="channel-overlay"
|
||||||
|
fxLayout="row"
|
||||||
|
*ngIf="channel && epgProgram"
|
||||||
|
[class.visible]="isVisible"
|
||||||
|
[class.hidden]="!isVisible"
|
||||||
|
>
|
||||||
|
<div fxFlex="100px" fxLayoutAlign="center start" class="channel-logo">
|
||||||
|
<img
|
||||||
|
[src]="epgProgram?.icon[0] || channel?.tvg?.logo"
|
||||||
|
width="68"
|
||||||
|
onerror="this.style.display='none'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div fxFlex>
|
||||||
|
<div class="program-name">
|
||||||
|
{{ epgProgram?.title[0]?.value }} | {{ channel.name }}
|
||||||
|
</div>
|
||||||
|
<div class="program-description">
|
||||||
|
{{ epgProgram?.desc[0]?.value }}
|
||||||
|
</div>
|
||||||
|
<div class="program-progress">
|
||||||
|
<div
|
||||||
|
class="program-finished"
|
||||||
|
[ngStyle]="{
|
||||||
|
width: (finishedDuration * 100) / generalDuration + '%'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div class="duration">
|
||||||
|
{{ start | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm' }}
|
||||||
|
-
|
||||||
|
{{ stop | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<InfoOverlayComponent>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,12 @@
|
|||||||
[channel]="activeChannel$ | async"
|
[channel]="activeChannel$ | async"
|
||||||
>
|
>
|
||||||
</app-html-video-player>
|
</app-html-video-player>
|
||||||
|
|
||||||
|
<!-- channel overlay -->
|
||||||
|
<app-info-overlay
|
||||||
|
[channel]="activeChannel$ | async"
|
||||||
|
[epgProgram]="epgProgram$ | async"
|
||||||
|
></app-info-overlay>
|
||||||
</mat-drawer-content>
|
</mat-drawer-content>
|
||||||
<!-- right sidebar content -->
|
<!-- right sidebar content -->
|
||||||
<mat-drawer
|
<mat-drawer
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { InfoOverlayComponent } from './../info-overlay/info-overlay.component';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
@@ -35,6 +36,7 @@ describe('VideoPlayerComponent', () => {
|
|||||||
MockComponent(VjsPlayerComponent),
|
MockComponent(VjsPlayerComponent),
|
||||||
MockComponent(VideoPlayerComponent),
|
MockComponent(VideoPlayerComponent),
|
||||||
MockComponent(ChannelListContainerComponent),
|
MockComponent(ChannelListContainerComponent),
|
||||||
|
MockComponent(InfoOverlayComponent),
|
||||||
VideoPlayerComponent,
|
VideoPlayerComponent,
|
||||||
MockPipe(TranslatePipe),
|
MockPipe(TranslatePipe),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { EpgProgram } from './../../models/epg-program.model';
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ChannelQuery, Channel, ChannelStore } from '../../../state';
|
import { ChannelQuery, Channel, ChannelStore } from '../../../state';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@@ -35,6 +36,11 @@ export class VideoPlayerComponent implements OnInit {
|
|||||||
(store) => store.epgAvailable
|
(store) => store.epgAvailable
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Current epg program */
|
||||||
|
epgProgram$: Observable<EpgProgram> = this.channelQuery.select(
|
||||||
|
(store) => store.currentEpgProgram
|
||||||
|
);
|
||||||
|
|
||||||
/** Favorites list */
|
/** Favorites list */
|
||||||
favorites$: Observable<string[]> = this.channelQuery.select(
|
favorites$: Observable<string[]> = this.channelQuery.select(
|
||||||
(store) => store.favorites
|
(store) => store.favorites
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { RouterModule, Routes } from '@angular/router';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { EpgListComponent } from './components/epg-list/epg-list.component';
|
import { EpgListComponent } from './components/epg-list/epg-list.component';
|
||||||
import { EpgItemDescriptionComponent } from './components/epg-list/epg-item-description/epg-item-description.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 }];
|
const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
|
|||||||
EpgItemDescriptionComponent,
|
EpgItemDescriptionComponent,
|
||||||
EpgListComponent,
|
EpgListComponent,
|
||||||
EpgListItemComponent,
|
EpgListItemComponent,
|
||||||
|
InfoOverlayComponent,
|
||||||
HtmlVideoPlayerComponent,
|
HtmlVideoPlayerComponent,
|
||||||
VideoPlayerComponent,
|
VideoPlayerComponent,
|
||||||
VjsPlayerComponent,
|
VjsPlayerComponent,
|
||||||
|
|||||||
Reference in new issue
Block a user