mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
refactor: some general code improvements
This commit is contained in:
1 parent
41f3f34faa
commit
cb07cc2386
22 files changed
+405
-319
No files matched your search
@@ -7,6 +7,8 @@ import {
|
||||
EPG_ERROR,
|
||||
EPG_FETCH,
|
||||
EPG_FETCH_DONE,
|
||||
EPG_GET_CHANNELS,
|
||||
EPG_GET_CHANNELS_DONE,
|
||||
EPG_GET_PROGRAM,
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
} from './shared/ipc-commands';
|
||||
@@ -105,3 +107,9 @@ ipcRenderer.on(EPG_GET_PROGRAM, (event, args) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(EPG_GET_CHANNELS, (event, args) => {
|
||||
ipcRenderer.send(EPG_GET_CHANNELS_DONE, {
|
||||
payload: EPG_DATA,
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
export class IpcCommand {
|
||||
constructor(public id: string, public callback: () => void) {
|
||||
constructor(public id: string, public callback: (payload) => void) {
|
||||
this.id = id;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ 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';
|
||||
export const EPG_GET_CHANNELS = 'EPG:GET_CHANNELS';
|
||||
export const EPG_GET_CHANNELS_DONE = 'EPG:GET_CHANNELS_DONE';
|
||||
|
||||
// Playlist related commands
|
||||
export const PLAYLIST_GET_ALL = 'PLAYLIST:GET_ALL';
|
||||
|
||||
@@ -12,7 +12,10 @@ export function aggregateFavoriteChannels(playlists: Playlist[]): Channel[] {
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.favorites?.length > 0) {
|
||||
playlist.playlist.items.forEach((channel) => {
|
||||
if (playlist.favorites.includes(channel.id)) {
|
||||
if (
|
||||
playlist.favorites.includes(channel.id) ||
|
||||
playlist.favorites.includes(channel.url)
|
||||
) {
|
||||
favorites.push(channel);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { DataService } from './services/data.service';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
import { WhatsNewService } from './services/whats-new.service';
|
||||
import { Language } from './settings/language.enum';
|
||||
import { Settings } from './settings/settings.interface';
|
||||
import { Theme } from './settings/theme.enum';
|
||||
import { STORE_KEY } from './shared/enums/store-keys.enum';
|
||||
@@ -55,7 +56,7 @@ export class AppComponent {
|
||||
];
|
||||
|
||||
/** Default language as fallback */
|
||||
DEFAULT_LANG = 'en';
|
||||
DEFAULT_LANG = Language.ENGLISH;
|
||||
|
||||
/**
|
||||
* Creates an instance of AppComponent
|
||||
@@ -109,7 +110,7 @@ export class AppComponent {
|
||||
if (this.electronService.isElectron) {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.electronService.listenOn(command.id, () =>
|
||||
this.ngZone.run(() => command.callback())
|
||||
this.ngZone.run((data) => command.callback(data))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,13 +17,7 @@
|
||||
<mat-icon class="tab-icon">list</mat-icon>
|
||||
{{ 'HOME.TABS.RECENTLY_ADDED' | translate }}
|
||||
</ng-template>
|
||||
<app-recent-playlists
|
||||
class="recent-playlists"
|
||||
[playlists]="playlists"
|
||||
(playlistClicked)="getPlaylist($event)"
|
||||
(refreshClicked)="refreshPlaylist($event)"
|
||||
(removeClicked)="removeClicked($event)"
|
||||
>
|
||||
<app-recent-playlists class="recent-playlists">
|
||||
</app-recent-playlists>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { UploadFile } from 'ngx-uploader';
|
||||
import {
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_UPDATE,
|
||||
} from './../../../shared/ipc-commands';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import { HomeComponent, PlaylistMeta } from './home.component';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import { RecentPlaylistsComponent } from '../home/recent-playlists/recent-playlists.component';
|
||||
import { FileUploadComponent } from '../home/file-upload/file-upload.component';
|
||||
import { UrlUploadComponent } from '../home/url-upload/url-upload.component';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { Router } from '@angular/router';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { ElectronServiceStub } from '../services/electron.service.stub';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { UploadFile } from 'ngx-uploader';
|
||||
import { of } from 'rxjs';
|
||||
import { FileUploadComponent } from '../home/file-upload/file-upload.component';
|
||||
import { RecentPlaylistsComponent } from '../home/recent-playlists/recent-playlists.component';
|
||||
import { UrlUploadComponent } from '../home/url-upload/url-upload.component';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { ElectronServiceStub } from '../services/electron.service.stub';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import {
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
} from './../../../shared/ipc-commands';
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
@@ -38,7 +34,6 @@ class NgxIndexedDBServiceStub {
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let dialogService: DialogService;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
let electronService: DataService;
|
||||
let router: Router;
|
||||
@@ -64,7 +59,6 @@ describe('HomeComponent', () => {
|
||||
providers: [
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
MockProvider(DialogService),
|
||||
MockProvider(TranslateService),
|
||||
{
|
||||
provide: NgxIndexedDBService,
|
||||
@@ -78,7 +72,7 @@ describe('HomeComponent', () => {
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
electronService = TestBed.inject(DataService);
|
||||
dialogService = TestBed.inject(DialogService);
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
TestBed.inject(NgxIndexedDBService);
|
||||
fixture.detectChanges();
|
||||
@@ -88,48 +82,6 @@ describe('HomeComponent', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should open the confirmation dialog on remove icon click', () => {
|
||||
const playlistId = '12345';
|
||||
jest.spyOn(dialogService, 'openConfirmDialog');
|
||||
component.removeClicked(playlistId);
|
||||
expect(dialogService.openConfirmDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to remove a playlist', () => {
|
||||
const playlistId = '12345';
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.removePlaylist(playlistId);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
{ id: playlistId }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to refresh a playlist', () => {
|
||||
const playlistMeta: PlaylistMeta = {
|
||||
_id: 'iptv1',
|
||||
filePath: '/home/user/lists/iptv.m3u',
|
||||
} as PlaylistMeta;
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.refreshPlaylist(playlistMeta);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_UPDATE,
|
||||
{ id: playlistMeta._id, filePath: playlistMeta.filePath }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to get a playlist', () => {
|
||||
const playlistId = '6789';
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.getPlaylist(playlistId);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_GET_BY_ID,
|
||||
{
|
||||
id: playlistId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to get a playlist by URL', () => {
|
||||
const playlistTitle = 'playlist.m3u';
|
||||
const playlistUrl = 'http://test.com/' + playlistTitle;
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { UploadFile } from 'ngx-uploader';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { ChannelStore } from '../state';
|
||||
import { DialogService } from './../services/dialog.service';
|
||||
|
||||
/** Type to describe meta data of a playlist */
|
||||
export type PlaylistMeta = Pick<
|
||||
@@ -45,8 +37,10 @@ export type PlaylistMeta = Pick<
|
||||
export class HomeComponent {
|
||||
/** Added playlists */
|
||||
playlists: PlaylistMeta[] = [];
|
||||
|
||||
/** Loading spinner state */
|
||||
isLoading = false;
|
||||
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
{
|
||||
@@ -56,21 +50,6 @@ export class HomeComponent {
|
||||
this.navigateToPlayer();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: PLAYLIST_GET_ALL_RESPONSE,
|
||||
execute: (response: { payload: Partial<PlaylistMeta[]> }): void => {
|
||||
this.playlists = response.payload;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
execute: (): void => {
|
||||
this.snackBar.open('Done! Playlist was removed.', null, {
|
||||
duration: 2000,
|
||||
});
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ERROR,
|
||||
execute: (response: { message: string; status: number }): void => {
|
||||
@@ -90,24 +69,18 @@ export class HomeComponent {
|
||||
/**
|
||||
* Creates an instanceof HomeComponent
|
||||
* @param channelStore channels store
|
||||
* @param dialogService dialog service
|
||||
* @param electronService electron service
|
||||
* @param ngZone angular ngZone module
|
||||
* @param router angular router
|
||||
* @param snackBar snackbar for notification messages
|
||||
* @param translate translate service
|
||||
*/
|
||||
constructor(
|
||||
private electronService: DataService,
|
||||
private dialogService: DialogService,
|
||||
private ngZone: NgZone,
|
||||
private channelStore: ChannelStore,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private translate: TranslateService
|
||||
private snackBar: MatSnackBar
|
||||
) {
|
||||
// get all playlists
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
// set all renderer listeners
|
||||
this.setRendererListeners();
|
||||
}
|
||||
@@ -177,51 +150,6 @@ export class HomeComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers on remove click
|
||||
* @param playlistId playlist id to remove
|
||||
*/
|
||||
removeClicked(playlistId: string): void {
|
||||
this.dialogService.openConfirmDialog({
|
||||
title: this.translate.instant('HOME.PLAYLISTS.REMOVE_DIALOG.TITLE'),
|
||||
message: this.translate.instant(
|
||||
'HOME.PLAYLISTS.REMOVE_DIALOG.MESSAGE'
|
||||
),
|
||||
onConfirm: (): void => this.removePlaylist(playlistId),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided playlist from the database
|
||||
* @param playlistId playlist id to remove
|
||||
*/
|
||||
removePlaylist(playlistId: string): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_REMOVE_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an IPC event with the playlist details to the main process to trigger the refresh operation
|
||||
* @param item playlist to update
|
||||
*/
|
||||
refreshPlaylist(item: PlaylistMeta): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_UPDATE, {
|
||||
id: item._id,
|
||||
...(item.url ? { url: item.url } : { filePath: item.filePath }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests playlist by id
|
||||
* @param playlistId playlist id
|
||||
*/
|
||||
getPlaylist(playlistId: string): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns last segment (part after last slash "/") of the given URL
|
||||
* @param value URL as string
|
||||
|
||||
@@ -55,15 +55,13 @@
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
<ng-container *ngIf="playlist.url || playlist.filePath">
|
||||
<mat-checkbox
|
||||
class="full-width"
|
||||
formControlName="autoRefresh"
|
||||
>
|
||||
<mat-checkbox class="full-width" formControlName="autoRefresh">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
|
||||
</mat-checkbox>
|
||||
<p>
|
||||
{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION' | translate
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<mat-list-item cdkDrag (click)="playlistClicked.emit(item._id)">
|
||||
<mat-icon mat-list-icon cdkDragHandle class="drag-icon"
|
||||
>drag_indicator</mat-icon
|
||||
>
|
||||
<mat-icon
|
||||
mat-list-icon
|
||||
*ngIf="item?.url"
|
||||
[matTooltip]="
|
||||
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) + ' ' + item?.url
|
||||
"
|
||||
>cloud</mat-icon
|
||||
>
|
||||
<mat-icon
|
||||
mat-list-icon
|
||||
*ngIf="!item?.url"
|
||||
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
|
||||
>folder</mat-icon
|
||||
>
|
||||
<div mat-line>{{ item.title || item.filename }}</div>
|
||||
<div mat-line class="meta">
|
||||
{{ 'HOME.PLAYLISTS.CHANNELS' | translate }}: {{ item.count }}
|
||||
<ng-container *ngIf="item.importDate?.length > 3">
|
||||
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
||||
{{ item.importDate | date }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="item.updateDate">
|
||||
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="item.updateState === 2">
|
||||
<mat-icon
|
||||
[matTooltip]="
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate
|
||||
"
|
||||
[inline]="true"
|
||||
>warning</mat-icon
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button
|
||||
*ngIf="item.url || item.filePath"
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); refreshClicked.emit(item)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.REFRESH' | translate"
|
||||
>
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); editPlaylistClicked.emit(item)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.SHOW_DETAILS' | translate"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); removeClicked.emit(item._id)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.REMOVE' | translate"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
</mat-list-item>
|
||||
@@ -0,0 +1,32 @@
|
||||
mat-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mat-list-base {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 12px !important;
|
||||
color: #666;
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
cursor: move;
|
||||
margin-left: -20px;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* tslint:disable:no-unused-variable */
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe, MockProviders } from 'ng-mocks';
|
||||
import { PlaylistItemComponent } from './playlist-item.component';
|
||||
|
||||
describe('PlaylistItemComponent', () => {
|
||||
let component: PlaylistItemComponent;
|
||||
let fixture: ComponentFixture<PlaylistItemComponent>;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PlaylistItemComponent, MockPipe(TranslatePipe)],
|
||||
providers: [MockProviders(TranslateService)],
|
||||
imports: [
|
||||
MockModule(MatIconModule),
|
||||
MockModule(MatListModule),
|
||||
MockModule(MatTooltipModule),
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PlaylistItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = { title: 'Playlist', id: '1' } as any;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { PlaylistMeta } from '../../home.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-item',
|
||||
templateUrl: './playlist-item.component.html',
|
||||
styleUrls: ['./playlist-item.component.scss'],
|
||||
})
|
||||
export class PlaylistItemComponent {
|
||||
/** Playlist item */
|
||||
@Input() item: PlaylistMeta;
|
||||
|
||||
/** Emits on playlist selection */
|
||||
@Output() playlistClicked: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
/** Emits on playlist refresh click */
|
||||
@Output() refreshClicked: EventEmitter<PlaylistMeta> = new EventEmitter();
|
||||
|
||||
/** Emits on playlist remove click */
|
||||
@Output() removeClicked: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
/** Emits on playlist edit click */
|
||||
@Output() editPlaylistClicked: EventEmitter<PlaylistMeta> =
|
||||
new EventEmitter();
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
{{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item *ngIf="playlists.length > 0" (click)="playlistClicked.emit('GLOBAL_FAVORITES')">
|
||||
<mat-list-item
|
||||
*ngIf="playlists.length > 0"
|
||||
(click)="getPlaylist('GLOBAL_FAVORITES')"
|
||||
>
|
||||
<mat-icon class="favorites-icon" mat-list-icon>star</mat-icon>
|
||||
<div mat-line>Global favorites</div>
|
||||
<div mat-line class="meta">
|
||||
@@ -15,74 +18,12 @@
|
||||
<mat-divider></mat-divider>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item
|
||||
cdkDrag
|
||||
<app-playlist-item
|
||||
*ngFor="let item of playlists; last as last"
|
||||
(click)="playlistClicked.emit(item._id)"
|
||||
>
|
||||
<mat-icon mat-list-icon cdkDragHandle class="drag-icon"
|
||||
>drag_indicator</mat-icon
|
||||
>
|
||||
<mat-icon
|
||||
mat-list-icon
|
||||
*ngIf="item?.url"
|
||||
[matTooltip]="
|
||||
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) + ' ' + item?.url
|
||||
"
|
||||
>cloud</mat-icon
|
||||
>
|
||||
<mat-icon
|
||||
mat-list-icon
|
||||
*ngIf="!item?.url"
|
||||
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
|
||||
>folder</mat-icon
|
||||
>
|
||||
<div mat-line>{{ item.title || item.filename }}</div>
|
||||
<div mat-line class="meta">
|
||||
{{ 'HOME.PLAYLISTS.CHANNELS' | translate }}: {{ item.count }}
|
||||
<ng-container *ngIf="item.importDate?.length > 3">
|
||||
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
||||
{{ item.importDate | date }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="item.updateDate">
|
||||
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="item.updateState === 2">
|
||||
<mat-icon
|
||||
[matTooltip]="
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate
|
||||
"
|
||||
[inline]="true"
|
||||
>warning</mat-icon
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button
|
||||
*ngIf="item.url || item.filePath"
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); refreshClicked.emit(item)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.REFRESH' | translate"
|
||||
>
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); openInfoDialog(item)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.SHOW_DETAILS' | translate"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="$event.stopPropagation(); removeClicked.emit(item._id)"
|
||||
[matTooltip]="'HOME.PLAYLISTS.REMOVE' | translate"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<mat-divider *ngIf="!last"></mat-divider>
|
||||
</mat-list-item>
|
||||
[item]="item"
|
||||
(editPlaylistClicked)="openInfoDialog($event)"
|
||||
(playlistClicked)="getPlaylist($event)"
|
||||
(refreshClicked)="refreshPlaylist($event)"
|
||||
(removeClicked)="removeClicked($event)"
|
||||
></app-playlist-item>
|
||||
</mat-nav-list>
|
||||
@@ -1,11 +1,3 @@
|
||||
mat-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mat-list-base {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 12px !important;
|
||||
color: #666;
|
||||
@@ -33,25 +25,6 @@ mat-list-item {
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
cursor: move;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
.favorites-icon {
|
||||
margin-left: 16px;
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { RecentPlaylistsComponent } from './recent-playlists.component';
|
||||
import { PlaylistMeta } from '../home.component';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import {
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_UPDATE,
|
||||
} from '../../../../shared/ipc-commands';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { DialogService } from '../../services/dialog.service';
|
||||
import { ElectronServiceStub } from '../../services/electron.service.stub';
|
||||
import { PlaylistMeta } from '../home.component';
|
||||
import { RecentPlaylistsComponent } from './recent-playlists.component';
|
||||
|
||||
describe('RecentPlaylistsComponent', () => {
|
||||
let component: RecentPlaylistsComponent;
|
||||
let fixture: ComponentFixture<RecentPlaylistsComponent>;
|
||||
let electronService: DataService;
|
||||
let dialog: MatDialog;
|
||||
let dialogService: DialogService;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
@@ -32,6 +39,7 @@ describe('RecentPlaylistsComponent', () => {
|
||||
providers: [
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
MockProvider(TranslateService),
|
||||
MockProvider(DialogService),
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
@@ -43,6 +51,7 @@ describe('RecentPlaylistsComponent', () => {
|
||||
component.playlists = [];
|
||||
dialog = TestBed.inject(MatDialog);
|
||||
electronService = TestBed.inject(DataService);
|
||||
dialogService = TestBed.inject(DialogService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -71,4 +80,46 @@ describe('RecentPlaylistsComponent', () => {
|
||||
component.drop(event);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should open the confirmation dialog on remove icon click', () => {
|
||||
const playlistId = '12345';
|
||||
jest.spyOn(dialogService, 'openConfirmDialog');
|
||||
component.removeClicked(playlistId);
|
||||
expect(dialogService.openConfirmDialog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to remove a playlist', () => {
|
||||
const playlistId = '12345';
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.removePlaylist(playlistId);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
{ id: playlistId }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to refresh a playlist', () => {
|
||||
const playlistMeta: PlaylistMeta = {
|
||||
_id: 'iptv1',
|
||||
filePath: '/home/user/lists/iptv.m3u',
|
||||
} as PlaylistMeta;
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.refreshPlaylist(playlistMeta);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_UPDATE,
|
||||
{ id: playlistMeta._id, filePath: playlistMeta.filePath }
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to get a playlist', () => {
|
||||
const playlistId = '6789';
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.getPlaylist(playlistId);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_GET_BY_ID,
|
||||
{
|
||||
id: playlistId,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,22 @@
|
||||
import { PLAYLIST_UPDATE_POSITIONS } from './../../../../shared/ipc-commands';
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
|
||||
import { PlaylistMeta } from './../home.component';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { IpcCommand } from '../../../../shared/ipc-command.class';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import {
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
} from './../../../../shared/ipc-commands';
|
||||
import { DialogService } from './../../services/dialog.service';
|
||||
import { PlaylistMeta } from './../home.component';
|
||||
import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recent-playlists',
|
||||
@@ -13,27 +25,66 @@ import { DataService } from '../../services/data.service';
|
||||
})
|
||||
export class RecentPlaylistsComponent {
|
||||
/** All available playlists */
|
||||
@Input() playlists: PlaylistMeta[];
|
||||
playlists: PlaylistMeta[] = [];
|
||||
|
||||
/** Emits on playlist selection */
|
||||
@Output() playlistClicked: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
/** Emits on playlist refresh click */
|
||||
@Output() refreshClicked: EventEmitter<PlaylistMeta> = new EventEmitter();
|
||||
|
||||
/** Emits on playlist remove click */
|
||||
@Output() removeClicked: EventEmitter<string> = new EventEmitter();
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
new IpcCommand(
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
(response: { payload: Partial<PlaylistMeta[]> }) => {
|
||||
this.playlists = response.payload;
|
||||
}
|
||||
),
|
||||
new IpcCommand(PLAYLIST_REMOVE_BY_ID_RESPONSE, (): void => {
|
||||
this.snackBar.open('Done! Playlist was removed.', null, {
|
||||
duration: 2000,
|
||||
});
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates an instance of the component
|
||||
* @param dialog angular material dialog reference
|
||||
* @param dialogService dialog service
|
||||
* @param electronService electron service
|
||||
* @param snackBar angular material snackbar reference
|
||||
* @param translate translate service
|
||||
*/
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private electronService: DataService
|
||||
private dialog: MatDialog,
|
||||
private dialogService: DialogService,
|
||||
private electronService: DataService,
|
||||
private ngZone: NgZone,
|
||||
private snackBar: MatSnackBar,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// get all playlists
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
this.setRendererListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set electrons main process listeners
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) => {
|
||||
if (this.electronService.isElectron) {
|
||||
this.electronService.listenOn(command.id, (event, response) =>
|
||||
this.ngZone.run(() => command.callback(response))
|
||||
);
|
||||
} else {
|
||||
this.electronService.listenOn(command.id, (response) => {
|
||||
if (response.data.type === command.id) {
|
||||
command.callback(response.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the details dialog with the information about the provided playlist
|
||||
* @param data selected playlist
|
||||
@@ -59,4 +110,49 @@ export class RecentPlaylistsComponent {
|
||||
this.playlists
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests playlist by id
|
||||
* @param playlistId playlist id
|
||||
*/
|
||||
getPlaylist(playlistId: string): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers on remove click
|
||||
* @param playlistId playlist id to remove
|
||||
*/
|
||||
removeClicked(playlistId: string): void {
|
||||
this.dialogService.openConfirmDialog({
|
||||
title: this.translate.instant('HOME.PLAYLISTS.REMOVE_DIALOG.TITLE'),
|
||||
message: this.translate.instant(
|
||||
'HOME.PLAYLISTS.REMOVE_DIALOG.MESSAGE'
|
||||
),
|
||||
onConfirm: (): void => this.removePlaylist(playlistId),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided playlist from the database
|
||||
* @param playlistId playlist id to remove
|
||||
*/
|
||||
removePlaylist(playlistId: string): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_REMOVE_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an IPC event with the playlist details to the main process to trigger the refresh operation
|
||||
* @param item playlist to update
|
||||
*/
|
||||
refreshPlaylist(item: PlaylistMeta): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_UPDATE, {
|
||||
id: item._id,
|
||||
...(item.url ? { url: item.url } : { filePath: item.filePath }),
|
||||
});
|
||||
}
|
||||
}
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
>
|
||||
</app-channel-list-container>
|
||||
<ng-template #playlistsTemplate>
|
||||
<app-recent-playlists
|
||||
[playlists]="playlists"
|
||||
(playlistClicked)="getPlaylist($event)"
|
||||
>
|
||||
<app-recent-playlists (playlistClicked)="getPlaylist($event)">
|
||||
</app-recent-playlists>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { filter, Observable } from 'rxjs';
|
||||
import { Channel } from '../../../../../shared/channel.interface';
|
||||
import {
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
} from '../../../../../shared/ipc-commands';
|
||||
@@ -66,12 +65,6 @@ export class VideoPlayerComponent implements OnInit {
|
||||
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
{
|
||||
id: PLAYLIST_GET_ALL_RESPONSE,
|
||||
execute: (response: { payload: Playlist[] }): void => {
|
||||
this.playlists = response.payload;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: PLAYLIST_PARSE_RESPONSE,
|
||||
execute: (response: { payload: Playlist }): void => {
|
||||
|
||||
@@ -6,13 +6,13 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { FilterPipeModule } from 'ngx-filter-pipe';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { PlaylistItemComponent } from '../home/recent-playlists/playlist-item/playlist-item.component';
|
||||
import { RecentPlaylistsComponent } from '../home/recent-playlists/recent-playlists.component';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { HeaderComponent } from './components/';
|
||||
import { AboutDialogComponent } from './components/about-dialog/about-dialog.component';
|
||||
import { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dialog.component';
|
||||
import { MomentDatePipe } from './pipes/moment-date.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ConfirmDialogComponent,
|
||||
@@ -20,6 +20,7 @@ import { MomentDatePipe } from './pipes/moment-date.pipe';
|
||||
MomentDatePipe,
|
||||
AboutDialogComponent,
|
||||
RecentPlaylistsComponent,
|
||||
PlaylistItemComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -44,6 +45,7 @@ import { MomentDatePipe } from './pipes/moment-date.pipe';
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
RecentPlaylistsComponent,
|
||||
PlaylistItemComponent,
|
||||
],
|
||||
})
|
||||
export class SharedModule {}
|
||||
+18
-31
@@ -1,33 +1,20 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"module": "es2020",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"es2016",
|
||||
"es2015",
|
||||
"dom"
|
||||
],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"module": "es2020",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"lib": ["es2017", "es2016", "es2015", "dom"],
|
||||
"resolveJsonModule": true,
|
||||
"strict": false
|
||||
},
|
||||
"files": ["src/main.ts", "src/polyfills.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in new issue
Block a user