mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
refactor: improve structure of home screen
This commit is contained in:
1 parent
f778e1d9d8
commit
90b4a54e27
23 files changed
+479
-328
No files matched your search
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
-19
@@ -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;
|
||||
+6
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-20
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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>
|
||||
File renamed without changes.
+1
-1
@@ -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';
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user