mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
feat: integrate epg feature
This commit is contained in:
16 files changed
+380
-14
No files matched your search
@@ -4,6 +4,8 @@ import { AppComponent } from './app.component';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
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 { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
|
import { MockModule } from 'ng-mocks';
|
||||||
|
|
||||||
jest.mock('custom-electron-titlebar', () => {
|
jest.mock('custom-electron-titlebar', () => {
|
||||||
return {
|
return {
|
||||||
@@ -16,14 +18,23 @@ jest.mock('custom-electron-titlebar', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AppComponen t', () => {
|
class MatSnackBarStub {
|
||||||
|
open(): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AppComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||||
|
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
MockModule(MatSnackBarModule),
|
||||||
|
RouterTestingModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
],
|
],
|
||||||
imports: [RouterTestingModule, TranslateModule.forRoot()],
|
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { akitaDevtools } from '@datorama/akita';
|
|||||||
import { Titlebar, Color } from 'custom-electron-titlebar';
|
import { Titlebar, Color } from 'custom-electron-titlebar';
|
||||||
import { ElectronService } from './services/electron.service';
|
import { ElectronService } from './services/electron.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { ChannelStore } from './state';
|
||||||
|
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||||
|
import { Settings } from './settings/settings.interface';
|
||||||
|
import { EPG_ERROR, EPG_FETCH, EPG_FETCH_DONE } from './shared/ipc-commands';
|
||||||
|
|
||||||
// create custom title bar
|
// create custom title bar
|
||||||
new Titlebar({
|
new Titlebar({
|
||||||
@@ -20,10 +25,13 @@ new Titlebar({
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
constructor(
|
constructor(
|
||||||
|
private channelStore: ChannelStore,
|
||||||
private electronService: ElectronService,
|
private electronService: ElectronService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private translate: TranslateService
|
private translate: TranslateService,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
|
private storage: StorageMap
|
||||||
) {
|
) {
|
||||||
if (!AppConfig.production) {
|
if (!AppConfig.production) {
|
||||||
akitaDevtools(ngZone);
|
akitaDevtools(ngZone);
|
||||||
@@ -51,6 +59,32 @@ export class AppComponent {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.electronService.ipcRenderer
|
||||||
|
.on(EPG_FETCH_DONE, () => {
|
||||||
|
this.ngZone.run(() => {
|
||||||
|
this.channelStore.update((store) => ({
|
||||||
|
...store,
|
||||||
|
epgAvailable: true,
|
||||||
|
}));
|
||||||
|
this.snackBar.open(
|
||||||
|
'EPG was successfully downloaded!',
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
verticalPosition: 'bottom',
|
||||||
|
horizontalPosition: 'right',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on(EPG_ERROR, () => {
|
||||||
|
this.snackBar.open('EPG Error: something went wrong...', null, {
|
||||||
|
duration: 2000,
|
||||||
|
verticalPosition: 'bottom',
|
||||||
|
horizontalPosition: 'right',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(this.electronService.remote.process.platform === 'linux' ||
|
(this.electronService.remote.process.platform === 'linux' ||
|
||||||
this.electronService.remote.process.platform === 'win32') &&
|
this.electronService.remote.process.platform === 'win32') &&
|
||||||
@@ -70,4 +104,36 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes for app settings on component init
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.storage.get('settings').subscribe((settings: Settings) => {
|
||||||
|
if (
|
||||||
|
settings &&
|
||||||
|
Object.keys(settings).length > 0 &&
|
||||||
|
settings.epgUrl
|
||||||
|
) {
|
||||||
|
this.electronService.ipcRenderer.send(EPG_FETCH, {
|
||||||
|
url: settings.epgUrl,
|
||||||
|
});
|
||||||
|
this.snackBar.open('Fetch EPG data...', 'Close', {
|
||||||
|
verticalPosition: 'bottom',
|
||||||
|
horizontalPosition: 'right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all ipc command listeners on component destroy
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.electronService.ipcRenderer.removeAllListeners(EPG_FETCH_DONE);
|
||||||
|
this.electronService.ipcRenderer.removeAllListeners(EPG_ERROR);
|
||||||
|
this.electronService.ipcRenderer.removeAllListeners(
|
||||||
|
'add-playlist-view'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+3
@@ -16,6 +16,8 @@ import { FilterPipeModule } from 'ngx-filter-pipe';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { createChannel } from '../../../state';
|
import { createChannel } from '../../../state';
|
||||||
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
|
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
|
||||||
|
import { ElectronService } from '../../../services/electron.service';
|
||||||
|
import { ElectronServiceStub } from '../../../home/home.component.spec';
|
||||||
|
|
||||||
class MatSnackBarStub {
|
class MatSnackBarStub {
|
||||||
open(): void {}
|
open(): void {}
|
||||||
@@ -32,6 +34,7 @@ describe('ChannelListContainerComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
ChannelQuery,
|
ChannelQuery,
|
||||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||||
|
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
MockModule(MatSnackBarModule),
|
MockModule(MatSnackBarModule),
|
||||||
|
|||||||
+1
-4
@@ -79,10 +79,7 @@ export class ChannelListContainerComponent {
|
|||||||
*/
|
*/
|
||||||
selectChannel(channel: Channel): void {
|
selectChannel(channel: Channel): void {
|
||||||
this.selected = channel;
|
this.selected = channel;
|
||||||
this.channelStore.update((store) => ({
|
this.channelStore.setActiveChannel(channel);
|
||||||
...store,
|
|
||||||
active: channel,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<ng-container *ngIf="channel; else noEpg">
|
||||||
|
<div id="channel-info" fxLayoutAlign="row">
|
||||||
|
<img
|
||||||
|
fxFlex="48px"
|
||||||
|
*ngIf="channel?.icon"
|
||||||
|
[src]="channel.icon"
|
||||||
|
width="48"
|
||||||
|
/>
|
||||||
|
<div fxFlex="100" fxLayoutAlign="start center" class="channel-name">
|
||||||
|
{{ channel?.name[0]?.value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item
|
||||||
|
*ngFor="let item of items"
|
||||||
|
[class.active]="timeNow >= item.start && timeNow <= item.stop"
|
||||||
|
>
|
||||||
|
<div matLine>
|
||||||
|
<span class="time">{{ item.start }} - {{ item.stop }}</span>
|
||||||
|
</div>
|
||||||
|
<p matLine>{{ item?.title[0]?.value }}</p>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #noEpg>
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item>
|
||||||
|
Ooops, EPG is not available for this channel.<br />
|
||||||
|
Please add/change the url of EPG list the settings of the app.
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</ng-template>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.active {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#channel-info {
|
||||||
|
padding:10px;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
padding-left: 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-list {
|
||||||
|
height: calc(100vh - 110px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { EpgListComponent } from './epg-list.component';
|
||||||
|
import { MatListModule } from '@angular/material/list';
|
||||||
|
import { MockModule } from 'ng-mocks';
|
||||||
|
import { ElectronService } from 'app/services/electron.service';
|
||||||
|
import { ElectronServiceStub } from 'app/home/home.component.spec';
|
||||||
|
|
||||||
|
describe('EpgListComponent', () => {
|
||||||
|
let component: EpgListComponent;
|
||||||
|
let fixture: ComponentFixture<EpgListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [EpgListComponent],
|
||||||
|
imports: [MockModule(MatListModule)],
|
||||||
|
providers: [
|
||||||
|
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EpgListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Component, NgZone } from '@angular/core';
|
||||||
|
import { EpgChannel } from '../../models/epg-channel.model';
|
||||||
|
import { EpgProgram } from '../../models/epg-program.model';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import { ElectronService } from '../../../services/electron.service';
|
||||||
|
import { EPG_GET_PROGRAM_DONE } from '../../../shared/ipc-commands';
|
||||||
|
|
||||||
|
interface EpgData {
|
||||||
|
channel: EpgChannel;
|
||||||
|
items: EpgProgram[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-epg-list',
|
||||||
|
templateUrl: './epg-list.component.html',
|
||||||
|
styleUrls: ['./epg-list.component.scss'],
|
||||||
|
})
|
||||||
|
export class EpgListComponent {
|
||||||
|
/** Channel info in EPG format */
|
||||||
|
channel: EpgChannel;
|
||||||
|
|
||||||
|
/** Today as formatted date string */
|
||||||
|
dateToday: string;
|
||||||
|
|
||||||
|
/** Array with EPG programs */
|
||||||
|
items: EpgProgram[] = [];
|
||||||
|
|
||||||
|
/** Object with epg programs for the active channel */
|
||||||
|
programs: {
|
||||||
|
payload: EpgData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Current time as formatted string */
|
||||||
|
timeNow: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of EpgListComponent
|
||||||
|
* @param electronService
|
||||||
|
* @param ngZone
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private electronService: ElectronService,
|
||||||
|
private ngZone: NgZone
|
||||||
|
) {
|
||||||
|
this.electronService.ipcRenderer.on(
|
||||||
|
EPG_GET_PROGRAM_DONE,
|
||||||
|
(event, response) => {
|
||||||
|
this.ngZone.run(() => this.handleEpgData(response));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incoming epg programs for the active channel from the main process
|
||||||
|
* @param programs
|
||||||
|
*/
|
||||||
|
handleEpgData(programs: { payload: EpgData }): void {
|
||||||
|
if (programs.payload?.items?.length > 0) {
|
||||||
|
this.timeNow = moment(Date.now()).format('HH:mm');
|
||||||
|
this.dateToday = moment(Date.now()).format('YYYYMMDD');
|
||||||
|
this.channel = programs.payload?.channel;
|
||||||
|
this.items = 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);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.items = [];
|
||||||
|
this.channel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all ipc renderer listeners after destroy
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.electronService.ipcRenderer.removeAllListeners(
|
||||||
|
EPG_GET_PROGRAM_DONE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
<mat-toolbar color="primary">
|
<mat-toolbar color="primary">
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
mat-toolbar="Add to favorites"
|
|
||||||
(click)="drawer.toggle()"
|
(click)="drawer.toggle()"
|
||||||
matTooltip="Open channels list"
|
matTooltip="Open channels list"
|
||||||
>
|
>
|
||||||
@@ -40,8 +39,16 @@
|
|||||||
</button>
|
</button>
|
||||||
{{ activeChannel.name }}
|
{{ activeChannel.name }}
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button mat-icon-button (click)="openAbout()">
|
<!-- <button mat-icon-button (click)="openAbout()">
|
||||||
<mat-icon>info</mat-icon>
|
<mat-icon>info</mat-icon>
|
||||||
|
</button> -->
|
||||||
|
<button
|
||||||
|
*ngIf="epgAvailable$ | async"
|
||||||
|
mat-button
|
||||||
|
(click)="drawerRight.toggle()"
|
||||||
|
matTooltip="Open EPG list"
|
||||||
|
>
|
||||||
|
EPG
|
||||||
</button>
|
</button>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
|
|
||||||
@@ -65,4 +72,14 @@
|
|||||||
>
|
>
|
||||||
</app-html-video-player>
|
</app-html-video-player>
|
||||||
</mat-drawer-content>
|
</mat-drawer-content>
|
||||||
|
<!-- right sidebar content -->
|
||||||
|
<mat-drawer
|
||||||
|
position="end"
|
||||||
|
#drawerRight
|
||||||
|
mode="side"
|
||||||
|
(keydown.escape)="close()"
|
||||||
|
disableClose
|
||||||
|
>
|
||||||
|
<app-epg-list></app-epg-list>
|
||||||
|
</mat-drawer>
|
||||||
</mat-drawer-container>
|
</mat-drawer-container>
|
||||||
@@ -12,6 +12,7 @@ import { ChannelStore } from '../../../state/channel.store';
|
|||||||
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
|
import * as MOCKED_PLAYLIST from '../../../../mocks/playlist.json';
|
||||||
import { createChannel } from '../../../state';
|
import { createChannel } from '../../../state';
|
||||||
import { HtmlVideoPlayerComponent } from '../html-video-player/html-video-player.component';
|
import { HtmlVideoPlayerComponent } from '../html-video-player/html-video-player.component';
|
||||||
|
import { EpgListComponent } from '../epg-list/epg-list.component';
|
||||||
|
|
||||||
class MatSnackBarStub {
|
class MatSnackBarStub {
|
||||||
open(): void {}
|
open(): void {}
|
||||||
@@ -26,6 +27,7 @@ describe('VideoPlayerComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
MockComponent(EpgListComponent),
|
||||||
MockComponent(HtmlVideoPlayerComponent),
|
MockComponent(HtmlVideoPlayerComponent),
|
||||||
MockComponent(VjsPlayerComponent),
|
MockComponent(VjsPlayerComponent),
|
||||||
MockComponent(VideoPlayerComponent),
|
MockComponent(VideoPlayerComponent),
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ export class VideoPlayerComponent implements OnInit {
|
|||||||
/** Name of the selected channel */
|
/** Name of the selected channel */
|
||||||
channelTitle: string;
|
channelTitle: string;
|
||||||
|
|
||||||
|
/** EPG availability flag */
|
||||||
|
epgAvailable$: Observable<boolean> = this.channelQuery.select(
|
||||||
|
(store) => store.epgAvailable
|
||||||
|
);
|
||||||
|
|
||||||
/** Favorites list */
|
/** Favorites list */
|
||||||
favorites$: Observable<string[]> = this.channelQuery.select(
|
favorites$: Observable<string[]> = this.channelQuery.select(
|
||||||
(store) => store.favorites
|
(store) => store.favorites
|
||||||
@@ -42,6 +47,9 @@ export class VideoPlayerComponent implements OnInit {
|
|||||||
/** Sidebar object */
|
/** Sidebar object */
|
||||||
@ViewChild('sidenav') sideNav: MatSidenav;
|
@ViewChild('sidenav') sideNav: MatSidenav;
|
||||||
|
|
||||||
|
// DEV
|
||||||
|
epgPrograms = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of VideoPlayerComponent
|
* Creates an instance of VideoPlayerComponent
|
||||||
* @param channelQuery akita's channel query
|
* @param channelQuery akita's channel query
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { VideoPlayerComponent } from './components/video-player/video-player.com
|
|||||||
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
|
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
|
import { EpgListComponent } from './components/epg-list/epg-list.component';
|
||||||
|
|
||||||
const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
|
const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ const routes: Routes = [{ path: '', component: VideoPlayerComponent }];
|
|||||||
imports: [CommonModule, RouterModule.forChild(routes), SharedModule],
|
imports: [CommonModule, RouterModule.forChild(routes), SharedModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
ChannelListContainerComponent,
|
ChannelListContainerComponent,
|
||||||
|
EpgListComponent,
|
||||||
HtmlVideoPlayerComponent,
|
HtmlVideoPlayerComponent,
|
||||||
VideoPlayerComponent,
|
VideoPlayerComponent,
|
||||||
VjsPlayerComponent,
|
VjsPlayerComponent,
|
||||||
|
|||||||
@@ -12,6 +12,35 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<mat-list>
|
<mat-list>
|
||||||
|
<mat-list-item>
|
||||||
|
<div matLine fxLayout="row">
|
||||||
|
<p fxFlex>EPG Url (*.xml or *.xml.gz)</p>
|
||||||
|
<p fxFlex fxLayoutAlign="start center">
|
||||||
|
<mat-form-field class="full-width">
|
||||||
|
<input
|
||||||
|
placeholder="Enter a URL"
|
||||||
|
matInput
|
||||||
|
type="url"
|
||||||
|
formControlName="epgUrl"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
*ngIf="
|
||||||
|
(epgAvailable$ | async) &&
|
||||||
|
settingsForm.value.epgUrl
|
||||||
|
"
|
||||||
|
mat-icon-button
|
||||||
|
color="primary"
|
||||||
|
matTooltip="Refresh EPG"
|
||||||
|
type="button"
|
||||||
|
(click)="fetchEpg()"
|
||||||
|
>
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p matLine></p>
|
||||||
|
</mat-list-item>
|
||||||
<mat-list-item>
|
<mat-list-item>
|
||||||
<div matLine fxLayout="row">
|
<div matLine fxLayout="row">
|
||||||
<p fxFlex>Video player</p>
|
<p fxFlex>Video player</p>
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { FormGroup, FormBuilder } from '@angular/forms';
|
|||||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { Settings } from './settings.interface';
|
import { Settings } from './settings.interface';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { ElectronService } from 'app/services/electron.service';
|
import { ElectronService } from 'app/services/electron.service';
|
||||||
|
import { ChannelQuery } from 'app/state';
|
||||||
|
import { EPG_FETCH } from 'app/shared/ipc-commands';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
@@ -43,14 +45,23 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||||||
/** Update message to show */
|
/** Update message to show */
|
||||||
updateMessage: string;
|
updateMessage: string;
|
||||||
|
|
||||||
|
/** EPG availability flag */
|
||||||
|
epgAvailable$: Observable<boolean> = this.channelQuery.select(
|
||||||
|
(store) => store.epgAvailable
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of SettingsComponent and injects some dependencies into the component
|
* Creates an instance of SettingsComponent and injects some dependencies into the component
|
||||||
|
* @param channelQuery
|
||||||
|
* @param electronService
|
||||||
* @param formBuilder
|
* @param formBuilder
|
||||||
|
* @param http
|
||||||
* @param router
|
* @param router
|
||||||
* @param snackBar
|
* @param snackBar
|
||||||
* @param storage
|
* @param storage
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
|
private channelQuery: ChannelQuery,
|
||||||
private electronService: ElectronService,
|
private electronService: ElectronService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
@@ -60,6 +71,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||||||
) {
|
) {
|
||||||
this.settingsForm = this.formBuilder.group({
|
this.settingsForm = this.formBuilder.group({
|
||||||
player: ['html5'], // default value
|
player: ['html5'], // default value
|
||||||
|
epgUrl: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subscription.add(
|
this.subscription.add(
|
||||||
@@ -88,7 +100,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||||||
this.subscription.add(
|
this.subscription.add(
|
||||||
this.storage.get('settings').subscribe((settings: Settings) => {
|
this.storage.get('settings').subscribe((settings: Settings) => {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
this.settingsForm.setValue(settings);
|
this.settingsForm.setValue({
|
||||||
|
player: settings.player ? settings.player : 'html5',
|
||||||
|
epgUrl: settings.epgUrl ? settings.epgUrl : '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -110,6 +125,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||||||
duration: 2000,
|
duration: 2000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
// check whether the epg url was changed or not
|
||||||
|
if (this.settingsForm.value.epgUrl) {
|
||||||
|
this.fetchEpg();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -127,4 +146,17 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and updates EPG from the given URL
|
||||||
|
*/
|
||||||
|
fetchEpg(): void {
|
||||||
|
this.electronService.ipcRenderer.send(EPG_FETCH, {
|
||||||
|
url: this.settingsForm.value.epgUrl,
|
||||||
|
});
|
||||||
|
this.snackBar.open('Fetch EPG data...', 'Close', {
|
||||||
|
verticalPosition: 'bottom',
|
||||||
|
horizontalPosition: 'right',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,5 @@ export type VideoPlayerType = 'html5' | 'videojs';
|
|||||||
*/
|
*/
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
player: VideoPlayerType;
|
player: VideoPlayerType;
|
||||||
|
epgUrl: string;
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
|
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
|
||||||
|
import { EPG_GET_PROGRAM } from '../shared/ipc-commands';
|
||||||
import { ElectronService } from '../services/electron.service';
|
import { ElectronService } from '../services/electron.service';
|
||||||
import { Channel } from './channel.model';
|
import { Channel } from './channel.model';
|
||||||
|
|
||||||
export interface ChannelState extends EntityState<Channel> {
|
export interface ChannelState extends EntityState<Channel> {
|
||||||
|
active: Channel;
|
||||||
|
epgAvailable: boolean;
|
||||||
favorites: string[];
|
favorites: string[];
|
||||||
playlistId: string;
|
playlistId: string;
|
||||||
active: Channel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -14,13 +16,14 @@ export interface ChannelState extends EntityState<Channel> {
|
|||||||
export class ChannelStore extends EntityStore<ChannelState> {
|
export class ChannelStore extends EntityStore<ChannelState> {
|
||||||
/**
|
/**
|
||||||
* Creates an instance of ChannelStore
|
* Creates an instance of ChannelStore
|
||||||
* @param channelStore channels store
|
* @param electronService electron service
|
||||||
*/
|
*/
|
||||||
constructor(private electronService: ElectronService) {
|
constructor(private electronService: ElectronService) {
|
||||||
super({
|
super({
|
||||||
|
active: undefined,
|
||||||
|
epgAvailable: false,
|
||||||
favorites: [],
|
favorites: [],
|
||||||
playlistId: '',
|
playlistId: '',
|
||||||
active: undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,4 +48,18 @@ export class ChannelStore extends EntityStore<ChannelState> {
|
|||||||
return { favorites };
|
return { favorites };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates selected/active channel in store
|
||||||
|
* @param channel selected channel
|
||||||
|
*/
|
||||||
|
setActiveChannel(channel: Channel): void {
|
||||||
|
this.update((store) => ({
|
||||||
|
...store,
|
||||||
|
active: channel,
|
||||||
|
}));
|
||||||
|
this.electronService.ipcRenderer.send(EPG_GET_PROGRAM, {
|
||||||
|
channelName: channel.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user