mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
refactor: replace tabs by mat-menu
This commit is contained in:
1 parent
623874984d
commit
1c7d14d216
20 files changed
+484
-535
No files matched your search
@@ -1,89 +1,10 @@
|
||||
<div class="grid-container">
|
||||
<!-- Application header-->
|
||||
<app-header
|
||||
[title]="'HOME.TITLE' | translate"
|
||||
[subtitle]="'HOME.SUBTITLE' | translate"
|
||||
></app-header>
|
||||
|
||||
<!-- Tab-based layout -->
|
||||
<mat-tab-group
|
||||
[dynamicHeight]="true"
|
||||
color="primary"
|
||||
backgroundColor="primary"
|
||||
mat-stretch-tabs
|
||||
>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">list</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'HOME.TABS.RECENTLY_ADDED' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template matTabContent>
|
||||
<app-recent-playlists class="recent-playlists">
|
||||
</app-recent-playlists>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">attach_file</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'HOME.TABS.FILE_UPLOAD' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template matTabContent>
|
||||
<app-file-upload
|
||||
(fileRejected)="rejectFile($event)"
|
||||
(fileSelected)="handlePlaylist($event)"
|
||||
/>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">link</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'HOME.TABS.URL_UPLOAD' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template matTabContent>
|
||||
<app-url-upload (urlAdded)="sendPlaylistsUrl($event)" />
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">text_format</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'HOME.TABS.TEXT_IMPORT' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template matTabContent>
|
||||
<app-text-import (textAdded)="uploadAsText($event)" />
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">video_library</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'HOME.TABS.XTREME_IMPORT' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template matTabContent>
|
||||
<app-xtream-code-import />
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">dashboard</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'HOME.TABS.STALKER_PORTAL_IMPORT' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template matTabContent>
|
||||
<app-stalker-portal-import />
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<app-recent-playlists class="recent-playlists" />
|
||||
|
||||
<!-- Loading spinner -->
|
||||
<mat-progress-bar *ngIf="isLoading" mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
@@ -3,39 +3,30 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { Actions } from '@ngrx/effects';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslatePipe,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { Observable } 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_BY_URL } from './../../../shared/ipc-commands';
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
class MatSnackBarStub {
|
||||
open(): void {}
|
||||
}
|
||||
|
||||
class NgxIndexedDBServiceStub {
|
||||
getAll() {
|
||||
return of([] as any);
|
||||
}
|
||||
}
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
let electronService: DataService;
|
||||
let router: Router;
|
||||
let mockStore: MockStore;
|
||||
const actions$ = new Observable<Actions>();
|
||||
|
||||
@@ -56,15 +47,13 @@ describe('HomeComponent', () => {
|
||||
MockModule(MatSnackBarModule),
|
||||
MockModule(RouterTestingModule),
|
||||
MockModule(NgxIndexedDBModule),
|
||||
MockModule(TranslateModule),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatSnackBar, useClass: MatSnackBarStub },
|
||||
MockProvider(MatSnackBar),
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
MockProvider(TranslateService),
|
||||
{
|
||||
provide: NgxIndexedDBService,
|
||||
useClass: NgxIndexedDBServiceStub,
|
||||
},
|
||||
MockProvider(NgxIndexedDBService),
|
||||
provideMockStore(),
|
||||
provideMockActions(actions$),
|
||||
],
|
||||
@@ -79,7 +68,6 @@ describe('HomeComponent', () => {
|
||||
mockStore = TestBed.inject(MockStore);
|
||||
mockStore.setState({});
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
TestBed.inject(NgxIndexedDBService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -88,36 +76,6 @@ describe('HomeComponent', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
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;
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
component.sendPlaylistsUrl(playlistUrl);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledWith(
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
{
|
||||
title: playlistTitle,
|
||||
url: playlistUrl,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should send an event to the main process to parse a playlist', () => {
|
||||
jest.spyOn(mockStore, 'dispatch');
|
||||
const title = 'my-list.m3u';
|
||||
const path = '/home/user/iptv/' + title;
|
||||
const playlistContent = 'test';
|
||||
const file: File = {
|
||||
path,
|
||||
name: title,
|
||||
} as unknown as File;
|
||||
const uploadEvent: Event = {
|
||||
target: { result: playlistContent },
|
||||
} as unknown as Event;
|
||||
component.handlePlaylist({ file, uploadEvent });
|
||||
expect(mockStore.dispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should set IPC event listeners', () => {
|
||||
jest.spyOn(electronService, 'listenOn');
|
||||
component.setRendererListeners();
|
||||
@@ -126,12 +84,6 @@ describe('HomeComponent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should send notification on file reject', () => {
|
||||
jest.spyOn(component, 'showNotification');
|
||||
component.rejectFile('wrong-file.txt');
|
||||
expect(component.showNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should remove all ipc listeners on destroy', () => {
|
||||
jest.spyOn(electronService, 'removeAllListeners');
|
||||
component.ngOnDestroy();
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import { ERROR, PLAYLIST_PARSE_RESPONSE } from '../../../shared/ipc-commands';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { getFilenameFromUrl } from '../../../shared/playlist.utils';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { addPlaylist, parsePlaylist } from '../state/actions';
|
||||
import { addPlaylist } from '../state/actions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -35,7 +29,10 @@ export class HomeComponent {
|
||||
},
|
||||
{
|
||||
id: ERROR,
|
||||
execute: () => (this.isLoading = false),
|
||||
execute: (error: { message: string; status: string }) => {
|
||||
//this.isLoading = false;
|
||||
this.showNotification('Error2: ' + error.message);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -45,8 +42,7 @@ export class HomeComponent {
|
||||
private dataService: DataService,
|
||||
private ngZone: NgZone,
|
||||
private snackBar: MatSnackBar,
|
||||
private readonly store: Store,
|
||||
private translateService: TranslateService
|
||||
private readonly store: Store
|
||||
) {
|
||||
this.setRendererListeners();
|
||||
}
|
||||
@@ -72,65 +68,6 @@ export class HomeComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the filename of rejected file
|
||||
* @param filename name of the uploaded file
|
||||
*/
|
||||
rejectFile(filename: string): void {
|
||||
this.showNotification(
|
||||
this.translateService.instant('HOME.FILE_UPLOAD.REJECTED', {
|
||||
filename,
|
||||
})
|
||||
);
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and store uploaded playlist
|
||||
* @param payload
|
||||
*/
|
||||
handlePlaylist(payload: { uploadEvent: Event; file: File }): void {
|
||||
this.isLoading = true;
|
||||
const playlist = (payload.uploadEvent.target as FileReader)
|
||||
.result as string;
|
||||
|
||||
this.store.dispatch(
|
||||
parsePlaylist({
|
||||
uploadType: 'FILE',
|
||||
playlist,
|
||||
title: payload.file.name,
|
||||
path: payload.file.path,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends url of the playlist to the renderer process
|
||||
* @param playlistUrl url of the added playlist
|
||||
*/
|
||||
sendPlaylistsUrl(playlistUrl: string): void {
|
||||
this.isLoading = true;
|
||||
this.dataService.sendIpcEvent(PLAYLIST_PARSE_BY_URL, {
|
||||
title: getFilenameFromUrl(playlistUrl),
|
||||
url: playlistUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends IPC event to the renderer process to parse playlist
|
||||
* @param text playlist as string
|
||||
*/
|
||||
uploadAsText(playlist: string): void {
|
||||
this.isLoading = true;
|
||||
this.store.dispatch(
|
||||
parsePlaylist({
|
||||
uploadType: 'TEXT',
|
||||
playlist,
|
||||
title: 'Imported as text',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows snack bar notification with a given message
|
||||
* @param message message to show
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { DragDropFileUploadDirective } from './file-upload/drag-drop-file-upload.directive';
|
||||
import { HomeComponent } from './home.component';
|
||||
import { HomeRoutingModule } from './home.routing';
|
||||
import { StalkerPortalImportComponent } from './stalker-portal-import/stalker-portal-import.component';
|
||||
import { XtreamCodeImportComponent } from './xtream-code-import/xtream-code-import.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
HomeRoutingModule,
|
||||
SharedModule,
|
||||
XtreamCodeImportComponent,
|
||||
StalkerPortalImportComponent,
|
||||
],
|
||||
imports: [CommonModule, HeaderComponent, HomeRoutingModule, SharedModule],
|
||||
declarations: [DragDropFileUploadDirective, HomeComponent],
|
||||
})
|
||||
export class HomeModule {}
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
} from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { Actions } from '@ngrx/effects';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -27,29 +28,30 @@ describe('PlaylistInfoComponent', () => {
|
||||
let mockStore: MockStore;
|
||||
const actions$ = new Observable<Actions>();
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
MockModule(MatDialogModule),
|
||||
MockModule(MatCheckboxModule),
|
||||
MockModule(MatFormFieldModule),
|
||||
MockModule(TranslateModule),
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
declarations: [PlaylistInfoComponent, MockPipe(DatePipe)],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
UntypedFormBuilder,
|
||||
provideMockStore(),
|
||||
provideMockActions(actions$),
|
||||
MockProvider(NgxIndexedDBService),
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
);
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
MockModule(MatDialogModule),
|
||||
MockModule(MatCheckboxModule),
|
||||
MockModule(MatFormFieldModule),
|
||||
MockModule(TranslateModule),
|
||||
MockModule(MatSnackBarModule),
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
declarations: [PlaylistInfoComponent, MockPipe(DatePipe)],
|
||||
providers: [
|
||||
MockProvider(MatSnackBar),
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: DataService, useClass: ElectronServiceStub },
|
||||
UntypedFormBuilder,
|
||||
provideMockStore(),
|
||||
provideMockActions(actions$),
|
||||
MockProvider(NgxIndexedDBService),
|
||||
MockProvider(TranslateService),
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PlaylistInfoComponent);
|
||||
|
||||
@@ -56,8 +56,12 @@
|
||||
>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #xtreamPlaylist
|
||||
>Xtream/Stalker Playlist
|
||||
<ng-template #xtreamPlaylist>
|
||||
@if (item?.macAddress) {
|
||||
Stalker Portal
|
||||
} @else {
|
||||
Xtream Code Playlist
|
||||
}
|
||||
<ng-container *ngIf="item.importDate?.length > 3">
|
||||
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
||||
{{ item.importDate | date }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="allPlaylistsLoaded$ | async; else skeletonView">
|
||||
@if (allPlaylistsLoaded()) {
|
||||
<mat-form-field
|
||||
class="full-width"
|
||||
subscriptSizing="dynamic"
|
||||
@@ -11,98 +11,105 @@
|
||||
[placeholder]="'HOME.PLAYLISTS.SEARCH_PLAYLISTS' | translate"
|
||||
(input)="onSearchQueryUpdate(searchQuery.value)"
|
||||
/></mat-form-field>
|
||||
|
||||
<mat-nav-list
|
||||
cdkDropList
|
||||
(cdkDropListDropped)="drop($event, playlists)"
|
||||
*ngIf="playlists$ | async as playlists"
|
||||
class="playlists-list"
|
||||
>
|
||||
<mat-list-item *ngIf="playlists.length === 0; else playlistsTemplate">
|
||||
<mat-icon class="favorites-icon" matListItemAvatar>search</mat-icon>
|
||||
<div matListItemTitle>
|
||||
{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}
|
||||
</div>
|
||||
<div matListItemLine class="meta">
|
||||
{{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
|
||||
<ng-template #playlistsTemplate>
|
||||
<mat-list-item (click)="getGlobalFavorites()">
|
||||
@defer {
|
||||
<mat-nav-list
|
||||
cdkDropList
|
||||
(cdkDropListDropped)="drop($event, playlists)"
|
||||
*ngIf="playlists$ | async as playlists"
|
||||
class="playlists-list"
|
||||
>
|
||||
<mat-list-item
|
||||
*ngIf="playlists.length === 0; else playlistsTemplate"
|
||||
>
|
||||
<mat-icon class="favorites-icon" matListItemAvatar
|
||||
>star</mat-icon
|
||||
>search</mat-icon
|
||||
>
|
||||
<div matListItemTitle>
|
||||
{{ 'HOME.PLAYLISTS.GLOBAL_FAVORITES' | translate }}
|
||||
{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}
|
||||
</div>
|
||||
<div matListItemLine class="meta">
|
||||
{{
|
||||
'HOME.PLAYLISTS.GLOBAL_FAVORITES_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
{{ 'HOME.PLAYLISTS.ADD_FIRST' | translate }}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<app-playlist-item
|
||||
*ngFor="let item of playlists; last as last; trackBy: trackByFn"
|
||||
[item]="item"
|
||||
[showActions]="!sidebarMode"
|
||||
(editPlaylistClicked)="openInfoDialog($event)"
|
||||
(playlistClicked)="getPlaylist(item)"
|
||||
(refreshClicked)="refreshPlaylist($event)"
|
||||
(removeClicked)="removeClicked($event)"
|
||||
></app-playlist-item>
|
||||
<ng-template #playlistsTemplate>
|
||||
<mat-list-item (click)="getGlobalFavorites()">
|
||||
<mat-icon class="favorites-icon" matListItemAvatar
|
||||
>star</mat-icon
|
||||
>
|
||||
<div matListItemTitle>
|
||||
{{ 'HOME.PLAYLISTS.GLOBAL_FAVORITES' | translate }}
|
||||
</div>
|
||||
<div matListItemLine class="meta">
|
||||
{{
|
||||
'HOME.PLAYLISTS.GLOBAL_FAVORITES_DESCRIPTION'
|
||||
| translate
|
||||
}}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div *ngIf="isMigrationPossible" id="migration-container">
|
||||
<p>{{ migrationMessage }}</p>
|
||||
<button
|
||||
mat-button
|
||||
(click)="migratePlaylists()"
|
||||
class="migration-btn"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
Migrate playlists</button
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
(click)="deleteMigratedPlaylists()"
|
||||
class="migration-btn"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
{{ 'SETTINGS.REMOVE_DIALOG.TITLE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-nav-list>
|
||||
</ng-container>
|
||||
<app-playlist-item
|
||||
*ngFor="
|
||||
let item of playlists;
|
||||
last as last;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
[item]="item"
|
||||
[showActions]="!sidebarMode"
|
||||
(editPlaylistClicked)="openInfoDialog($event)"
|
||||
(playlistClicked)="getPlaylist(item)"
|
||||
(refreshClicked)="refreshPlaylist($event)"
|
||||
(removeClicked)="removeClicked($event)"
|
||||
></app-playlist-item>
|
||||
|
||||
<ng-template #skeletonView>
|
||||
<div id="skeleton-container">
|
||||
<div class="skeleton-item" *ngFor="let a of [].constructor(10)">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
appearance="circle"
|
||||
></ngx-skeleton-loader>
|
||||
<div class="text">
|
||||
<div *ngIf="isMigrationPossible" id="migration-container">
|
||||
<p>{{ migrationMessage }}</p>
|
||||
<button
|
||||
mat-button
|
||||
(click)="migratePlaylists()"
|
||||
class="migration-btn"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
Migrate playlists</button
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
(click)="deleteMigratedPlaylists()"
|
||||
class="migration-btn"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
{{ 'SETTINGS.REMOVE_DIALOG.TITLE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-nav-list>
|
||||
} @placeholder {
|
||||
<div id="skeleton-container">
|
||||
<div class="skeleton-item" *ngFor="let a of [].constructor(10)">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '30%', height: '20px' }"
|
||||
></ngx-skeleton-loader>
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '50%', height: '10px' }"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ngx-skeleton-loader
|
||||
count="2"
|
||||
appearance="circle"
|
||||
></ngx-skeleton-loader>
|
||||
<div class="text">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '30%', height: '20px' }"
|
||||
></ngx-skeleton-loader>
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '50%', height: '10px' }"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ngx-skeleton-loader
|
||||
count="2"
|
||||
appearance="circle"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
)
|
||||
);
|
||||
|
||||
allPlaylistsLoaded$ = this.store.select(selectPlaylistsLoadingFlag);
|
||||
allPlaylistsLoaded = this.store.selectSignal(selectPlaylistsLoadingFlag);
|
||||
|
||||
@Input() sidebarMode = false;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { TranslateModule, TranslatePipe } from '@ngx-translate/core';
|
||||
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { TextImportComponent } from './text-import.component';
|
||||
@@ -10,18 +10,17 @@ describe('TextImportComponent', () => {
|
||||
let component: TextImportComponent;
|
||||
let fixture: ComponentFixture<TextImportComponent>;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TextImportComponent, MockPipe(TranslatePipe)],
|
||||
imports: [
|
||||
MockModule(MatInputModule),
|
||||
MockModule(FormsModule),
|
||||
MockModule(ReactiveFormsModule),
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
);
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TextImportComponent, MockPipe(TranslatePipe)],
|
||||
imports: [
|
||||
MockModule(MatInputModule),
|
||||
MockModule(FormsModule),
|
||||
MockModule(ReactiveFormsModule),
|
||||
MockModule(TranslateModule),
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TextImportComponent);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MatCardModule } from '@angular/material/card';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { TranslateModule, TranslatePipe } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import { ElectronServiceStub } from '../../services/electron.service.stub';
|
||||
@@ -14,26 +14,25 @@ describe('UrlUploadComponent', () => {
|
||||
let component: UrlUploadComponent;
|
||||
let fixture: ComponentFixture<UrlUploadComponent>;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [UrlUploadComponent, MockPipe(TranslatePipe)],
|
||||
imports: [
|
||||
MockModule(MatInputModule),
|
||||
MockModule(MatCardModule),
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: DataService,
|
||||
useClass: ElectronServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
);
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [UrlUploadComponent, MockPipe(TranslatePipe)],
|
||||
imports: [
|
||||
MockModule(MatInputModule),
|
||||
MockModule(MatCardModule),
|
||||
MockModule(TranslateModule),
|
||||
MockModule(FormsModule),
|
||||
MockModule(ReactiveFormsModule),
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: DataService,
|
||||
useClass: ElectronServiceStub,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UrlUploadComponent);
|
||||
@@ -45,31 +44,27 @@ describe('UrlUploadComponent', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it(
|
||||
'submit form with playlist url',
|
||||
waitForAsync(() => {
|
||||
jest.spyOn(component.urlAdded, 'emit');
|
||||
const TEST_URL = 'http://example.org/playlist.m3u';
|
||||
const submitButton =
|
||||
fixture.debugElement.nativeElement.querySelector('button');
|
||||
it('submit form with playlist url', waitForAsync(() => {
|
||||
jest.spyOn(component.urlAdded, 'emit');
|
||||
const TEST_URL = 'http://example.org/playlist.m3u';
|
||||
const submitButton =
|
||||
fixture.debugElement.nativeElement.querySelector('button');
|
||||
|
||||
// test input field validation
|
||||
expect(submitButton.disabled).toBeTruthy();
|
||||
component.form.setValue({ playlistUrl: 'wrong url here' });
|
||||
fixture.detectChanges();
|
||||
expect(submitButton.disabled).toBeTruthy();
|
||||
component.form.setValue({ playlistUrl: TEST_URL + '8' });
|
||||
fixture.detectChanges();
|
||||
expect(submitButton.disabled).toBeFalsy();
|
||||
component.form.setValue({ playlistUrl: TEST_URL });
|
||||
fixture.detectChanges();
|
||||
expect(submitButton.disabled).toBeFalsy();
|
||||
// test input field validation
|
||||
expect(submitButton.disabled).toBeTruthy();
|
||||
component.form.setValue({ playlistUrl: 'wrong url here' });
|
||||
fixture.detectChanges();
|
||||
expect(submitButton.disabled).toBeTruthy();
|
||||
component.form.setValue({ playlistUrl: TEST_URL + '8' });
|
||||
fixture.detectChanges();
|
||||
expect(submitButton.disabled).toBeFalsy();
|
||||
component.form.setValue({ playlistUrl: TEST_URL });
|
||||
fixture.detectChanges();
|
||||
expect(submitButton.disabled).toBeFalsy();
|
||||
|
||||
const form = fixture.debugElement.query(By.css('form'));
|
||||
form.triggerEventHandler('ngSubmit', null);
|
||||
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);
|
||||
})
|
||||
);
|
||||
expect(component.urlAdded.emit).toHaveBeenCalledWith(TEST_URL);
|
||||
}));
|
||||
});
|
||||
+130
-120
@@ -1,108 +1,27 @@
|
||||
<mat-tab-group color="primary" backgroundColor="primary">
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">list</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.ALL_CHANNELS' | translate }}
|
||||
@if (_channelList) {
|
||||
<mat-tab-group color="primary" backgroundColor="primary">
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">list</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.ALL_CHANNELS' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="search-panel">
|
||||
<input
|
||||
class="full-width search-bar"
|
||||
type="search"
|
||||
#search
|
||||
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
|
||||
[(ngModel)]="searchTerm.name"
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="search-panel">
|
||||
<input
|
||||
class="full-width search-bar"
|
||||
type="search"
|
||||
#search
|
||||
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
|
||||
[(ngModel)]="searchTerm.name"
|
||||
/>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-nav-list>
|
||||
<app-channel-list-item
|
||||
*ngFor="
|
||||
let channel of _channelList
|
||||
| filterBy: searchTerm.name : 'name';
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name || 'CHANNELS.UNNAMED_CHANNEL' | translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel?.id"
|
||||
></app-channel-list-item>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">category</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.GROUPS' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<mat-nav-list id="groups-list">
|
||||
<mat-accordion multi>
|
||||
<ng-container *ngFor="let groups of groupedChannels | keyvalue">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header
|
||||
*ngIf="groups.value.length > 0"
|
||||
>
|
||||
{{
|
||||
groups.key || ('CHANNELS.UNGROUPED' | translate)
|
||||
| titlecase
|
||||
}}
|
||||
({{ groups.value.length }})
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let channel of groups.value;
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
>
|
||||
<app-channel-list-item
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name ||
|
||||
'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel.id"
|
||||
></app-channel-list-item>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</ng-container>
|
||||
</mat-accordion>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab *ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'">
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">star</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.FAVORITES' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<mat-nav-list
|
||||
cdkDropList
|
||||
*ngIf="favorites$ | async as favorites"
|
||||
(cdkDropListDropped)="drop($event, favorites)"
|
||||
id="favorites-list"
|
||||
>
|
||||
<ng-container *ngIf="favorites.length > 0; else noFavorites">
|
||||
<mat-divider></mat-divider>
|
||||
<mat-nav-list>
|
||||
<app-channel-list-item
|
||||
*ngFor="
|
||||
let channel of favorites;
|
||||
let channel of _channelList
|
||||
| filterBy: searchTerm.name : 'name';
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
@@ -113,26 +32,117 @@
|
||||
(channel?.name || 'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[isDraggable]="true"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel?.id"
|
||||
[showFavoriteButton]="true"
|
||||
(favoriteToggled)="toggleFavoriteChannel(channel, $event)"
|
||||
></app-channel-list-item>
|
||||
</ng-container>
|
||||
<ng-template #noFavorites>
|
||||
<mat-list-item
|
||||
><strong>{{
|
||||
'CHANNELS.NO_FAVORITES' | translate
|
||||
}}</strong></mat-list-item
|
||||
>
|
||||
<mat-list-item>
|
||||
{{
|
||||
'CHANNELS.USE_STAR_TO_FAVORITE' | translate
|
||||
}}</mat-list-item
|
||||
>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">category</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.GROUPS' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<mat-nav-list id="groups-list">
|
||||
<mat-accordion multi>
|
||||
<ng-container
|
||||
*ngFor="let groups of groupedChannels | keyvalue"
|
||||
>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header
|
||||
*ngIf="groups.value.length > 0"
|
||||
>
|
||||
{{
|
||||
groups.key ||
|
||||
('CHANNELS.UNGROUPED' | translate)
|
||||
| titlecase
|
||||
}}
|
||||
({{ groups.value.length }})
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<ng-container
|
||||
*ngFor="
|
||||
let channel of groups.value;
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
>
|
||||
<app-channel-list-item
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name ||
|
||||
'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel.id"
|
||||
></app-channel-list-item>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</ng-container>
|
||||
</mat-accordion>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab *ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'">
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">star</mat-icon>
|
||||
<div class="tab-title">
|
||||
{{ 'CHANNELS.FAVORITES' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<mat-nav-list
|
||||
cdkDropList
|
||||
*ngIf="favorites$ | async as favorites"
|
||||
(cdkDropListDropped)="drop($event, favorites)"
|
||||
id="favorites-list"
|
||||
>
|
||||
<ng-container *ngIf="favorites.length > 0; else noFavorites">
|
||||
<app-channel-list-item
|
||||
*ngFor="
|
||||
let channel of favorites;
|
||||
index as i;
|
||||
trackBy: trackByFn
|
||||
"
|
||||
[name]="
|
||||
i +
|
||||
1 +
|
||||
'. ' +
|
||||
(channel?.name || 'CHANNELS.UNNAMED_CHANNEL'
|
||||
| translate)
|
||||
"
|
||||
[isDraggable]="true"
|
||||
[logo]="channel?.tvg?.logo"
|
||||
(clicked)="selectChannel(channel)"
|
||||
[selected]="selected?.id === channel?.id"
|
||||
[showFavoriteButton]="true"
|
||||
(favoriteToggled)="
|
||||
toggleFavoriteChannel(channel, $event)
|
||||
"
|
||||
></app-channel-list-item>
|
||||
</ng-container>
|
||||
<ng-template #noFavorites>
|
||||
<mat-list-item
|
||||
><strong>{{
|
||||
'CHANNELS.NO_FAVORITES' | translate
|
||||
}}</strong></mat-list-item
|
||||
>
|
||||
<mat-list-item>
|
||||
{{
|
||||
'CHANNELS.USE_STAR_TO_FAVORITE' | translate
|
||||
}}</mat-list-item
|
||||
>
|
||||
</ng-template>
|
||||
</mat-nav-list>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
} @else {
|
||||
No channels there
|
||||
}
|
||||
+11
-6
@@ -70,17 +70,18 @@ export class ChannelListContainerComponent {
|
||||
);
|
||||
|
||||
/** List with favorites */
|
||||
favorites$ = this.store
|
||||
.select(selectFavorites)
|
||||
.pipe(
|
||||
map((favoriteChannelIds) =>
|
||||
favorites$ = this.store.select(selectFavorites).pipe(
|
||||
map(
|
||||
(
|
||||
favoriteChannelIds // TODO: move to selector
|
||||
) =>
|
||||
favoriteChannelIds.map((favoriteChannelId) =>
|
||||
this.channelList.find(
|
||||
(channel) => channel.id === favoriteChannelId
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
@@ -129,4 +130,8 @@ export class ChannelListContainerComponent {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.store.dispatch(PlaylistActions.setChannels({ channels: [] }));
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="channels-count">
|
||||
{{
|
||||
sidebarView === 'CHANNELS'
|
||||
? channels.length +
|
||||
? channels?.length +
|
||||
' ' +
|
||||
('HOME.PLAYLISTS.CHANNELS' | translate)
|
||||
: ('HOME.PLAYLISTS.MY_PLAYLISTS_SUBTITLE' | translate)
|
||||
@@ -38,16 +38,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<app-channel-list-container
|
||||
*ngIf="sidebarView === 'CHANNELS'; else playlistsTemplate"
|
||||
[channelList]="channels"
|
||||
>
|
||||
</app-channel-list-container>
|
||||
<ng-template #playlistsTemplate>
|
||||
@if (sidebarView === 'CHANNELS') {
|
||||
<app-channel-list-container [channelList]="channels" />
|
||||
} @else {
|
||||
<app-recent-playlists
|
||||
class="recent-playlists"
|
||||
[sidebarMode]="true"
|
||||
[class.electron]="dataService.isElectron"
|
||||
(playlistClicked)="selectPlaylist()"
|
||||
></app-recent-playlists>
|
||||
</ng-template>
|
||||
/>
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
<mat-drawer-container class="main-container">
|
||||
<!-- sidebar content -->
|
||||
<mat-drawer #drawer mode="side" opened disableClose>
|
||||
<app-sidebar
|
||||
*ngIf="channels$ | async as channels; else noChannels"
|
||||
[channels]="channels"
|
||||
/>
|
||||
<ng-template #noChannels
|
||||
>No channels <br />
|
||||
<button mat-button color="secondary" (click)="navigateHome()">
|
||||
Go back
|
||||
</button></ng-template
|
||||
>
|
||||
@defer {
|
||||
<app-sidebar [channels]="channels$ | async" />
|
||||
} @loading {
|
||||
<p>Loading...</p>
|
||||
} @placeholder {
|
||||
<p>Loading...</p>
|
||||
}
|
||||
</mat-drawer>
|
||||
<mat-drawer-content>
|
||||
<ng-container *ngIf="activeChannel$ | async as activeChannel">
|
||||
|
||||
@@ -4,12 +4,21 @@ import {
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { take } from 'rxjs';
|
||||
import * as semver from 'semver';
|
||||
import {
|
||||
@@ -22,8 +31,8 @@ import { DataService } from '../services/data.service';
|
||||
import { DialogService } from '../services/dialog.service';
|
||||
import { EpgService } from '../services/epg.service';
|
||||
import { PlaylistsService } from '../services/playlists.service';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
import { STORE_KEY } from '../shared/enums/store-keys.enum';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { selectIsEpgAvailable } from '../state/selectors';
|
||||
import { SettingsService } from './../services/settings.service';
|
||||
@@ -35,7 +44,20 @@ import { Theme } from './theme.enum';
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
standalone: true,
|
||||
imports: [CommonModule, SharedModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
HeaderComponent,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatTooltipModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
/** List with available languages as enum */
|
||||
|
||||
@@ -6,14 +6,82 @@
|
||||
<div class="subtitle">{{ subtitle }}</div>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="add-playlist-btn"
|
||||
[matMenuTriggerFor]="addPlaylistMenu"
|
||||
[matTooltip]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate"
|
||||
data-test-id="add-playlist"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
<!-- <button
|
||||
mat-icon-button
|
||||
[matTooltip]="'HOME.FILTER_BY_NAME' | translate"
|
||||
data-test-id="filter-playlist-by-name"
|
||||
>
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="'HOME.FILTER_BY_TYPE' | translate"
|
||||
data-test-id="filter-playlist-by-type"
|
||||
>
|
||||
<mat-icon>filter_list</mat-icon>
|
||||
</button> -->
|
||||
<button
|
||||
*ngIf="isElectron; else pwaMenu"
|
||||
mat-icon-button
|
||||
(click)="openSettings()"
|
||||
[matTooltip]="'MENU.SETTINGS' | translate"
|
||||
data-test-id="open-settings"
|
||||
>
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
<mat-menu #addPlaylistMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.FILE_UPLOAD' | translate"
|
||||
(click)="opedAddPlaylistDialog('file')"
|
||||
>
|
||||
<mat-icon>attach_file</mat-icon>
|
||||
<span>{{ 'HOME.TABS.FILE_UPLOAD' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.URL_UPLOAD' | translate"
|
||||
(click)="opedAddPlaylistDialog('url')"
|
||||
>
|
||||
<mat-icon>link</mat-icon>
|
||||
<span>{{ 'HOME.TABS.URL_UPLOAD' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.TEXT_IMPORT' | translate"
|
||||
(click)="opedAddPlaylistDialog('text')"
|
||||
>
|
||||
<mat-icon>text_format</mat-icon>
|
||||
<span>{{ 'HOME.TABS.TEXT_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.XTREME_IMPORT' | translate"
|
||||
(click)="opedAddPlaylistDialog('xtream')"
|
||||
>
|
||||
<mat-icon>video_library</mat-icon>
|
||||
<span>{{ 'HOME.TABS.XTREME_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[attr.aria-label]="'HOME.TABS.STALKER_PORTAL_IMPORT' | translate"
|
||||
(click)="opedAddPlaylistDialog('stalker')"
|
||||
>
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
<span>{{ 'HOME.TABS.STALKER_PORTAL_IMPORT' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<ng-template #pwaMenu>
|
||||
|
||||
@@ -32,7 +32,15 @@
|
||||
.menu {
|
||||
place-content: center;
|
||||
align-self: center;
|
||||
min-width: 50px;
|
||||
max-width: 50px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.add-playlist-btn {
|
||||
background-color: #3f51b5;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.main {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,40 @@
|
||||
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgxWhatsNewModule } from 'ngx-whats-new';
|
||||
import { HomeComponent } from '../../../home/home.component';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { WhatsNewService } from '../../../services/whats-new.service';
|
||||
import { AboutDialogComponent } from '../about-dialog/about-dialog.component';
|
||||
import {
|
||||
AddPlaylistDialogComponent,
|
||||
PlaylistType,
|
||||
} from '../add-playlist/add-playlist-dialog.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
NgFor,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
NgxWhatsNewModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
/** Title of the header */
|
||||
@@ -82,4 +107,14 @@ export class HeaderComponent implements OnInit {
|
||||
data: this.dataService.getAppVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
opedAddPlaylistDialog(type: PlaylistType) {
|
||||
this.dialog.open<AddPlaylistDialogComponent, { type: PlaylistType }>(
|
||||
AddPlaylistDialogComponent,
|
||||
{
|
||||
width: '600px',
|
||||
data: { type },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,11 @@ 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 { FilterPipe } from './pipes/filter.pipe';
|
||||
import { MomentDatePipe } from './pipes/moment-date.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
HeaderComponent,
|
||||
MomentDatePipe,
|
||||
RecentPlaylistsComponent,
|
||||
PlaylistItemComponent,
|
||||
@@ -37,7 +35,6 @@ import { MomentDatePipe } from './pipes/moment-date.pipe';
|
||||
DragDropModule,
|
||||
FilterPipe,
|
||||
FormsModule,
|
||||
HeaderComponent,
|
||||
MaterialModule,
|
||||
NgxWhatsNewModule,
|
||||
MomentDatePipe,
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ textarea,
|
||||
}
|
||||
|
||||
.recent-playlists {
|
||||
height: calc(100vh - 152px - 28px) !important;
|
||||
height: calc(100vh - 106px - 28px) !important;
|
||||
}
|
||||
|
||||
#groups-list,
|
||||
|
||||
Reference in new issue
Block a user