refactor: improve structure of home screen

This commit is contained in:
4gray committed 2020-10-06 21:37:18 +02:00
1 parent f778e1d9d8
commit 90b4a54e27
23 files changed
+479 -328

No files matched your search

+3 -3
View File
@@ -2,7 +2,7 @@ import { app, ipcMain } from 'electron';
import { parse } from 'iptv-playlist-parser';
import axios from 'axios';
import { guid } from '@datorama/akita';
import { Playlist } from './src/app/playlist-uploader/playlist.interface';
import { Playlist } from './src/app/home/playlist.interface';
import Nedb from 'nedb-promises-ts';
const fs = require('fs');
@@ -26,7 +26,7 @@ export class Api {
args.url
);
this.insertToDb(playlistObject);
event.sender.send('parse-url-response', {
event.sender.send('parse-response', {
payload: playlistObject,
});
});
@@ -54,7 +54,7 @@ export class Api {
ipcMain.on('playlist-by-id', async (event, args) => {
const playlist = await db.findOne({ _id: args.id });
event.sender.send('playlist-by-id-result', {
event.sender.send('parse-response', {
payload: playlist,
});
});
+2 -2
View File
@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './shared/components';
import { PlaylistUploaderComponent } from './playlist-uploader/playlist-uploader.component';
import { HomeComponent } from './home/home.component';
import { VideoPlayerComponent } from './video-player/video-player.component';
const routes: Routes = [
{
path: '',
component: PlaylistUploaderComponent,
component: HomeComponent,
},
{
path: 'iptv',
+8 -4
View File
@@ -14,14 +14,16 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AppComponent } from './app.component';
import { CommonModule } from '@angular/common';
import { PlaylistUploaderComponent } from './playlist-uploader/playlist-uploader.component';
import { HomeComponent } from './home/home.component';
import { NgxUploaderModule } from 'ngx-uploader';
import { VideoPlayerComponent } from './video-player/video-player.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RecentPlaylistsComponent } from './recent-playlists/recent-playlists.component';
import { RecentPlaylistsComponent } from './home/recent-playlists/recent-playlists.component';
import { ChannelListContainerComponent } from './channel-list-container/channel-list-container.component';
import { FilterPipeModule } from 'ngx-filter-pipe';
import { VjsPlayerComponent } from './vjs-player/vjs-player.component';
import { UrlUploadComponent } from './home/url-upload/url-upload.component';
import { FileUploadComponent } from './home/file-upload/file-upload.component';
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
@@ -32,10 +34,12 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
declarations: [
AppComponent,
ChannelListContainerComponent,
PlaylistUploaderComponent,
VideoPlayerComponent,
FileUploadComponent,
HomeComponent,
RecentPlaylistsComponent,
VideoPlayerComponent,
VjsPlayerComponent,
UrlUploadComponent,
],
imports: [
CommonModule,
@@ -0,0 +1,23 @@
<div
class="drop-container"
ngFileDrop
[options]="options"
(uploadOutput)="onUploadOutput($event)"
[uploadInput]="uploadInput"
>
<mat-icon class="upload-icon">save_alt</mat-icon>
<p class="upload-title">Drag & drop files here</p>
<p>or</p>
<input
type="file"
ngFileSelect
[options]="options"
(uploadOutput)="onUploadOutput($event)"
class="input-file"
name="file"
id="file"
[uploadInput]="uploadInput"
multiple
/>
<label for="file"> Choose a playlist </label>
</div>
@@ -1,18 +1,4 @@
::ng-deep {
.mat-tab-label-content {
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;
}
}
.drop-container {
margin: 10px;
padding: 10px;
@@ -23,11 +9,6 @@
color: #666;
}
.url-form {
margin: 15px;
height: 304px;
}
#file input {
border-radius: 4px;
border: 1px solid #ccc;
@@ -3,21 +3,21 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { PlaylistUploaderComponent } from './playlist-uploader.component';
import { FileUploadComponent } from './file-upload.component';
describe('PlaylistUploaderComponent', () => {
let component: PlaylistUploaderComponent;
let fixture: ComponentFixture<PlaylistUploaderComponent>;
describe('FileUploadComponent', () => {
let component: FileUploadComponent;
let fixture: ComponentFixture<FileUploadComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PlaylistUploaderComponent ]
declarations: [ FileUploadComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PlaylistUploaderComponent);
fixture = TestBed.createComponent(FileUploadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -0,0 +1,90 @@
import { Component, EventEmitter, Output } from '@angular/core';
import {
UploaderOptions,
UploadFile,
UploadOutput,
UploadInput,
} from 'ngx-uploader';
@Component({
selector: 'app-file-upload',
templateUrl: './file-upload.component.html',
styleUrls: ['./file-upload.component.scss'],
})
export class FileUploadComponent {
/** Array with uploaded files */
files: UploadFile[] = [];
/** Upload emitter */
uploadInput: EventEmitter<UploadInput> = new EventEmitter<UploadInput>();
/** Drag over flag */
dragOver: boolean;
/** ngx-uploader lib options */
options: UploaderOptions = {
allowedContentTypes: [
'application/x-mpegurl',
'application/octet-stream',
'application/mpegurl',
'application/vnd.apple.mpegurl',
'application/vnd.apple.mpegurl.audio',
'audio/x-mpegurl',
'audio/mpegurl',
],
concurrency: 1,
maxUploads: 1,
};
/** Emits on reject event */
@Output() fileRejected: EventEmitter<string> = new EventEmitter();
/** Emits after successful file selection */
@Output() fileSelected: EventEmitter<{
uploadEvent: Event;
file: UploadFile;
}> = new EventEmitter();
/**
* Handles file upload
* @param output
*/
onUploadOutput(output: UploadOutput): void {
if (output.type === 'allAddedToQueue') {
if (this.files.length > 0) {
const fileReader = new FileReader();
fileReader.onload = (uploadEvent) =>
this.fileSelected.emit({
uploadEvent,
file: this.files[0],
});
fileReader.readAsText(this.files[0].nativeFile);
}
} else if (
output.type === 'addedToQueue' &&
typeof output.file !== 'undefined'
) {
this.files.push(output.file);
} else if (
output.type === 'uploading' &&
typeof output.file !== 'undefined'
) {
const index = this.files.findIndex(
(file) =>
typeof output.file !== 'undefined' &&
file.id === output.file.id
);
this.files[index] = output.file;
} else if (output.type === 'cancelled' || output.type === 'removed') {
this.files = this.files.filter(
(file: UploadFile) => file !== output.file
);
} else if (output.type === 'dragOver') {
this.dragOver = true;
} else if (output.type === 'dragOut') {
this.dragOver = false;
} else if (output.type === 'drop') {
this.dragOver = false;
} else if (
output.type === 'rejected' &&
typeof output.file !== 'undefined'
) {
this.fileRejected.emit(output.file.name);
}
}
}
@@ -1,4 +1,7 @@
<!-- Applications header-->
<app-header fxLayout="row" title="IPTVnator" subtitle="Please select a playlist from the list or add a new playlist via file upload or via url"></app-header>
<!-- Tab-based layout -->
<div fxLayout="row" fxFlexAlign="start start">
<div fxFlex="100">
<mat-tab-group [dynamicHeight]="true" color="primary" backgroundColor="primary" mat-stretch-tabs>
@@ -15,19 +18,7 @@
<mat-icon class="tab-icon">attach_file</mat-icon> Add via file upload
</ng-template>
<ng-template matTabContent>
<div class="drop-container" ngFileDrop [options]="options" (uploadOutput)="onUploadOutput($event)"
[uploadInput]="uploadInput">
<mat-icon class="upload-icon">save_alt</mat-icon>
<p class="upload-title">
Drag & drop files here
</p>
<p>or</p>
<input type="file" ngFileSelect [options]="options" (uploadOutput)="onUploadOutput($event)"
class="input-file" name="file" id="file" [uploadInput]="uploadInput" multiple>
<label for="file">
Choose a playlist
</label>
</div>
<app-file-upload (fileRejected)="rejectFile($event)" (fileSelected)="handlePlaylist($event)"></app-file-upload>
</ng-template>
</mat-tab>
<mat-tab>
@@ -35,13 +26,7 @@
<mat-icon class="tab-icon">link</mat-icon> Add via URL
</ng-template>
<ng-template matTabContent>
<div class="url-form">
<mat-form-field class="full-width">
<input matInput placeholder="Playlist URL (m3u, m3u8)" [(ngModel)]="playlistUrl">
</mat-form-field>
<button mat-flat-button [disabled]="!playlistUrl" color="primary"
(click)="sendPlaylistsUrl()">ADD PLAYLIST</button>
</div>
<app-url-upload (urlAdded)="sendPlaylistsUrl($event)"></app-url-upload>
</ng-template>
</mat-tab>
</mat-tab-group>
+13
View File
@@ -0,0 +1,13 @@
::ng-deep {
.mat-tab-label-content {
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;
}
}
+25
View File
@@ -0,0 +1,25 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HomeComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+173
View File
@@ -0,0 +1,173 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, NgZone } from '@angular/core';
import { UploadFile } from 'ngx-uploader';
import { ChannelStore, createChannel } from '../state';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Playlist } from './playlist.interface';
import { ElectronService } from '../services/electron.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent {
/** Added playlists */
playlists: Pick<Playlist, 'count' | 'title' | '_id'>[] = [];
/** Loading spinner state */
isLoading = false;
/** IPC Renderer commands list with callbacks */
commandsList = [
{
id: 'parse-response',
execute: (response: any) => this.setPlaylist(response.payload),
},
{
id: 'playlist-all-result',
execute: (response: any) => (this.playlists = response.payload),
},
{
id: 'playlist-remove-by-id-result',
execute: () => {
this.snackBar.open('Done! Playlist was removed.', null, {
duration: 2000,
});
this.electronService.ipcRenderer.send('playlists-all');
},
},
];
/**
* Creates an instanceof HomeComponent
* @param channelStore channels store
* @param electronService electron service
* @param ngZone angulars ngZone module
* @param router angulars router
* @param snackBar snackbars with notification messages
*/
constructor(
private channelStore: ChannelStore,
private electronService: ElectronService,
private ngZone: NgZone,
private router: Router,
private snackBar: MatSnackBar
) {
// get all playlists
this.electronService.ipcRenderer.send('playlists-all');
// set all renderer listeners
this.setRendererListeners();
}
/**
* Set electrons main process listeners
*/
setRendererListeners(): void {
this.commandsList.forEach((command) => {
this.electronService.ipcRenderer.on(
command.id,
(event, response) => {
this.ngZone.run(() => command.execute(response));
}
);
});
}
/**
* Shows the filename of rejected file
* @param fileName name of the uploaded file
*/
rejectFile(fileName: string): void {
this.snackBar.open(
`File was rejected, unsupported file format (${fileName}).`,
null,
{
duration: 2000,
}
);
this.isLoading = false;
}
/**
* Parse and store uploaded playlist
* @param payload
*/
handlePlaylist(payload: { uploadEvent: Event; file: UploadFile }): void {
this.isLoading = true;
const result = (payload.uploadEvent.target as FileReader).result;
const array = (result as string).split('\n');
this.electronService.ipcRenderer.send('parse-playlist', {
title: payload.file.name,
playlist: array,
});
}
/**
* Navigates to the video player route
*/
navigateToPlayer(): void {
this.isLoading = false;
this.router.navigateByUrl('/iptv', { skipLocationChange: true });
}
/**
* Sends url of the playlist to the renderer process
* @param playlistUrl url of the added playlist
*/
sendPlaylistsUrl(playlistUrl: string): void {
this.electronService.ipcRenderer.send('parse-playlist-by-url', {
title: this.getLastUrlSegment(playlistUrl),
url: playlistUrl,
});
}
/**
* Sets the given playlist as active for the current session
* @param playlist playlist object
*/
setPlaylist(playlist: Playlist): void {
this.channelStore.reset();
const favorites = playlist.favorites || [];
const channels = playlist.playlist.items.map((element) =>
createChannel(element, favorites)
);
this.channelStore.upsertMany(channels);
this.channelStore.update(() => ({
favorites,
playlistId: playlist.id,
}));
this.navigateToPlayer();
}
/**
* Removes the provided playlist from the database
* @param playlistId playlist id to remove
*/
removePlaylist(playlistId: string): void {
this.electronService.ipcRenderer.send('playlist-remove-by-id', {
id: playlistId,
});
}
/**
* Requests playlist by id
* @param playlistId playlist id
*/
getPlaylist(playlistId: string): void {
this.electronService.ipcRenderer.send('playlist-by-id', {
id: playlistId,
});
}
/**
* Returns last segment (part after last slash "/") of the given URL
* @param value URL as string
*/
getLastUrlSegment(value: string): string {
if (value && value.length > 1) {
return value.substr(value.lastIndexOf('/') + 1);
} else {
return '';
}
}
}
@@ -0,0 +1,33 @@
<mat-nav-list>
<mat-list-item *ngIf="playlists.length === 0">
<mat-icon mat-list-icon>cloud</mat-icon>
<div mat-line>No playlists were added</div>
<div mat-line class="meta">
Please add your first playlist from the other tab
</div>
</mat-list-item>
<a
mat-list-item
*ngFor="let item of playlists; last as last"
(click)="playlistClicked.emit(item._id)"
>
<mat-icon mat-list-icon *ngIf="item?.url">cloud</mat-icon>
<mat-icon mat-list-icon *ngIf="!item?.url">folder</mat-icon>
<div mat-line>{{ item.title || item.filename }}</div>
<div mat-line class="meta">
Channels: {{ item.count }}
<ng-container *ngIf="item.importDate?.length > 3">
| Added: {{ item.importDate | date }}
</ng-container>
</div>
<button
mat-icon-button
color="primary"
(click)="$event.stopPropagation(); removeClicked.emit(item._id)"
matTooltip="Remove playlist"
>
<mat-icon>delete</mat-icon>
</button>
<mat-divider *ngIf="!last"></mat-divider>
</a>
</mat-nav-list>
@@ -1,5 +1,5 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Playlist } from 'app/playlist-uploader/playlist.interface';
import { Playlist } from '../playlist.interface';
@Component({
selector: 'app-recent-playlists',
@@ -0,0 +1,18 @@
<form (ngSubmit)="urlAdded.emit(playlistUrl.value)">
<mat-form-field class="full-width">
<input
type="url"
matInput
placeholder="Playlist URL (m3u, m3u8)"
#playlistUrl
/>
</mat-form-field>
<button
mat-flat-button
[disabled]="!playlistUrl.value"
color="primary"
type="submit"
>
Add playlist
</button>
</form>
@@ -0,0 +1,7 @@
form {
margin: 15px;
}
button {
text-transform: uppercase;
}
@@ -0,0 +1,49 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { UrlUploadComponent } from './url-upload.component';
import { MockModule } from 'ng-mocks';
import { MatInputModule } from '@angular/material/input';
describe('UrlUploadComponent', () => {
let component: UrlUploadComponent;
let fixture: ComponentFixture<UrlUploadComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [UrlUploadComponent],
imports: [MockModule(MatInputModule)],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UrlUploadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('submit form with playlist url', async(() => {
spyOn(component.urlAdded, 'emit');
const TEST_URL = 'http://example.org/playlist.m3u';
const input = fixture.debugElement.query(By.css('input'));
const el = input.nativeElement;
const submitButton = fixture.debugElement.nativeElement.querySelector(
'button'
);
expect(submitButton.disabled).toBeTruthy();
el.value = TEST_URL;
fixture.detectChanges();
expect(submitButton.disabled).toBeFalsy();
const form = fixture.debugElement.query(By.css('form'));
form.triggerEventHandler('ngSubmit', null);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(component.urlAdded.emit).toHaveBeenCalledWith(TEST_URL);
}));
});
@@ -0,0 +1,11 @@
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-url-upload',
templateUrl: './url-upload.component.html',
styleUrls: ['./url-upload.component.scss'],
})
export class UrlUploadComponent {
/** Emits url string to the parent component on form submit */
@Output() urlAdded: EventEmitter<string> = new EventEmitter();
}
@@ -1,240 +0,0 @@
import { Component, EventEmitter, NgZone } from '@angular/core';
import {
UploadOutput,
UploadInput,
UploadFile,
humanizeBytes,
UploaderOptions,
} from 'ngx-uploader';
import { ChannelStore, createChannel } from '../state';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Playlist } from './playlist.interface';
import { ElectronService } from 'app/services/electron.service';
@Component({
selector: 'app-playlist-uploader',
templateUrl: './playlist-uploader.component.html',
styleUrls: ['./playlist-uploader.component.scss'],
})
export class PlaylistUploaderComponent {
playlistUrl = '';
formData: FormData;
files: UploadFile[] = [];
uploadInput: EventEmitter<UploadInput> = new EventEmitter<UploadInput>();
humanizeBytes: any = humanizeBytes;
dragOver: boolean;
options: UploaderOptions = {
allowedContentTypes: [
'application/x-mpegurl',
'application/octet-stream',
'application/mpegurl',
'application/vnd.apple.mpegurl',
'application/vnd.apple.mpegurl.audio',
'audio/x-mpegurl',
'audio/mpegurl',
],
concurrency: 1,
maxUploads: 1,
};
playlists: { count: number; title: string; _id: string }[] = [];
isLoading = false;
/**
* Creates an instanceof PlaylistUploaderComponent
* @param channelStore channels store
* @param electronService electron service
* @param ngZone angulars ngZone module
* @param router angulars router
* @param snackBar snackbars with notification messages
*/
constructor(
private channelStore: ChannelStore,
private electronService: ElectronService,
private ngZone: NgZone,
private router: Router,
private snackBar: MatSnackBar
) {
// get all playlists
this.electronService.ipcRenderer.send('playlists-all');
// set all renderer listeners
this.setRendererListeners();
}
/**
* Set electrons main process listeners
*/
setRendererListeners(): void {
this.electronService.ipcRenderer.on(
'parse-response',
(event, response) => {
this.ngZone.run(() => this.setPlaylist(response.payload));
}
);
this.electronService.ipcRenderer.on(
'parse-url-response',
(event, response) => {
this.ngZone.run(() => this.setPlaylist(response.payload));
}
);
this.electronService.ipcRenderer.on(
'playlist-all-result',
(event, response) => {
this.ngZone.run(() => (this.playlists = response.payload));
}
);
this.electronService.ipcRenderer.on(
'playlist-by-id-result',
(event, response) => {
this.ngZone.run(() => this.setPlaylist(response.payload));
}
);
this.electronService.ipcRenderer.on(
'playlist-remove-by-id-result',
() => {
this.ngZone.run(() => {
this.snackBar.open('Done! Playlist was removed.', null, {
duration: 2000,
});
this.electronService.ipcRenderer.send('playlists-all');
});
}
);
}
/**
* Handles file upload
* @param output
*/
onUploadOutput(output: UploadOutput): void {
if (output.type === 'allAddedToQueue') {
if (this.files.length > 0) {
this.isLoading = true;
const fileReader = new FileReader();
fileReader.onload = (fileLoadedEvent) =>
this.handlePlaylist(fileLoadedEvent);
fileReader.readAsText(this.files[0].nativeFile);
}
} else if (
output.type === 'addedToQueue' &&
typeof output.file !== 'undefined'
) {
this.files.push(output.file);
} else if (
output.type === 'uploading' &&
typeof output.file !== 'undefined'
) {
const index = this.files.findIndex(
(file) =>
typeof output.file !== 'undefined' &&
file.id === output.file.id
);
this.files[index] = output.file;
} else if (output.type === 'cancelled' || output.type === 'removed') {
this.files = this.files.filter(
(file: UploadFile) => file !== output.file
);
} else if (output.type === 'dragOver') {
this.dragOver = true;
} else if (output.type === 'dragOut') {
this.dragOver = false;
} else if (output.type === 'drop') {
this.dragOver = false;
} else if (
output.type === 'rejected' &&
typeof output.file !== 'undefined'
) {
this.snackBar.open(
`File was rejected, unsupported file format (${output.file.name}).`,
null,
{
duration: 2000,
}
);
this.isLoading = false;
}
}
/**
* Parse and store uploaded playlist
* @param fileLoadedEvent
*/
handlePlaylist(fileLoadedEvent: Event): void {
const result = (fileLoadedEvent.target as FileReader).result;
const array = (result as string).split('\n');
this.electronService.ipcRenderer.send('parse-playlist', {
title: this.files[0].name,
playlist: array,
});
}
/**
* Navigates to the video player route
*/
navigateToPlayer(): void {
this.isLoading = false;
this.router.navigateByUrl('/iptv', { skipLocationChange: true });
}
/**
* Sends url of the playlist to the renderer process
*/
sendPlaylistsUrl(): void {
this.electronService.ipcRenderer.send('parse-playlist-by-url', {
title: this.getLastUrlSegment(this.playlistUrl),
url: this.playlistUrl,
});
}
/**
* Sets the given playlist as active for the current session
* @param playlist playlist object
*/
setPlaylist(playlist: Playlist): void {
this.channelStore.reset();
const favorites = playlist.favorites || [];
const channels = playlist.playlist.items.map((element) =>
createChannel(element, favorites)
);
this.channelStore.upsertMany(channels);
this.channelStore.update(() => ({
favorites,
playlistId: playlist.id,
}));
this.navigateToPlayer();
}
/**
* Removes the provided playlist from the database
* @param playlistId playlist id to remove
*/
removePlaylist(playlistId: string): void {
this.electronService.ipcRenderer.send('playlist-remove-by-id', {
id: playlistId,
});
}
/**
* Requests playlist by id
* @param playlistId playlist id
*/
getPlaylist(playlistId: string): void {
this.electronService.ipcRenderer.send('playlist-by-id', {
id: playlistId,
});
}
/**
* Returns last segment (part after last slash "/") of the given URL
* @param value URL as string
*/
getLastUrlSegment(value: string): string {
if (value && value.length > 1) {
return value.substr(value.lastIndexOf('/') + 1);
} else {
return '';
}
}
}
@@ -1,31 +0,0 @@
<mat-nav-list>
<mat-list-item *ngIf="playlists.length === 0">
<mat-icon mat-list-icon>cloud</mat-icon>
<div mat-line>No playlists were added</div>
<div mat-line class="meta">Please add your first playlist from the other tab</div>
</mat-list-item>
<a mat-list-item *ngFor="let item of playlists; last as last" (click)="playlistClicked.emit(item._id)">
<mat-icon mat-list-icon *ngIf="item?.url">cloud</mat-icon>
<mat-icon mat-list-icon *ngIf="!item?.url">folder</mat-icon>
<div mat-line>{{ item.title || item.filename }}</div>
<div mat-line class="meta">
Channels: {{ item.count }} <ng-container *ngIf="item.importDate?.length > 3">
| Added: {{item.importDate | date}}
</ng-container>
</div>
<!--<button mat-icon-button color="primary" (click)="$event.stopPropagation(); renameClicked.emit(item)" matTooltip="Rename playlist">
<mat-icon>edit</mat-icon>
</button>-->
<!-- <button *ngIf="item.url" mat-icon-button color="primary" (click)="$event.stopPropagation(); "
matTooltip="Update playlist by URL">
<mat-icon>refresh</mat-icon>
</button> -->
<button mat-icon-button color="primary" (click)="$event.stopPropagation(); removeClicked.emit(item._id)"
matTooltip="Remove playlist">
<mat-icon>delete</mat-icon>
</button>
<mat-divider *ngIf="!last"></mat-divider>
</a>
</mat-nav-list>
@@ -3,9 +3,9 @@ import * as Hls from 'hls.js';
import { ChannelQuery, Channel } from '../state';
import { Observable } from 'rxjs';
import { MatSidenav } from '@angular/material/sidenav';
import { ElectronService } from 'app/services/electron.service';
import { ElectronService } from '../services/electron.service';
import { StorageMap } from '@ngx-pwa/local-storage';
import { Settings, VideoPlayerType } from 'app/settings/settings.interface';
import { Settings, VideoPlayerType } from '../settings/settings.interface';
/** Settings key in storage */
export const SETTINGS_STORE_KEY = 'settings';
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
}