mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
52 files changed
+43280
-348
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
EPG_FETCH_DONE,
|
||||
EPG_GET_PROGRAM,
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
} from './ipc-commands';
|
||||
|
||||
const fs = require('fs');
|
||||
@@ -24,6 +25,9 @@ export class Api {
|
||||
/** Instance of the main application window */
|
||||
mainWindow: BrowserWindow;
|
||||
|
||||
/** Default user agent stored as a fallback value */
|
||||
defaultUserAgent: string;
|
||||
|
||||
/** Instance of the epg browser window */
|
||||
workerWindow: BrowserWindow;
|
||||
|
||||
@@ -61,18 +65,11 @@ export class Api {
|
||||
event.sender.send('parse-response', { payload: playlistObject });
|
||||
});
|
||||
|
||||
ipcMain.on('playlists-all', async (event) => {
|
||||
const playlists = await db.find(
|
||||
{ type: { $exists: false } },
|
||||
{ count: 1, title: 1, _id: 1, url: 1, importDate: 1 }
|
||||
);
|
||||
event.sender.send('playlist-all-result', {
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
ipcMain.on('playlists-all', (event) => this.sendAllPlaylists(event));
|
||||
|
||||
ipcMain.on('playlist-by-id', async (event, args) => {
|
||||
const playlist = await db.findOne({ _id: args.id });
|
||||
this.setUserAgent(playlist.userAgent);
|
||||
event.sender.send('parse-response', {
|
||||
payload: playlist,
|
||||
});
|
||||
@@ -137,6 +134,51 @@ export class Api {
|
||||
.on(EPG_ERROR, (event, arg) =>
|
||||
this.mainWindow.webContents.send(EPG_ERROR, arg)
|
||||
);
|
||||
|
||||
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
|
||||
const updated = await db.update(
|
||||
{ _id: args._id },
|
||||
{ $set: { title: args.title, userAgent: args.userAgent } }
|
||||
);
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
console.error('Error: Playlist details were not updated');
|
||||
}
|
||||
this.sendAllPlaylists(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends list with all playlists which are stored in the database
|
||||
* @param event main event
|
||||
*/
|
||||
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
|
||||
const playlists = await db.find(
|
||||
{ type: { $exists: false } },
|
||||
{
|
||||
count: 1,
|
||||
title: 1,
|
||||
_id: 1,
|
||||
url: 1,
|
||||
importDate: 1,
|
||||
userAgent: 1,
|
||||
filename: 1,
|
||||
}
|
||||
);
|
||||
event.sender.send('playlist-all-result', {
|
||||
payload: playlists,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user agent header for all http requests
|
||||
* @param userAgent user agent to use
|
||||
*/
|
||||
setUserAgent(userAgent: string): void {
|
||||
if (userAgent === undefined || userAgent === null || userAgent === '') {
|
||||
userAgent = this.defaultUserAgent;
|
||||
}
|
||||
this.mainWindow.webContents.setUserAgent(userAgent);
|
||||
console.log(`Success: Set "${userAgent}" as user agent header`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,6 +187,9 @@ export class Api {
|
||||
*/
|
||||
setEpgWorkerWindow(workerWindow: BrowserWindow): void {
|
||||
this.workerWindow = workerWindow;
|
||||
|
||||
// store default user agent as fallback
|
||||
this.defaultUserAgent = this.workerWindow.webContents.getUserAgent();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
// EPG related commands
|
||||
export const EPG_FETCH = 'EPG:FETCH';
|
||||
export const EPG_FETCH_DONE = 'EPG:FETCH_DONE';
|
||||
export const EPG_ERROR = 'EPG:ERROR';
|
||||
export const EPG_GET_PROGRAM = 'EPG:GET_PROGRAM';
|
||||
export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE';
|
||||
|
||||
// Playlist related commands
|
||||
export const PLAYLIST_SAVE_DETAILS = 'PLAYLIST:SAVE_DETAILS';
|
||||
|
||||
// General
|
||||
export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW';
|
||||
@@ -52,6 +52,10 @@ function createWindow(): BrowserWindow {
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
win = null;
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
return win;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { SHOW_WHATS_NEW } from './ipc-commands';
|
||||
const openAboutWindow = require('about-window').default;
|
||||
|
||||
export class AppMenu {
|
||||
@@ -75,6 +76,10 @@ export class AppMenu {
|
||||
return new MenuItem({
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'What is new',
|
||||
click: () => this.window.webContents.send(SHOW_WHATS_NEW),
|
||||
},
|
||||
{
|
||||
label: 'Report a bug',
|
||||
click: () =>
|
||||
|
||||
Generated
+42053
-71
File diff suppressed because it is too large.
Load diff
@@ -128,6 +128,7 @@
|
||||
"nedb-promises-ts": "0.0.1-4",
|
||||
"ngx-filter-pipe": "2.1.2",
|
||||
"ngx-uploader": "11.0.0",
|
||||
"ngx-whats-new": "0.0.1",
|
||||
"semver": "7.3.4",
|
||||
"uuid": "8.3.0",
|
||||
"video-js": "0.7.0"
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
<router-outlet></router-outlet>
|
||||
<!-- whats new dialog -->
|
||||
<ngx-whats-new
|
||||
*ngIf="isDialogVisible$ | async"
|
||||
(closeModal)="setDialogVisibility(false)"
|
||||
[items]="modals"
|
||||
[options]="options"
|
||||
>
|
||||
</ngx-whats-new>
|
||||
@@ -1,3 +1,4 @@
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
@@ -33,6 +34,7 @@ describe('AppComponent', () => {
|
||||
],
|
||||
imports: [
|
||||
MockModule(MatSnackBarModule),
|
||||
MockModule(NgxWhatsNewModule),
|
||||
RouterTestingModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
|
||||
+96
-36
@@ -7,7 +7,16 @@ 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 '../../ipc-commands';
|
||||
import {
|
||||
EPG_ERROR,
|
||||
EPG_FETCH,
|
||||
EPG_FETCH_DONE,
|
||||
SHOW_WHATS_NEW,
|
||||
} from '../../ipc-commands';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
import { WhatsNewService } from './services/whats-new.service';
|
||||
import * as semver from 'semver';
|
||||
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
|
||||
|
||||
// create custom title bar
|
||||
new Titlebar({
|
||||
@@ -22,31 +31,55 @@ new Titlebar({
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
/** Visibility flag of the "what is new" modal dialog */
|
||||
isDialogVisible$ = this.whatsNewService.dialogState$;
|
||||
/** Dialog options */
|
||||
options = this.whatsNewService.options;
|
||||
/** Modals to show for the updated version of the application */
|
||||
modals: ModalWindow[];
|
||||
|
||||
/**
|
||||
* Creates an instance of AppComponent
|
||||
*/
|
||||
constructor(
|
||||
private channelStore: ChannelStore,
|
||||
private electronService: ElectronService,
|
||||
private ngZone: NgZone,
|
||||
private router: Router,
|
||||
private translate: TranslateService,
|
||||
private settingsService: SettingsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private storage: StorageMap
|
||||
private storage: StorageMap,
|
||||
private whatsNewService: WhatsNewService
|
||||
) {
|
||||
this.translate.setDefaultLang('en');
|
||||
|
||||
if (electronService.isElectron) {
|
||||
console.log(process.env);
|
||||
console.log('Run in electron');
|
||||
console.log(
|
||||
'Electron ipcRenderer',
|
||||
this.electronService.ipcRenderer
|
||||
this.setRendererListeners();
|
||||
|
||||
if (
|
||||
(this.electronService.remote.process.platform === 'linux' ||
|
||||
this.electronService.remote.process.platform === 'win32') &&
|
||||
this.electronService.remote.process.argv.length > 2
|
||||
) {
|
||||
const filePath = this.electronService.remote.process.argv.find(
|
||||
(filepath) =>
|
||||
filepath.endsWith('.m3u') || filepath.endsWith('.m3u8')
|
||||
);
|
||||
console.log(
|
||||
'NodeJS childProcess',
|
||||
this.electronService.childProcess
|
||||
);
|
||||
} else {
|
||||
console.log('Run in browser');
|
||||
if (filePath) {
|
||||
const filePathsArray = filePath.split('/');
|
||||
const fileName = filePathsArray[filePathsArray.length - 1];
|
||||
this.electronService.ipcRenderer.send('open-file', {
|
||||
filePath,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all necessary listeners for the events from the renderer process
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.electronService.ipcRenderer.on('add-playlist-view', () => {
|
||||
this.ngZone.run(() => {
|
||||
this.router.navigateByUrl('/', { skipLocationChange: true });
|
||||
@@ -71,6 +104,7 @@ export class AppComponent {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.electronService.ipcRenderer.on(EPG_ERROR, () => {
|
||||
this.snackBar.open('EPG Error: something went wrong...', null, {
|
||||
duration: 2000,
|
||||
@@ -79,24 +113,14 @@ export class AppComponent {
|
||||
});
|
||||
});
|
||||
|
||||
if (
|
||||
(this.electronService.remote.process.platform === 'linux' ||
|
||||
this.electronService.remote.process.platform === 'win32') &&
|
||||
this.electronService.remote.process.argv.length > 2
|
||||
) {
|
||||
const filePath = this.electronService.remote.process.argv.find(
|
||||
(filepath) =>
|
||||
filepath.endsWith('.m3u') || filepath.endsWith('.m3u8')
|
||||
);
|
||||
if (filePath) {
|
||||
const filePathsArray = filePath.split('/');
|
||||
const fileName = filePathsArray[filePathsArray.length - 1];
|
||||
this.electronService.ipcRenderer.send('open-file', {
|
||||
filePath,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.electronService.ipcRenderer.on(SHOW_WHATS_NEW, () => {
|
||||
this.ngZone.run(() => {
|
||||
this.modals = this.whatsNewService.getModalsByVersion(
|
||||
this.electronService.getAppVersion()
|
||||
);
|
||||
this.setDialogVisibility(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,13 +139,49 @@ export class AppComponent {
|
||||
horizontalPosition: 'right',
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.theme) {
|
||||
this.settingsService.changeTheme(settings.theme);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// store current app version (TODO: "what is new" dialog in v0.5)
|
||||
this.storage
|
||||
.set('version', this.electronService.getAppVersion())
|
||||
.subscribe(() => {});
|
||||
this.handleWhatsNewDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the actual version of the application and shows the "what is new" dialog if the updated version was detected
|
||||
*/
|
||||
handleWhatsNewDialog(): void {
|
||||
const actualVersion = this.electronService.getAppVersion();
|
||||
this.storage.get('version').subscribe((version) => {
|
||||
const isNewVersion = semver.gt(actualVersion, version || '0.0.0');
|
||||
if (!version || isNewVersion) {
|
||||
this.modals = this.whatsNewService.getModalsByVersion(
|
||||
actualVersion
|
||||
);
|
||||
this.setDialogVisibility(true);
|
||||
}
|
||||
this.setVersion(actualVersion);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the version of the application in localstorage
|
||||
* @param version actual version of the application
|
||||
*/
|
||||
setVersion(version: string): void {
|
||||
this.storage.set('version', version).subscribe(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility flag of the modal window
|
||||
* @param visible show/hide window flag
|
||||
*/
|
||||
setDialogVisibility(visible: boolean): void {
|
||||
if (this.modals.length > 0) {
|
||||
this.whatsNewService.changeDialogVisibleState(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,9 +4,9 @@ import '../polyfills';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
|
||||
// NG Translate
|
||||
@@ -26,8 +26,8 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
CommonModule,
|
||||
HttpClientModule,
|
||||
NgxWhatsNewModule,
|
||||
SharedModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mat-tab-group.mat-background-primary .mat-tab-header {
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
.mat-tab-group.mat-background-primary .mat-tab-header-pagination {
|
||||
display:none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,10 @@ import { Playlist } from './playlist.interface';
|
||||
import { ElectronService } from '../services/electron.service';
|
||||
|
||||
/** Type to describe meta data of a playlist */
|
||||
export type PlaylistMeta = Pick<Playlist, 'count' | 'title' | 'filename' | '_id' | 'url' | 'importDate'>;
|
||||
export type PlaylistMeta = Pick<
|
||||
Playlist,
|
||||
'count' | 'title' | 'filename' | '_id' | 'url' | 'importDate' | 'userAgent'
|
||||
>;
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
|
||||
@@ -14,4 +14,5 @@ export interface Playlist {
|
||||
favorites: string[];
|
||||
count: number;
|
||||
url?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
@@ -1,34 +1,60 @@
|
||||
<h2 mat-dialog-title>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.PLAYLIST_DETAILS' | translate }}
|
||||
</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.TITLE' | translate
|
||||
}}</mat-label>
|
||||
<input matInput [value]="playlist.title" disabled="true" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.url">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.FROM_URL' | translate
|
||||
}}</mat-label>
|
||||
<input matInput [value]="playlist.url" disabled="true" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.IMPORT_DATE' | translate
|
||||
}}</mat-label>
|
||||
<input matInput [value]="playlist.importDate | date" disabled="true" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.CHANNELS' | translate
|
||||
}}</mat-label>
|
||||
<input matInput [value]="playlist.count" disabled="true" />
|
||||
</mat-form-field>
|
||||
</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>
|
||||
<form
|
||||
[formGroup]="playlistDetails"
|
||||
(ngSubmit)="saveChanges(playlistDetails.value)"
|
||||
>
|
||||
<h2 mat-dialog-title>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.PLAYLIST_DETAILS' | translate }}
|
||||
</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.TITLE' | translate
|
||||
}}</mat-label>
|
||||
<input formControlName="title" matInput />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.url">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.FROM_URL' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="url" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.ORIGINAL_FILENAME' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="filename" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.IMPORT_DATE' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="importDate" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.CHANNELS' | translate
|
||||
}}</mat-label>
|
||||
<input matInput formControlName="count" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width">
|
||||
<mat-label>{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.USER_AGENT' | translate
|
||||
}}</mat-label>
|
||||
<input formControlName="userAgent" matInput />
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button
|
||||
mat-flat-button
|
||||
mat-dialog-close
|
||||
color="accent"
|
||||
type="submit"
|
||||
[disabled]="!playlistDetails.valid || playlistDetails.pristine"
|
||||
>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.SAVE' | translate }}
|
||||
</button>
|
||||
<button mat-button mat-dialog-close cdkFocusInitial color="accent">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
@@ -1,24 +1,40 @@
|
||||
import { PLAYLIST_SAVE_DETAILS } from './../../../../../ipc-commands';
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { ElectronServiceStub } from './../../home.component.spec';
|
||||
import { ElectronService } from './../../../services/electron.service';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { PlaylistInfoComponent } from './playlist-info.component';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
||||
|
||||
describe('PlaylistInfoComponent', () => {
|
||||
let component: PlaylistInfoComponent;
|
||||
let fixture: ComponentFixture<PlaylistInfoComponent>;
|
||||
let electronService: ElectronService;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
MockModule(MatDialogModule),
|
||||
MockModule(MatFormFieldModule),
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
declarations: [
|
||||
PlaylistInfoComponent,
|
||||
MockPipe(TranslatePipe),
|
||||
MockPipe(DatePipe),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
FormBuilder,
|
||||
],
|
||||
declarations: [PlaylistInfoComponent, MockPipe(TranslatePipe)],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: {} }],
|
||||
}).compileComponents();
|
||||
})
|
||||
);
|
||||
@@ -26,10 +42,22 @@ describe('PlaylistInfoComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PlaylistInfoComponent);
|
||||
component = fixture.componentInstance;
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should send an event to the main process after save', () => {
|
||||
const playlistToSave = { _id: 'a12345', title: 'Playlist' };
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
component.saveChanges(playlistToSave);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
playlistToSave
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,75 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { PLAYLIST_SAVE_DETAILS } from '../../../../../ipc-commands';
|
||||
import { ElectronService } from '../../../services/electron.service';
|
||||
import { Playlist } from '../../playlist.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-info',
|
||||
templateUrl: './playlist-info.component.html',
|
||||
styleUrls: ['./playlist-info.component.scss'],
|
||||
providers: [DatePipe],
|
||||
})
|
||||
export class PlaylistInfoComponent {
|
||||
/** Playlist object */
|
||||
playlist: Playlist;
|
||||
|
||||
/** Form group with playlist details */
|
||||
playlistDetails: FormGroup;
|
||||
|
||||
/**
|
||||
* Creates an instance of the component and injects the selected playlist from the parent component
|
||||
* @param data playlist object to show
|
||||
* @param datePipe
|
||||
* @param formBuilder
|
||||
* @param electronService
|
||||
* @param playlist playlist object to show
|
||||
*/
|
||||
constructor(@Inject(MAT_DIALOG_DATA) playlist: Playlist) {
|
||||
constructor(
|
||||
private datePipe: DatePipe,
|
||||
private formBuilder: FormBuilder,
|
||||
private electronService: ElectronService,
|
||||
@Inject(MAT_DIALOG_DATA) playlist: Playlist
|
||||
) {
|
||||
this.playlist = playlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the form and set initial data on component init
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.playlistDetails = this.formBuilder.group({
|
||||
_id: this.playlist._id,
|
||||
title: new FormControl(this.playlist.title, Validators.required),
|
||||
userAgent: this.playlist.userAgent || '',
|
||||
filename: new FormControl({
|
||||
value: this.playlist.filename || '',
|
||||
disabled: true,
|
||||
}),
|
||||
count: new FormControl({
|
||||
value: this.playlist.count,
|
||||
disabled: true,
|
||||
}),
|
||||
importDate: new FormControl({
|
||||
value: this.datePipe.transform(this.playlist.importDate),
|
||||
disabled: true,
|
||||
}),
|
||||
url: new FormControl({ value: this.playlist.url, disabled: true }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves updated playlist information
|
||||
* @param data updated form data
|
||||
*/
|
||||
saveChanges(data: Pick<Playlist, '_id' | 'title' | 'userAgent'>): void {
|
||||
this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data);
|
||||
}
|
||||
}
|
||||
@@ -35,15 +35,15 @@
|
||||
</div>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); openInfoDialog(item)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.SHOW_DETAILS' | translate"
|
||||
>
|
||||
<mat-icon>info</mat-icon>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); removeClicked.emit(item._id)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.REMOVE' | translate"
|
||||
>
|
||||
|
||||
+24
-5
@@ -1,6 +1,6 @@
|
||||
<mat-tab-group>
|
||||
<mat-tab [label]="'CHANNELS.ALL_CHANNELS' | translate">
|
||||
<mat-list id="top-panel">
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
<input
|
||||
class="full-width search-bar"
|
||||
@@ -21,6 +21,7 @@
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-nav-list>
|
||||
<cdk-virtual-scroll-viewport itemSize="50" class="scroll-viewport">
|
||||
<mat-list-item
|
||||
@@ -32,7 +33,13 @@
|
||||
[class.active]="selected?.id === channel.id"
|
||||
(click)="selectChannel(channel)"
|
||||
>
|
||||
<p matLine>
|
||||
<img
|
||||
[src]="channel.tvg.logo"
|
||||
*ngIf="channel.tvg.logo"
|
||||
width="48"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
<p matLine class="channel-name">
|
||||
{{
|
||||
i + 1 + '. ' + channel?.name ||
|
||||
('CHANNELS.UNNAMED_CHANNEL' | translate)
|
||||
@@ -64,7 +71,13 @@
|
||||
[class.active]="selected?.id === channel.id"
|
||||
(click)="selectChannel(channel)"
|
||||
>
|
||||
<p matLine>
|
||||
<img
|
||||
[src]="channel.tvg.logo"
|
||||
*ngIf="channel.tvg.logo"
|
||||
width="48"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
<p matLine class="channel-name">
|
||||
{{
|
||||
i + 1 + '. ' + channel?.name ||
|
||||
('CHANNELS.UNNAMED_CHANNEL'
|
||||
@@ -94,7 +107,13 @@
|
||||
[class.active]="selected?.id === channel.id"
|
||||
(click)="selectChannel(channel)"
|
||||
>
|
||||
<p matLine>
|
||||
<img
|
||||
[src]="channel.tvg.logo"
|
||||
*ngIf="channel.tvg.logo"
|
||||
width="48"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
<p matLine class="channel-name">
|
||||
{{
|
||||
i + 1 + '. ' + channel?.name ||
|
||||
('CHANNELS.UNNAMED_CHANNEL' | translate)
|
||||
@@ -106,7 +125,7 @@
|
||||
[matTooltip]="'CHANNELS.REMOVE_FAVORITE' | translate"
|
||||
(click)="toggleFavoriteChannel(channel, $event)"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
<mat-icon color="accent">star</mat-icon>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
</mat-list-item>
|
||||
|
||||
+36
-9
@@ -1,11 +1,13 @@
|
||||
#top-panel {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.dark-theme .active {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-list-base {
|
||||
padding-top: 0;
|
||||
}
|
||||
@@ -20,6 +22,20 @@
|
||||
|
||||
.search-bar {
|
||||
padding: 8px;
|
||||
background: #ddd;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::ng-deep .dark-theme {
|
||||
.search-bar {
|
||||
background: #666;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-bar::placeholder {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
@@ -30,9 +46,20 @@
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.mat-tab-label, .mat-tab-label-active {
|
||||
min-width: 0!important;
|
||||
padding: 15px!important;
|
||||
margin: 5px!important;
|
||||
.mat-tab-label,
|
||||
.mat-tab-label-active {
|
||||
min-width: 0 !important;
|
||||
padding: 15px !important;
|
||||
margin: 5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
font-size: 0.9em;
|
||||
width: 290px;
|
||||
}
|
||||
+40
@@ -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="accent">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
.subheading-2 {
|
||||
color: #666;
|
||||
text-decoration: underline;
|
||||
text-transform: uppercase;
|
||||
font-weight: 900;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.dark-theme .subheading-2 {
|
||||
color: #bdbdbd;
|
||||
}
|
||||
}
|
||||
+59
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
+20
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngIf="channel; else noEpg">
|
||||
<!-- channel info panel -->
|
||||
<div fxFlex="120px" id="channel-info" fxLayout="column">
|
||||
<div fxFlex="120px" id="channel-info" color="primary" fxLayout="column">
|
||||
<div fxFlex="79px" fxLayout="row" fxLayoutGap="10px">
|
||||
<div
|
||||
fxFlex="60px"
|
||||
@@ -22,6 +22,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div fxFlex="40px" fxLayout="row" id="date-navigator">
|
||||
<button
|
||||
fxFlex="40px"
|
||||
@@ -48,6 +49,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- program list -->
|
||||
<mat-selection-list
|
||||
@@ -56,51 +58,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,19 +13,7 @@
|
||||
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);
|
||||
background-color: #ccc;
|
||||
|
||||
.channel-icon {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -48,28 +36,8 @@ 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;
|
||||
|
||||
.selected-date {
|
||||
font-size: 0.9em;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: '',
|
||||
|
||||
@@ -3,8 +3,8 @@ export interface EpgProgram {
|
||||
stop: string;
|
||||
channel: string;
|
||||
title: { lang: string; value: string }[];
|
||||
desc: any[];
|
||||
category: any[];
|
||||
desc: { lang: string; value: string }[];
|
||||
category: { lang: string; value: string }[];
|
||||
date: any[];
|
||||
episodeNum: any[];
|
||||
previouslyShown: any[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Theme } from './../settings/theme.enum';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SettingsService {
|
||||
/**
|
||||
* Changes the visual theme of the application
|
||||
* @param selectedTheme theme to set
|
||||
*/
|
||||
changeTheme(selectedTheme: Theme): void {
|
||||
if (selectedTheme === Theme.LightTheme) {
|
||||
document.body.classList.remove('dark-theme');
|
||||
} else if (selectedTheme === Theme.DarkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { WhatsNewService } from './whats-new.service';
|
||||
|
||||
describe('Service: WhatsNew', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [WhatsNewService],
|
||||
});
|
||||
});
|
||||
|
||||
it('should ...', inject([WhatsNewService], (service: WhatsNewService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should change the state', inject(
|
||||
[WhatsNewService],
|
||||
(service: WhatsNewService) => {
|
||||
spyOn(service.dialogState$, 'next');
|
||||
service.changeDialogVisibleState(true);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledTimes(1);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledWith(true);
|
||||
service.changeDialogVisibleState(false);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledTimes(2);
|
||||
expect(service.dialogState$.next).toHaveBeenCalledWith(false);
|
||||
}
|
||||
));
|
||||
|
||||
it('should return modal windows for the requested version ', inject(
|
||||
[WhatsNewService],
|
||||
(service: WhatsNewService) => {
|
||||
const version = '0.6.0';
|
||||
const modals = service.getModalsByVersion(version);
|
||||
expect(modals).toBeDefined();
|
||||
expect(modals).toHaveLength(service.modals[version].length);
|
||||
}
|
||||
));
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class WhatsNewService {
|
||||
/** Visibility state of the modal */
|
||||
dialogState$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/** Array with all available modals */
|
||||
modals = {
|
||||
'0.6.0': [
|
||||
{
|
||||
imageHeight: 470,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/060/dark-theme.png',
|
||||
title: 'New in v0.6.0 - Dark theme 🎉',
|
||||
text:
|
||||
'Finally, the dark theme is now available. Check the settings page of the application to change the theme.',
|
||||
button: {
|
||||
text: 'OKAY',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 470,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/060/channel-logos.png',
|
||||
title: 'Channel logos 📺',
|
||||
text:
|
||||
'The channel list was extended with the visualization of logotypes (only visible if they are defined in the playlist)',
|
||||
button: {
|
||||
text: 'OKAY',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
{
|
||||
imageHeight: 470,
|
||||
imageBgColor: '#333',
|
||||
imageSrc: './assets/updates/060/custom-user-agent.png',
|
||||
title: 'Custom user agent 🕵️♂️',
|
||||
text:
|
||||
'Some IPTV providers need a specific user-agent in order to play their playlist. Support of custom user agent is available now.',
|
||||
button: {
|
||||
text: 'GOT IT',
|
||||
textColor: '#ccc',
|
||||
bgColor: '#111',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** Options for the "what is new" modal dialogs */
|
||||
options = {
|
||||
width: '500px',
|
||||
customStyle: {
|
||||
boxShadow: '0px 0px 10px 5px #111',
|
||||
backgroundColor: '#333',
|
||||
textColor: '#eee',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the visibility state of the modal dialog
|
||||
* @param value flag to set
|
||||
*/
|
||||
changeDialogVisibleState(value: boolean): void {
|
||||
this.dialogState$.next(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with modals for the provided version of the application
|
||||
* @param version version string
|
||||
*/
|
||||
getModalsByVersion(version: string): ModalWindow[] {
|
||||
return this.modals[version] || [];
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,30 @@
|
||||
</p>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<div matLine fxLayout="row">
|
||||
<p fxFlex>{{ 'SETTINGS.THEME' | translate }}</p>
|
||||
<p fxFlex>
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{
|
||||
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER'
|
||||
| translate
|
||||
}}</mat-label>
|
||||
<mat-select formControlName="theme">
|
||||
<mat-option
|
||||
*ngFor="
|
||||
let theme of themeEnum | keyvalue
|
||||
"
|
||||
[value]="theme.value"
|
||||
>{{
|
||||
'THEMES.' + theme.value | translate
|
||||
}}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<div matLine fxLayout="row">
|
||||
<p fxFlex>{{ 'SETTINGS.VERSION' | translate }}</p>
|
||||
@@ -109,7 +133,7 @@
|
||||
<mat-card-actions>
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
color="accent"
|
||||
type="submit"
|
||||
[disabled]="settingsForm.pristine"
|
||||
>
|
||||
@@ -119,7 +143,7 @@
|
||||
<div fxFlex></div>
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
color="accent"
|
||||
type="reset"
|
||||
(click)="backToHome()"
|
||||
>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { ElectronService } from './../services/electron.service';
|
||||
import { ElectronServiceStub } from './../home/home.component.spec';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
@@ -14,14 +19,25 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { EPG_FETCH } from '../../../ipc-commands';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
}
|
||||
|
||||
class MockRouter {
|
||||
navigateByUrl(url: string) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
let electronService: ElectronService;
|
||||
let storage: StorageMap;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
@@ -32,11 +48,18 @@ describe('SettingsComponent', () => {
|
||||
MockPipe(TranslatePipe),
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
{
|
||||
provide: TranslateService,
|
||||
useClass: TranslateServiceStub,
|
||||
},
|
||||
{ provide: ElectronService, useClass: ElectronServiceStub },
|
||||
{
|
||||
provide: Router,
|
||||
useClass: MockRouter,
|
||||
},
|
||||
StorageMap,
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
@@ -56,10 +79,29 @@ describe('SettingsComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
electronService = TestBed.inject(ElectronService);
|
||||
storage = TestBed.inject(StorageMap);
|
||||
router = TestBed.inject(Router);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create and init component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should send epg fetch command', () => {
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
component.fetchEpg();
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledWith(
|
||||
EPG_FETCH,
|
||||
{ url: '' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should navigate back to home page', () => {
|
||||
spyOn(router, 'navigateByUrl');
|
||||
component.backToHome();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,8 @@ import { ElectronService } from '../services/electron.service';
|
||||
import { ChannelQuery } from '../state';
|
||||
import { EPG_FETCH } from '../../../ipc-commands';
|
||||
import { Language } from './language.enum';
|
||||
import { Theme } from './theme.enum';
|
||||
import { SettingsService } from './../services/settings.service';
|
||||
|
||||
/** Url of the package.json file in the app repository, required to get the version of the released app */
|
||||
const PACKAGE_JSON_URL =
|
||||
@@ -55,6 +57,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
(store) => store.epgAvailable
|
||||
);
|
||||
|
||||
/** All available visual themes */
|
||||
themeEnum = Theme;
|
||||
|
||||
/**
|
||||
* Creates an instance of SettingsComponent and injects some dependencies into the component
|
||||
* @param channelQuery
|
||||
@@ -62,8 +67,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
* @param formBuilder
|
||||
* @param http
|
||||
* @param router
|
||||
* @param settingsService
|
||||
* @param snackBar
|
||||
* @param storage
|
||||
* @param translate
|
||||
*/
|
||||
constructor(
|
||||
private channelQuery: ChannelQuery,
|
||||
@@ -71,6 +78,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
private formBuilder: FormBuilder,
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private settingsService: SettingsService,
|
||||
private snackBar: MatSnackBar,
|
||||
private storage: StorageMap,
|
||||
private translate: TranslateService
|
||||
@@ -79,6 +87,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
player: ['videojs'],
|
||||
epgUrl: '',
|
||||
language: Language.ENGLISH,
|
||||
theme: Theme.LightTheme,
|
||||
});
|
||||
|
||||
this.subscription.add(
|
||||
@@ -119,6 +128,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
language: settings.language
|
||||
? settings.language
|
||||
: Language.ENGLISH,
|
||||
theme: settings.theme
|
||||
? settings.theme
|
||||
: Theme.LightTheme,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -139,6 +151,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.fetchEpg();
|
||||
}
|
||||
this.translate.use(this.settingsForm.value.language);
|
||||
this.settingsService.changeTheme(
|
||||
this.settingsForm.value.theme
|
||||
);
|
||||
this.snackBar.open(
|
||||
this.translate.instant('SETTINGS.SETTINGS_SAVED'),
|
||||
null,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Language } from './language.enum';
|
||||
import { Theme } from './theme.enum';
|
||||
|
||||
export type VideoPlayerType = 'html5' | 'videojs';
|
||||
|
||||
@@ -9,4 +10,5 @@ export interface Settings {
|
||||
player: VideoPlayerType;
|
||||
epgUrl: string;
|
||||
language: Language;
|
||||
theme: Theme;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum Theme {
|
||||
DarkTheme = 'DARK_THEME',
|
||||
LightTheme = 'LIGHT_THEME',
|
||||
}
|
||||
+30
-13
@@ -8,7 +8,7 @@
|
||||
"SEARCH_CHANNEL": "Nach Sender suchen",
|
||||
"UNGROUPED": "Ohne Gruppe",
|
||||
"UNNAMED_CHANNEL": "Sender ohne Namen",
|
||||
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Neue Playliste hochladen oder eine andere aussuchen",
|
||||
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Neue Playlist hochladen oder eine andere aussuchen",
|
||||
"USE_STAR_TO_FAVORITE": "Nutzen Sie das Star-Icon um Sender als Favoriten zu markieren"
|
||||
},
|
||||
"EPG": {
|
||||
@@ -20,32 +20,44 @@
|
||||
"NEXT_DAY": "Zum nächsten Tag",
|
||||
"PREVIOUS_DAY": "Zum vorherigen Tag",
|
||||
"TIMESHIFT_AVAILABLE": "Die Timeshift-Funktion (Archiv) ist verfügbar, klicken Sie an um abzuspielen",
|
||||
"FETCH_EPG": "EPG-Daten werden geladen..."
|
||||
"FETCH_EPG": "EPG-Daten werden geladen...",
|
||||
"PROGRAM_DIALOG": {
|
||||
"PROGRAM_DETAILS": "Details zum TV-Programm",
|
||||
"TITLE": "Titel",
|
||||
"CATEGORY": "Ketegorie",
|
||||
"PARENTAL_RATING_SYSTEM": "Alterskennzeichnung (USK)",
|
||||
"DESCRIPTION": "Beschreibung",
|
||||
"LANGUAGE": "Sprache",
|
||||
"SHOW_PROGRAM_DETAILS": "Details anzeigen"
|
||||
}
|
||||
},
|
||||
"HOME": {
|
||||
"SUBTITLE": "Wählen Sie eine Playliste aus oder laden Sie eine neue Playliste hoch",
|
||||
"SUBTITLE": "Wählen Sie eine Playlist aus oder laden Sie eine neue Playlist hoch",
|
||||
"TITLE": "IPTVnator",
|
||||
"FILE_UPLOAD": {
|
||||
"CHOOSE_PLAYLIST": "Playliste auswählen",
|
||||
"CHOOSE_PLAYLIST": "Playlist auswählen",
|
||||
"DRAG_DROP": "Ziehen Sie Dateien per Drag & Drop",
|
||||
"OR": "oder"
|
||||
},
|
||||
"PLAYLISTS": {
|
||||
"ADDED": "Hinzugefügt am",
|
||||
"ADDED_VIA_FILE": "Diese Playliste wurde aus dem Dateisystem hochgeladen",
|
||||
"ADDED_VIA_URL": "Diese Playliste wurde von der URL hinzugefügt",
|
||||
"ADD_FIRST": "Bitte fügen Sie eine neue Playliste hinzu",
|
||||
"ADDED_VIA_FILE": "Diese Playlist wurde aus dem Dateisystem hochgeladen",
|
||||
"ADDED_VIA_URL": "Diese Playlist wurde von der URL hinzugefügt",
|
||||
"ADD_FIRST": "Bitte fügen Sie eine neue Playlist hinzu",
|
||||
"CHANNELS": "TV-Sender",
|
||||
"NO_PLAYLISTS": "Noch keine Playlisten hinzugefügt",
|
||||
"REMOVE": "Playliste entfernen",
|
||||
"REMOVE": "Playlist entfernen",
|
||||
"SHOW_DETAILS": "Details anzeigen",
|
||||
"INFO_DIALOG": {
|
||||
"CHANNELS": "Anzahl von Sender",
|
||||
"CLOSE": "Schließen",
|
||||
"FROM_URL": "Von der URL hinzugefügt",
|
||||
"IMPORT_DATE": "Hinzugefügt am",
|
||||
"PLAYLIST_DETAILS": "Informationen über die Playliste",
|
||||
"TITLE": "Titel"
|
||||
"PLAYLIST_DETAILS": "Infos über die Playlist",
|
||||
"TITLE": "Titel",
|
||||
"USER_AGENT": "User agent",
|
||||
"SAVE": "Speichern",
|
||||
"ORIGINAL_FILENAME": "Original-Dateinamen"
|
||||
}
|
||||
},
|
||||
"TABS": {
|
||||
@@ -54,8 +66,8 @@
|
||||
"URL_UPLOAD": "Via URL hinzufügen"
|
||||
},
|
||||
"URL_UPLOAD": {
|
||||
"ADD_PLAYLIST": "Playliste hinzufügen",
|
||||
"PLAYLIST_URL": "URL der Playliste"
|
||||
"ADD_PLAYLIST": "Playlist hinzufügen",
|
||||
"PLAYLIST_URL": "URL der Playlist"
|
||||
}
|
||||
},
|
||||
"LANGUAGES": {
|
||||
@@ -78,7 +90,12 @@
|
||||
"VERSION": "Version",
|
||||
"VIDEO_PLAYER_LABEL": "Video player",
|
||||
"VIDEO_PLAYER_PLACEHOLDER": "Option auswählen",
|
||||
"SETTINGS_SAVED": "Einstellungen wurden erfolgreich gespeichert!"
|
||||
"SETTINGS_SAVED": "Einstellungen wurden erfolgreich gespeichert!",
|
||||
"THEME": "Farbdesign"
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Dunkel",
|
||||
"LIGHT_THEME": "Hell"
|
||||
},
|
||||
"TOP_MENU": {
|
||||
"OPEN_CHANNELS_LIST": "Senderliste öffnen",
|
||||
|
||||
+20
-3
@@ -22,7 +22,10 @@
|
||||
"FROM_URL": "Added from the URL",
|
||||
"IMPORT_DATE": "Import date",
|
||||
"CHANNELS": "Number of channels",
|
||||
"CLOSE": "Close"
|
||||
"CLOSE": "Close",
|
||||
"SAVE": "Save",
|
||||
"USER_AGENT": "User agent",
|
||||
"ORIGINAL_FILENAME": "Original filename"
|
||||
}
|
||||
},
|
||||
"FILE_UPLOAD": {
|
||||
@@ -50,7 +53,12 @@
|
||||
"NEW_VERSION_AVAILABLE": "There is a new version available",
|
||||
"LATEST_VERSION": "You are using the latest version",
|
||||
"LANGUAGE": "Language",
|
||||
"SETTINGS_SAVED": "Success! Configuration was saved."
|
||||
"SETTINGS_SAVED": "Success! Configuration was saved.",
|
||||
"THEME": "Visual theme"
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Dark theme",
|
||||
"LIGHT_THEME": "Light theme"
|
||||
},
|
||||
"CHANNELS": {
|
||||
"SEARCH_CHANNEL": "Search channel",
|
||||
@@ -78,7 +86,16 @@
|
||||
"EPG_NOT_AVAILABLE_DATE": "Ooops, EPG is not available for the selected date",
|
||||
"EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ooops, EPG is not available for this channel.",
|
||||
"EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Please add/change the url of EPG list the settings of the app.",
|
||||
"FETCH_EPG": "Fetch EPG data..."
|
||||
"FETCH_EPG": "Fetch EPG data...",
|
||||
"PROGRAM_DIALOG": {
|
||||
"PROGRAM_DETAILS": "Details about the TV Program",
|
||||
"TITLE": "Title",
|
||||
"CATEGORY": "Category",
|
||||
"PARENTAL_RATING_SYSTEM": "Television content rating system",
|
||||
"DESCRIPTION": "Description",
|
||||
"LANGUAGE": "Language",
|
||||
"SHOW_PROGRAM_DETAILS": "Show details about this program"
|
||||
}
|
||||
},
|
||||
"LANGUAGES": {
|
||||
"ENGLISH": "English",
|
||||
|
||||
+21
-4
@@ -22,7 +22,10 @@
|
||||
"FROM_URL": "Добавлен по ссылке",
|
||||
"IMPORT_DATE": "Дата импорта",
|
||||
"CHANNELS": "Количество каналов",
|
||||
"CLOSE": "Закрыть"
|
||||
"CLOSE": "Закрыть",
|
||||
"SAVE": "Сохранить",
|
||||
"USER_AGENT": "User agent",
|
||||
"ORIGINAL_FILENAME": "Имя файла при загрузке"
|
||||
}
|
||||
},
|
||||
"FILE_UPLOAD": {
|
||||
@@ -50,11 +53,16 @@
|
||||
"NEW_VERSION_AVAILABLE": "Доступна новая версия приложения",
|
||||
"LATEST_VERSION": "Вы используете последнюю версию приложения",
|
||||
"LANGUAGE": "Язык",
|
||||
"SETTINGS_SAVED": "Новые настройки были успешно сохранены."
|
||||
"SETTINGS_SAVED": "Новые настройки были успешно сохранены.",
|
||||
"THEME": "Тема оформления"
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Тёмная тема",
|
||||
"LIGHT_THEME": "Светлая тема"
|
||||
},
|
||||
"CHANNELS": {
|
||||
"SEARCH_CHANNEL": "Поиск каналов",
|
||||
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Загрузить илди выбрать другой плейлист",
|
||||
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Загрузить или выбрать другой плейлист",
|
||||
"ALL_CHANNELS": "Все каналы",
|
||||
"GROUPS": "Группы",
|
||||
"UNGROUPED": "Без групп",
|
||||
@@ -78,7 +86,16 @@
|
||||
"EPG_NOT_AVAILABLE_DATE": "Ой, к сожалению, программа отсутствует на этот день.",
|
||||
"EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ой, к сожалению, программа не доступна для этого канала.",
|
||||
"EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Попробуйте изменить или обновить ссылку на EPG в настройках приложения.",
|
||||
"FETCH_EPG": "Загружаю программу тв-передач (EPG)..."
|
||||
"FETCH_EPG": "Загружаю программу тв-передач (EPG)...",
|
||||
"PROGRAM_DIALOG": {
|
||||
"PROGRAM_DETAILS": "Подробности передачи",
|
||||
"TITLE": "Название",
|
||||
"CATEGORY": "Категория",
|
||||
"PARENTAL_RATING_SYSTEM": "Возрастное ограничение",
|
||||
"DESCRIPTION": "Описание",
|
||||
"LANGUAGE": "Язык",
|
||||
"SHOW_PROGRAM_DETAILS": "Показать подробности"
|
||||
}
|
||||
},
|
||||
"LANGUAGES": {
|
||||
"ENGLISH": "English",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Vendored
+1
@@ -0,0 +1 @@
|
||||
import 'jest-extended';
|
||||
+11
-11
@@ -1,14 +1,14 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>IPTVnator</title>
|
||||
<base href="/">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>IPTVnator</title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico" />
|
||||
</head>
|
||||
<body class="mat-app-background">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
+69
-53
@@ -1,82 +1,98 @@
|
||||
$material-icons-font-path: '~material-icons/iconfont/';
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
|
||||
$material-icons-font-path: '~material-icons/iconfont/';
|
||||
|
||||
@import '~material-icons/iconfont/material-icons.scss';
|
||||
@import '~@angular/material/theming';
|
||||
@include mat-core();
|
||||
@import 'themes/';
|
||||
|
||||
body,
|
||||
html {
|
||||
background: #fff;
|
||||
font-family: caption;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mat-toolbar.mat-primary {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* CAN (MUST) BE REMOVED ! Sample Global style */
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 50px 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff !important;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
background: #ed3330;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
transition: all 0.4s ease 0s;
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
color: #ed3330 !important;
|
||||
letter-spacing: 1px;
|
||||
-webkit-box-shadow: 0px 5px 40px -10px rgba(0,0,0,0.57);
|
||||
-moz-box-shadow: 0px 5px 40px -10px rgba(0,0,0,0.57);
|
||||
box-shadow: 5px 40px -10px rgba(0,0,0,0.57);
|
||||
transition: all 0.4s ease 0s;
|
||||
.title {
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 50px 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff !important;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
background: #ed3330;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
transition: all 0.4s ease 0s;
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
color: #ed3330 !important;
|
||||
letter-spacing: 1px;
|
||||
-webkit-box-shadow: 0px 5px 40px -10px rgba(0, 0, 0, 0.57);
|
||||
-moz-box-shadow: 0px 5px 40px -10px rgba(0, 0, 0, 0.57);
|
||||
box-shadow: 5px 40px -10px rgba(0, 0, 0, 0.57);
|
||||
transition: all 0.4s ease 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-toolbar-row, .mat-toolbar-single-row {
|
||||
height: 54px;
|
||||
.mat-toolbar-row,
|
||||
.mat-toolbar-single-row {
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
:not(input):not(textarea),
|
||||
:not(input):not(textarea)::after,
|
||||
:not(input):not(textarea)::before {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
input, button, textarea, :focus {
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-theme .mat-expansion-panel,
|
||||
.dark-theme .mat-drawer,
|
||||
.dark-theme .mat-app-background,
|
||||
.dark-theme.mat-app-background {
|
||||
background-color: #1d1d1d;
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
::-webkit-scrollbar {
|
||||
overflow-y: scroll;
|
||||
background: rgb(100, 100, 100);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(60, 60, 60);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
$black: (
|
||||
50: #000000,
|
||||
100: #000000,
|
||||
200: #000000,
|
||||
300: #000000,
|
||||
400: #000000,
|
||||
500: #000000,
|
||||
600: #000000,
|
||||
700: #000000,
|
||||
800: #000000,
|
||||
900: #000000,
|
||||
A100: #000000,
|
||||
A200: #000000,
|
||||
A400: #000000,
|
||||
A700: #000000,
|
||||
contrast: (
|
||||
50: $light-primary-text,
|
||||
100: $light-primary-text,
|
||||
200: $light-primary-text,
|
||||
300: $light-primary-text,
|
||||
400: $light-primary-text,
|
||||
500: $light-primary-text,
|
||||
600: $light-primary-text,
|
||||
700: $light-primary-text,
|
||||
800: $light-primary-text,
|
||||
900: $light-primary-text,
|
||||
A100: $light-primary-text,
|
||||
A200: $light-primary-text,
|
||||
A400: $light-primary-text,
|
||||
A700: $light-primary-text,
|
||||
),
|
||||
);
|
||||
|
||||
$angular-primary: mat-palette($black);
|
||||
$angular-accent: mat-palette($mat-indigo, 500, 600, 700);
|
||||
$angular-warn: mat-palette($mat-red);
|
||||
$angular-default-theme: mat-light-theme(
|
||||
(
|
||||
color: (
|
||||
primary: $angular-primary,
|
||||
accent: $angular-accent,
|
||||
warn: $angular-warn,
|
||||
),
|
||||
)
|
||||
);
|
||||
@include angular-material-theme($angular-default-theme);
|
||||
|
||||
@import 'dark';
|
||||
@@ -0,0 +1,16 @@
|
||||
$dark-primary: mat-palette($black, A200, A800, A500);
|
||||
$dark-accent: mat-palette($mat-teal, A400, A100, A400);
|
||||
$dark-warn: mat-palette($mat-red);
|
||||
$dark-theme: mat-dark-theme(
|
||||
(
|
||||
color: (
|
||||
primary: $dark-primary,
|
||||
accent: $dark-accent,
|
||||
warn: $dark-warn,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
.dark-theme {
|
||||
@include angular-material-color($dark-theme);
|
||||
}
|
||||
Reference in new issue
Block a user