feat: add dialog with detailed epg description

This commit is contained in:
4gray committed 2021-03-12 10:25:03 +01:00
1 parent 8d20cafe0f
commit 96e93c5b0c
14 files changed
+376 -69

No files matched your search

@@ -0,0 +1,40 @@
<h2 mat-dialog-title>
{{ 'EPG.PROGRAM_DIALOG.PROGRAM_DETAILS' | translate }}
</h2>
<mat-dialog-content class="mat-typography">
<ng-container *ngIf="epgProgram?.title?.length > 0">
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
</div>
<p data-test="title">{{ epgProgram.title[0].value }}</p>
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
</div>
<p data-test="lang">{{ epgProgram.title[0].lang }}</p>
</ng-container>
<ng-container *ngIf="epgProgram?.category?.length > 0">
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
</div>
<p data-test="category">{{ epgProgram.category[0].value }}</p>
</ng-container>
<ng-container *ngIf="epgProgram?.desc?.length > 0">
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
</div>
<p data-test="desc">{{ epgProgram.desc[0].value }}</p>
</ng-container>
<ng-container *ngIf="epgProgram?.rating?.length > 0">
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.PARENTAL_RATING_SYSTEM' | translate }}
</div>
<p data-test="rating">
{{ epgProgram.rating[0].value }} [{{ epgProgram.rating[0].system }}]
</p>
</ng-container>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close cdkFocusInitial color="primary">
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,7 @@
.subheading-2 {
color: #666;
text-decoration: underline;
text-transform: uppercase;
font-weight: 900;
font-size: 0.8em;
}
@@ -0,0 +1,59 @@
import { By } from '@angular/platform-browser';
import { MockPipe, MockModule } from 'ng-mocks';
import { TranslatePipe } from '@ngx-translate/core';
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EpgItemDescriptionComponent } from './epg-item-description.component';
import { EpgProgram } from '../../../models/epg-program.model';
describe('EpgItemDescriptionComponent', () => {
let component: EpgItemDescriptionComponent;
let fixture: ComponentFixture<EpgItemDescriptionComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
EpgItemDescriptionComponent,
MockPipe(TranslatePipe),
],
imports: [MockModule(MatDialogModule)],
providers: [{ provide: MAT_DIALOG_DATA, useValue: {} }],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(EpgItemDescriptionComponent);
component = fixture.componentInstance;
component.epgProgram = {} as EpgProgram;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render epg details in the dialog', () => {
component.epgProgram = {
title: [{ value: 'TV Show 1', lang: 'ru' }],
desc: [{ value: 'Highly interesting show about pets' }],
category: [{ value: 'Fun' }],
} as EpgProgram;
fixture.detectChanges();
const title = fixture.debugElement.query(By.css('[data-test="title"]'));
expect(title.nativeNode.innerHTML).toContain(
component.epgProgram.title[0].value
);
const category = fixture.debugElement.query(
By.css('[data-test="category"]')
);
expect(category.nativeNode.innerHTML).toContain(
component.epgProgram.category[0].value
);
const desc = fixture.debugElement.query(By.css('[data-test="desc"]'));
expect(desc.nativeNode.innerHTML).toContain(
component.epgProgram.desc[0].value
);
});
});
@@ -0,0 +1,20 @@
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { EpgProgram } from './../../../models/epg-program.model';
import { Component, Inject } from '@angular/core';
@Component({
selector: 'app-epg-item-description',
templateUrl: './epg-item-description.component.html',
styleUrls: ['./epg-item-description.component.scss'],
})
export class EpgItemDescriptionComponent {
/** EPG program object */
epgProgram: EpgProgram;
/**
* Creates an instance of the component and injects the program of the clicked epg program
* @param epgProgram epg program
*/
constructor(@Inject(MAT_DIALOG_DATA) epgProgram: EpgProgram) {
this.epgProgram = epgProgram;
}
}
@@ -0,0 +1,29 @@
<span class="time"
>{{ item.start | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm' }}
-
{{ item.stop | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm' }}</span
>
<span
*ngIf="timeNow >= item.start && timeNow <= item.stop"
class="live badge"
[matTooltip]="'EPG.LIVE_NOW' | translate"
>live</span
>
<span
*ngIf="
item.start < timeNow &&
item.stop < timeNow &&
item.start > timeshiftUntil
"
class="rec badge"
[matTooltip]="'EPG.TIMESHIFT_AVAILABLE' | translate"
>r</span
>
<span
(click)="$event.stopPropagation(); showDescription(item)"
*ngIf="item.desc.length > 0"
class="description-icon"
[matTooltip]="'EPG.PROGRAM_DIALOG.SHOW_PROGRAM_DETAILS' | translate"
><mat-icon>info</mat-icon></span
>
<mat-divider></mat-divider>
@@ -0,0 +1,29 @@
.time {
color: #666;
border-bottom: 1px solid #ccc;
font-size: 0.8em;
display: inline-block;
margin-bottom: 5px;
}
.badge {
border-radius: 2px;
padding: 2px 6px;
color: #fff;
text-transform: uppercase;
margin-left: 6px;
font-size: 0.8em;
}
.live {
background-color: red;
}
.rec {
background-color: grey;
}
.description-icon {
float: right;
cursor: pointer;
}
@@ -0,0 +1,103 @@
/* eslint-disable @typescript-eslint/unbound-method */
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatIconModule } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
import { MatListModule } from '@angular/material/list';
import { MomentDatePipe } from './../../../../shared/pipes/moment-date.pipe';
import { EpgProgram } from './../../../models/epg-program.model';
import { MockModule, MockPipe } from 'ng-mocks';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EpgListItemComponent } from './epg-list-item.component';
import { EpgItemDescriptionComponent } from './../epg-item-description/epg-item-description.component';
import * as moment from 'moment';
const EPG_PROGRAM_ITEM = {
start: moment(Date.now()).format('YYYYMMDD'),
stop: moment(Date.now()).format('YYYYMMDD'),
channel: '12345',
title: [{ lang: 'en', value: 'NOW on PBS' }],
desc: [
{
lang: 'en',
value:
"Jordan's Queen Rania has made job creation a priority to help curb the staggering unemployment rates among youths in the Middle East.",
},
],
date: ['20080711'],
category: [
{ lang: 'en', value: 'Newsmagazine' },
{ lang: 'en', value: 'Interview' },
],
episodeNum: [
{ system: 'dd_progid', value: 'EP01006886.0028' },
{ system: 'onscreen', value: '427' },
],
previouslyShown: [{ start: '20080711000000' }],
subtitles: [{ type: 'teletext' }],
rating: [
{
system: 'VCHIP',
value: 'TV-G',
},
],
credits: [
{
role: 'actor',
name: 'Peter Bergman',
},
],
icon: [
'http://imageswoapi.whatsonindia.com/WhatsOnTV/images/ProgramImages/xlarge/38B4DE4E9A7132257749051B6C8B4F699DB264F4V.jpg',
],
audio: [],
_attributes: {
start: moment(Date.now()).format('YYYYMMDD'),
stop: moment(Date.now()).format('YYYYMMDD'),
},
};
describe('EpgListItemComponent', () => {
let component: EpgListItemComponent;
let fixture: ComponentFixture<EpgListItemComponent>;
let dialog: MatDialog;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
EpgListItemComponent,
MockPipe(MomentDatePipe),
MockPipe(TranslatePipe),
],
imports: [
MockModule(MatDialogModule),
MockModule(MatListModule),
MockModule(MatIconModule),
MockModule(MatTooltipModule),
],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(EpgListItemComponent);
dialog = TestBed.inject(MatDialog);
component = fixture.componentInstance;
component.item = EPG_PROGRAM_ITEM;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should trigger the function to open the details dialog', () => {
spyOn(dialog, 'open');
component.showDescription({} as EpgProgram);
expect(dialog.open).toHaveBeenCalledTimes(1);
expect(dialog.open).toHaveBeenCalledWith(EpgItemDescriptionComponent, {
data: {},
});
});
});
@@ -0,0 +1,36 @@
import { EpgItemDescriptionComponent } from './../epg-item-description/epg-item-description.component';
import { MatDialog } from '@angular/material/dialog';
import { Component, Input } from '@angular/core';
import { EpgProgram } from '../../../models/epg-program.model';
@Component({
selector: 'app-epg-list-item',
templateUrl: './epg-list-item.component.html',
styleUrls: ['./epg-list-item.component.scss'],
})
export class EpgListItemComponent {
/** EPG Program to render */
@Input() item: EpgProgram;
/** Actual time */
@Input() timeNow: string;
/** Aviability of the timeshift function until date */
@Input() timeshiftUntil: string;
/**
* Creates an instance of EpgListItemComponent
* @param dialog angular material dialog
*/
constructor(private dialog: MatDialog) {}
/**
* Opens the dialog with details about the selected program
* @param program selected epg program
*/
showDescription(program: EpgProgram): void {
this.dialog.open(EpgItemDescriptionComponent, {
data: program,
});
}
}
@@ -56,51 +56,47 @@
*ngIf="timeshiftUntil$ | async as timeshiftUntil"
>
<ng-container *ngIf="items?.length > 0; else epgNotAvailable">
<mat-list-option
*ngFor="let item of items"
[value]="item"
[class.active]="timeNow >= item.start && timeNow <= item.stop"
(click)="
setEpgProgram(
item,
timeNow >= item.start && timeNow <= item.stop,
item.start > timeshiftUntil
)
"
[disabled]="
item.start < timeshiftUntil || item.start >= timeNow
"
>
<div matLine>
<span class="time"
>{{
item.start | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm'
}}
-
{{
item.stop | momentDate: 'YYYYMMDDHHmm ZZ':'HH:mm'
}}</span
>
<span
*ngIf="timeNow >= item.start && timeNow <= item.stop"
class="live badge"
[matTooltip]="'EPG.LIVE_NOW' | translate"
>live</span
>
<span
*ngIf="
item.start < timeNow &&
item.stop < timeNow &&
item.start > timeshiftUntil
<ng-container *ngFor="let item of items">
<mat-list-item
*ngIf="
item.start < timeshiftUntil || item.start >= timeNow;
else timeshiftItem
"
>
<div matLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="item"
></app-epg-list-item>
</div>
<p matLine>{{ item?.title[0]?.value }}</p>
</mat-list-item>
<ng-template #timeshiftItem>
<mat-list-option
[value]="item"
[class.active]="
timeNow >= item.start && timeNow <= item.stop
"
(click)="
setEpgProgram(
item,
timeNow >= item.start && timeNow <= item.stop,
item.start > timeshiftUntil
)
"
class="rec badge"
[matTooltip]="'EPG.TIMESHIFT_AVAILABLE' | translate"
>r</span
>
</div>
<p matLine>{{ item?.title[0]?.value }}</p>
<mat-divider></mat-divider>
</mat-list-option>
<div matLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="item"
></app-epg-list-item>
</div>
<p matLine>{{ item?.title[0]?.value }}</p>
</mat-list-option>
</ng-template>
</ng-container>
</ng-container>
<ng-template #epgNotAvailable>
<mat-list-option>{{
@@ -13,14 +13,6 @@
border-bottom: 1px dashed #666;
}
.time {
color: #666;
border-bottom: 1px solid #ccc;
font-size: 0.8em;
display: inline-block;
margin-bottom: 5px;
}
#channel-info {
border-bottom: 1px solid #ccc;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
@@ -48,22 +40,6 @@ mat-selection-list {
overflow: auto;
}
.badge {
border-radius: 2px;
padding: 2px 6px;
color: #fff;
text-transform: uppercase;
margin-left: 6px;
font-size: 0.8em;
}
.live {
background-color: red;
}
.rec {
background-color: grey;
}
#date-navigator {
border-top: 1px solid #999;
@@ -1,6 +1,8 @@
import { MockComponent } from 'ng-mocks';
/* eslint-disable @typescript-eslint/unbound-method */
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { TranslatePipe } from '@ngx-translate/core';
import { MatTooltipModule } from '@angular/material/tooltip';
/* eslint-disable @typescript-eslint/unbound-method */
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EpgListComponent, EpgData } from './epg-list.component';
import { MatListModule } from '@angular/material/list';
@@ -12,12 +14,14 @@ import { EPG_GET_PROGRAM_DONE } from '../../../../../ipc-commands';
import { Channel, ChannelStore } from '../../../state';
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
import { MatIconModule } from '@angular/material/icon';
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
describe('EpgListComponent', () => {
let component: EpgListComponent;
let fixture: ComponentFixture<EpgListComponent>;
let electronService: ElectronService;
let channelStore: ChannelStore;
let dialog: MatDialog;
const MOCKED_PROGRAMS = {
channel: {
@@ -90,11 +94,13 @@ describe('EpgListComponent', () => {
EpgListComponent,
MockPipe(MomentDatePipe),
MockPipe(TranslatePipe),
MockComponent(EpgListItemComponent),
],
imports: [
MockModule(MatIconModule),
MockModule(MatTooltipModule),
MockModule(MatListModule),
MockModule(MatDialogModule),
],
providers: [
{ provide: ElectronService, useClass: ElectronServiceStub },
@@ -107,6 +113,7 @@ describe('EpgListComponent', () => {
fixture = TestBed.createComponent(EpgListComponent);
component = fixture.componentInstance;
electronService = TestBed.inject(ElectronService);
dialog = TestBed.inject(MatDialog);
channelStore = TestBed.inject(ChannelStore);
channelStore.setActiveChannel(({
id: '',
+4
View File
@@ -1,3 +1,4 @@
import { EpgListItemComponent } from './components/epg-list/epg-list-item/epg-list-item.component';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChannelListContainerComponent } from './components/channel-list-container/channel-list-container.component';
@@ -7,6 +8,7 @@ import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component
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';
const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
@@ -14,7 +16,9 @@ const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
imports: [CommonModule, RouterModule.forChild(routes), SharedModule],
declarations: [
ChannelListContainerComponent,
EpgItemDescriptionComponent,
EpgListComponent,
EpgListItemComponent,
HtmlVideoPlayerComponent,
VideoPlayerComponent,
VjsPlayerComponent,
+1 -1
View File
@@ -28,7 +28,7 @@
"PARENTAL_RATING_SYSTEM": "Alterskennzeichnung (USK)",
"DESCRIPTION": "Beschreibung",
"LANGUAGE": "Sprache",
"SHOW_PROGRAM_DETAILS": "Show details about this program"
"SHOW_PROGRAM_DETAILS": "Details anzeigen"
}
},
"HOME": {
+1
View File
@@ -0,0 +1 @@
import 'jest-extended';