Files
iptvnator/src/app/settings/settings.component.ts
T
4gray e9a698ab48 feat(settings): refactor settings management and add store service
Refactor the settings management in the SettingsComponent to utilize a new
SettingsStore service for better state management. Remove direct local
storage access and replace it with a method to retrieve settings from the
store. Update the backToHome method to handle dialog navigation. Add
error logging for file reading issues and introduce an input property to
determine dialog context.
2024-11-28 23:51:03 +01:00

406 lines
12 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-base-to-string */
import { CommonModule } from '@angular/common';
import { Component, Inject, Input, OnInit, Optional } from '@angular/core';
import {
FormArray,
FormBuilder,
FormControl,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogModule,
} from '@angular/material/dialog';
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 { TranslateModule, TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs';
import * as semver from 'semver';
import {
SET_MPV_PLAYER_PATH,
SET_VLC_PLAYER_PATH,
SETTINGS_UPDATE,
} from '../../../shared/ipc-commands';
import { Playlist } from '../../../shared/playlist.interface';
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 { SettingsStore } from '../services/settings-store.service';
import { HeaderComponent } from '../shared/components/header/header.component';
import * as PlaylistActions from '../state/actions';
import { selectIsEpgAvailable } from '../state/selectors';
import { SettingsService } from './../services/settings.service';
import { Language } from './language.enum';
import { VideoPlayer } from './settings.interface';
import { Theme } from './theme.enum';
@Component({
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
standalone: true,
imports: [
CommonModule,
FormsModule,
HeaderComponent,
MatButtonModule,
MatCheckboxModule,
MatDividerModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatTooltipModule,
ReactiveFormsModule,
TranslateModule,
MatDialogModule,
],
})
export class SettingsComponent implements OnInit {
@Input() isDialog = false;
/** List with available languages as enum */
languageEnum = Language;
/** Flag that indicates whether the app runs in electron environment */
isTauri = this.electronService.getAppEnvironment() === 'tauri';
isPwa = this.electronService.getAppEnvironment() === 'pwa';
electronPlayers = [
{
id: VideoPlayer.MPV,
label: 'MPV Player',
},
{
id: VideoPlayer.VLC,
label: 'VLC',
},
];
/** Player options */
players = [
{
id: VideoPlayer.Html5Player,
label: 'HTML5 Video Player',
},
{
id: VideoPlayer.VideoJs,
label: 'VideoJs Player',
},
...this.electronPlayers,
/* ...(this.isTauri ? this.electronPlayers : []), */
];
/** Current version of the app */
version: string;
/** Update message to show */
updateMessage: string;
/** EPG availability flag */
epgAvailable$ = this.store.select(selectIsEpgAvailable);
/** All available visual themes */
themeEnum = Theme;
/** Settings form object */
settingsForm = this.formBuilder.group({
player: [VideoPlayer.VideoJs],
...(this.isTauri ? { epgUrl: new FormArray([]) } : {}),
language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme,
mpvPlayerPath: '',
vlcPlayerPath: '',
remoteControl: false,
remoteControlPort: 3000,
});
/** Form array with epg sources */
epgUrl = this.settingsForm.get('epgUrl') as FormArray;
/**
* Creates an instance of SettingsComponent and injects
* required dependencies into the component
*/
constructor(
private dialogService: DialogService,
public electronService: DataService,
private epgService: EpgService,
private formBuilder: FormBuilder,
private playlistsService: PlaylistsService,
private router: Router,
private settingsService: SettingsService,
private snackBar: MatSnackBar,
private store: Store,
private translate: TranslateService,
private matDialog: MatDialog,
private settingsStore: SettingsStore,
@Optional() @Inject(MAT_DIALOG_DATA) data?: { isDialog: boolean }
) {
this.isDialog = data?.isDialog ?? false;
}
/**
* Reads the config object from the browsers
* storage (indexed db)
*/
ngOnInit(): void {
this.setSettings();
this.checkAppVersion();
}
/**
* Sets saved settings from the indexed db store
*/
setSettings(): void {
const currentSettings = this.settingsStore.getSettings()();
this.settingsForm.patchValue(currentSettings);
if (this.isTauri && currentSettings.epgUrl) {
this.setEpgUrls(currentSettings.epgUrl);
}
}
/**
* Sets the epg urls to the form array
* @param epgUrls urls of the EPG sources
*/
setEpgUrls(epgUrls: string[] | string): void {
const URL_REGEX = /^(http|https|file):\/\/[^ "]+$/;
const urls = Array.isArray(epgUrls) ? epgUrls : [epgUrls];
const filteredUrls = urls.filter((url) => url !== '');
filteredUrls.forEach((url) => {
this.epgUrl.push(
new FormControl(url, [Validators.pattern(URL_REGEX)])
);
});
}
/**
* Checks whether the latest version of the application
* is used and updates the version message in the
* settings UI
*/
checkAppVersion(): void {
this.settingsService
.getAppVersion()
.pipe(take(1))
.subscribe((version) => this.showVersionInformation(version));
}
/**
* Updates the message in settings UI about the used
* version of the app
* @param currentVersion current version of the application
*/
showVersionInformation(currentVersion: string): void {
const isOutdated = this.isCurrentVersionOutdated(currentVersion);
if (isOutdated) {
this.updateMessage = `${
this.translate.instant(
'SETTINGS.NEW_VERSION_AVAILABLE'
) as string
}: ${currentVersion}`;
} else {
this.updateMessage = this.translate.instant(
'SETTINGS.LATEST_VERSION'
);
}
}
/**
* Compares actual with latest version of the
* application
* @param latestVersion latest version
* @returns returns true if an update is available
*/
isCurrentVersionOutdated(latestVersion: string): boolean {
this.version = this.electronService.getAppVersion();
return semver.lt(this.version, latestVersion);
}
/**
* Triggers on form submit and saves the config object to
* the indexed db store
*/
onSubmit(): void {
this.settingsStore.updateSettings(this.settingsForm.value).then(() => {
this.applyChangedSettings();
this.electronService.sendIpcEvent(
SETTINGS_UPDATE,
this.settingsForm.value
);
this.electronService.sendIpcEvent(
SET_MPV_PLAYER_PATH,
this.settingsForm.value.mpvPlayerPath
);
this.electronService.sendIpcEvent(
SET_VLC_PLAYER_PATH,
this.settingsForm.value.mpvPlayerPath
);
});
if (this.isDialog) {
this.matDialog.closeAll();
}
}
/**
* Applies the changed settings to the app
*/
applyChangedSettings(): void {
this.settingsForm.markAsPristine();
if (this.isTauri) {
let epgUrls = this.settingsForm.value.epgUrl;
if (epgUrls) {
if (!Array.isArray(epgUrls)) {
epgUrls = [epgUrls];
}
epgUrls = epgUrls.filter((url) => url !== '');
if (epgUrls.length > 0) {
// Fetch all EPG URLs at once
this.epgService.fetchEpg(epgUrls);
}
}
}
this.translate.use(this.settingsForm.value.language);
this.settingsService.changeTheme(this.settingsForm.value.theme);
this.snackBar.open(
this.translate.instant('SETTINGS.SETTINGS_SAVED'),
null,
{
duration: 2000,
horizontalPosition: 'start',
}
);
}
/**
* Navigates back to the applications homepage
*/
backToHome(): void {
if (this.isDialog) {
this.matDialog.closeAll();
} else {
this.router.navigateByUrl('/');
}
}
/**
* Fetches and updates EPG from the given URL
* @param url epg source url
*/
refreshEpg(url: string): void {
this.epgService.fetchEpg([url]);
}
/**
* Initializes new entry in form array for EPG URL
*/
addEpgSource(): void {
this.epgUrl.insert(this.epgUrl.length, new FormControl(''));
}
/**
* Removes entry from form array for EPG URL
* @param index index of the item to remove
*/
removeEpgSource(index: number): void {
this.epgUrl.removeAt(index);
this.settingsForm.markAsDirty();
}
exportData() {
this.playlistsService
.getAllData()
.pipe(take(1))
.subscribe((data) => {
const blob = new Blob([JSON.stringify(data)], {
type: 'text/plain',
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'playlists.json';
link.click();
window.URL.revokeObjectURL(url);
});
}
importData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.addEventListener('change', (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
const contents = reader.result;
try {
const parsedPlaylists: Playlist[] = JSON.parse(
contents.toString()
);
if (!Array.isArray(parsedPlaylists)) {
this.snackBar.open(
this.translate.instant('SETTINGS.IMPORT_ERROR'),
null,
{
duration: 2000,
}
);
} else {
this.store.dispatch(
PlaylistActions.addManyPlaylists({
playlists: parsedPlaylists,
})
);
}
} catch (error) {
this.snackBar.open(
this.translate.instant('SETTINGS.IMPORT_ERROR'),
null,
{
duration: 2000,
}
);
console.error(error);
}
};
reader.readAsText(file);
}
});
input.click();
}
removeAll() {
this.dialogService.openConfirmDialog({
title: this.translate.instant('SETTINGS.REMOVE_DIALOG.TITLE'),
message: this.translate.instant('SETTINGS.REMOVE_DIALOG.MESSAGE'),
onConfirm: (): void =>
this.store.dispatch(PlaylistActions.removeAllPlaylists()),
});
}
}