mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
feat(epg): epg program list with date navigation
This commit is contained in:
4 files changed
+147
-76
No files matched your search
@@ -1,49 +1,87 @@
|
|||||||
<ng-container *ngIf="channel; else noEpg">
|
<ng-container *ngIf="channel; else noEpg">
|
||||||
<div fxFlex="80px" id="channel-info" fxLayoutAlign="row" fxLayoutGap="10px">
|
<!-- channel info panel -->
|
||||||
<div fxLayoutAlign="column" fxLayoutGap="10px">
|
<div fxFlex="120px" id="channel-info" fxLayout="column">
|
||||||
<div fxFlex="48px" fxFlexAlign="center">
|
<div fxFlex="79px" fxLayout="row" fxLayoutGap="10px">
|
||||||
|
<div
|
||||||
|
fxFlex="60px"
|
||||||
|
class="channel-icon"
|
||||||
|
fxLayoutAlign="center center"
|
||||||
|
>
|
||||||
<img *ngIf="channel?.icon" [src]="channel.icon" width="48" />
|
<img *ngIf="channel?.icon" [src]="channel.icon" width="48" />
|
||||||
</div>
|
</div>
|
||||||
<div fxFlex="100">
|
<div fxFlex="100" fxFlexAlign="center">
|
||||||
<div class="channel-name">{{ channel?.name[0]?.value }}</div>
|
<div class="channel-name">
|
||||||
<ng-container>
|
{{ channel?.name[0]?.value }}
|
||||||
<div class="playing-now">Playing now:</div>
|
</div>
|
||||||
<div class="epg-program-name">
|
<div class="program-name">
|
||||||
{{
|
{{
|
||||||
playingNow
|
playingNow ? playingNow.title[0]?.value : 'Live stream'
|
||||||
? playingNow.title[0]?.value
|
}}
|
||||||
: 'Live stream'
|
</div>
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div fxFlex="40px" fxLayout="row" id="date-navigator">
|
||||||
|
<button
|
||||||
|
fxFlex="40px"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="changeDate('prev')"
|
||||||
|
matTooltip="Previous day"
|
||||||
|
>
|
||||||
|
<mat-icon>chevron_left</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
class="selected-date"
|
||||||
|
fxFlex="100"
|
||||||
|
fxLayoutAlign="center center"
|
||||||
|
>
|
||||||
|
{{ dateToday | momentDate: 'YYYYMMDD':'MMMM Do dddd' }}</span
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
fxFlex="40px"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="changeDate('next')"
|
||||||
|
matTooltip="Next day"
|
||||||
|
>
|
||||||
|
<mat-icon>chevron_right</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- program list -->
|
||||||
<mat-selection-list fxFlex [multiple]="false">
|
<mat-selection-list fxFlex [multiple]="false">
|
||||||
<mat-list-option
|
<ng-container *ngIf="items?.length > 0; else epgNotAvailable">
|
||||||
*ngFor="let item of items"
|
<mat-list-option
|
||||||
[value]="item"
|
*ngFor="let item of items"
|
||||||
[class.active]="timeNow >= item.start && timeNow <= item.stop"
|
[value]="item"
|
||||||
(click)="
|
[class.active]="timeNow >= item.start && timeNow <= item.stop"
|
||||||
setEpgProgram(
|
(click)="
|
||||||
item,
|
setEpgProgram(
|
||||||
timeNow >= item.start && timeNow <= item.stop
|
item,
|
||||||
)
|
timeNow >= item.start && timeNow <= item.stop
|
||||||
"
|
)
|
||||||
>
|
"
|
||||||
<div matLine>
|
>
|
||||||
<span class="time">{{ item.start }} - {{ item.stop }}</span>
|
<div matLine>
|
||||||
<span
|
<span class="time">{{ item.start }} - {{ item.stop }}</span>
|
||||||
*ngIf="timeNow >= item.start && timeNow <= item.stop"
|
<span
|
||||||
class="live-badge"
|
*ngIf="timeNow >= item.start && timeNow <= item.stop"
|
||||||
>live</span
|
class="live-badge"
|
||||||
>
|
>live</span
|
||||||
</div>
|
>
|
||||||
<p matLine>{{ item?.title[0]?.value }}</p>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<p matLine>{{ item?.title[0]?.value }}</p>
|
||||||
</mat-list-option>
|
<mat-divider></mat-divider>
|
||||||
|
</mat-list-option>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #epgNotAvailable>
|
||||||
|
<mat-list-option
|
||||||
|
>Ooops, EPG is not available for the selected
|
||||||
|
date</mat-list-option
|
||||||
|
>
|
||||||
|
</ng-template>
|
||||||
</mat-selection-list>
|
</mat-selection-list>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #noEpg>
|
<ng-template #noEpg>
|
||||||
<mat-list>
|
<mat-list>
|
||||||
<mat-list-item>
|
<mat-list-item>
|
||||||
|
|||||||
@@ -12,10 +12,9 @@
|
|||||||
.active {
|
.active {
|
||||||
border-bottom: 1px dashed #666;
|
border-bottom: 1px dashed #666;
|
||||||
background: linear-gradient(to top, #d2d2d2 0%, #d6d6d6 20%, rgba(255, 255, 255, 0) 100%)
|
background: linear-gradient(to top, #d2d2d2 0%, #d6d6d6 20%, rgba(255, 255, 255, 0) 100%)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time, .playing-now {
|
.time {
|
||||||
color: #666;
|
color: #666;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
@@ -23,29 +22,27 @@
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.epg-program-name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#channel-info {
|
#channel-info {
|
||||||
padding:10px;
|
|
||||||
border-bottom: 1px solid #ccc;
|
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);
|
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
|
||||||
|
|
||||||
.channel-name {
|
.channel-icon {
|
||||||
font-weight: 800;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-list {
|
.channel-name {
|
||||||
height: calc(100vh - 110px);
|
font-weight: 800;
|
||||||
overflow: auto;
|
}
|
||||||
|
|
||||||
|
.program-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-selection-list {
|
mat-selection-list {
|
||||||
@@ -60,4 +57,13 @@ mat-selection-list {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#date-navigator {
|
||||||
|
border-top: 1px solid #999;
|
||||||
|
|
||||||
|
.selected-date {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { EpgListComponent, EpgData } from './epg-list.component';
|
import { EpgListComponent, EpgData } from './epg-list.component';
|
||||||
import { MatListModule } from '@angular/material/list';
|
import { MatListModule } from '@angular/material/list';
|
||||||
import { MockModule } from 'ng-mocks';
|
import { MockModule, MockPipe } from 'ng-mocks';
|
||||||
import { ElectronService } from '../../../services/electron.service';
|
import { ElectronService } from '../../../services/electron.service';
|
||||||
import { ElectronServiceStub } from '../../../home/home.component.spec';
|
import { ElectronServiceStub } from '../../../home/home.component.spec';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { EPG_GET_PROGRAM_DONE } from '../../../shared/ipc-commands';
|
import { EPG_GET_PROGRAM_DONE } from '../../../shared/ipc-commands';
|
||||||
import { ChannelStore } from '../../../state';
|
import { ChannelStore } from '../../../state';
|
||||||
|
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
|
||||||
describe('EpgListComponent', () => {
|
describe('EpgListComponent', () => {
|
||||||
let component: EpgListComponent;
|
let component: EpgListComponent;
|
||||||
@@ -77,8 +79,8 @@ describe('EpgListComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [EpgListComponent],
|
declarations: [EpgListComponent, MockPipe(MomentDatePipe)],
|
||||||
imports: [MockModule(MatListModule)],
|
imports: [MockModule(MatIconModule), MockModule(MatListModule)],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export class EpgListComponent {
|
|||||||
/** Current time as formatted string */
|
/** Current time as formatted string */
|
||||||
timeNow: string;
|
timeNow: string;
|
||||||
|
|
||||||
|
selectedDate: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of EpgListComponent
|
* Creates an instance of EpgListComponent
|
||||||
* @param electronService
|
* @param electronService
|
||||||
@@ -61,25 +63,11 @@ export class EpgListComponent {
|
|||||||
*/
|
*/
|
||||||
handleEpgData(programs: { payload: EpgData }): void {
|
handleEpgData(programs: { payload: EpgData }): void {
|
||||||
if (programs.payload?.items?.length > 0) {
|
if (programs.payload?.items?.length > 0) {
|
||||||
|
this.programs = programs;
|
||||||
this.timeNow = moment(Date.now()).format('HH:mm');
|
this.timeNow = moment(Date.now()).format('HH:mm');
|
||||||
this.dateToday = moment(Date.now()).format('YYYYMMDD');
|
this.dateToday = moment(Date.now()).format('YYYYMMDD');
|
||||||
this.channel = programs.payload?.channel;
|
this.channel = programs.payload?.channel;
|
||||||
this.items = programs.payload?.items
|
this.items = this.selectPrograms(programs);
|
||||||
.filter((item) =>
|
|
||||||
item.start.includes(this.dateToday.toString())
|
|
||||||
)
|
|
||||||
.map((program) => ({
|
|
||||||
...program,
|
|
||||||
start: moment(program.start, 'YYYYMMDDHHmm ZZ').format(
|
|
||||||
'HH:mm'
|
|
||||||
),
|
|
||||||
stop: moment(program.stop, 'YYYYMMDDHHmm ZZ').format(
|
|
||||||
'HH:mm'
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a.start.localeCompare(b.start);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setPlayingNow();
|
this.setPlayingNow();
|
||||||
} else {
|
} else {
|
||||||
@@ -88,6 +76,42 @@ export class EpgListComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the program based on the active date
|
||||||
|
* @param programs object with all available epg programs for the active channel
|
||||||
|
*/
|
||||||
|
selectPrograms(programs: { payload: EpgData }): EpgProgram[] {
|
||||||
|
return programs.payload?.items
|
||||||
|
.filter((item) => item.start.includes(this.dateToday.toString()))
|
||||||
|
.map((program) => ({
|
||||||
|
...program,
|
||||||
|
start: moment(program.start, 'YYYYMMDDHHmm ZZ').format('HH:mm'),
|
||||||
|
stop: moment(program.stop, 'YYYYMMDDHHmm ZZ').format('HH:mm'),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.start.localeCompare(b.start);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the date to update the epg list with programs
|
||||||
|
* @param direction direction to switch
|
||||||
|
*/
|
||||||
|
changeDate(direction: 'next' | 'prev'): void {
|
||||||
|
let dateToSwitch;
|
||||||
|
if (direction === 'next') {
|
||||||
|
dateToSwitch = moment(this.dateToday, 'YYYYMMDD')
|
||||||
|
.add(1, 'days')
|
||||||
|
.format('YYYYMMDD');
|
||||||
|
} else if (direction === 'prev') {
|
||||||
|
dateToSwitch = moment(this.dateToday, 'YYYYMMDD')
|
||||||
|
.subtract(1, 'days')
|
||||||
|
.format('YYYYMMDD');
|
||||||
|
}
|
||||||
|
this.dateToday = dateToSwitch;
|
||||||
|
this.items = this.selectPrograms(this.programs);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the playing now variable based on the current time
|
* Sets the playing now variable based on the current time
|
||||||
*/
|
*/
|
||||||
@@ -99,7 +123,8 @@ export class EpgListComponent {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the provided epg program as active and starts to play
|
* Sets the provided epg program as active and starts to play
|
||||||
* @param program
|
* @param program epg program to set
|
||||||
|
* @param isLive live stream flag
|
||||||
*/
|
*/
|
||||||
setEpgProgram(program: EpgProgram, isLive?: boolean): void {
|
setEpgProgram(program: EpgProgram, isLive?: boolean): void {
|
||||||
isLive
|
isLive
|
||||||
|
|||||||
Reference in new issue
Block a user