refactor: delete all playlists feature

- Updated confirmation dialog messages to provide detailed consequences of deleting playlists.
- Added backup hints and progress messages for better user experience during playlist deletion.
- Ensured consistency in language across all supported translations (de, el, en, es, fr, it, ja, ko, nl, pl, pt, ru, tr, zh, zhtw).

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-21 20:09:05 +02:00
1 parent d7114c1580
commit fc3db71e85
27 files changed
+1140 -233

No files matched your search

+138 -25
View File
@@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import {
Component,
computed,
ElementRef,
effect,
inject,
@@ -39,9 +40,15 @@ import { EpgService } from '@iptvnator/epg/data-access';
import { EpgSourceStatusComponent } from '@iptvnator/ui/epg';
import { QRCodeComponent } from 'angularx-qrcode';
import { DialogService } from 'components';
import { PlaylistActions, selectIsEpgAvailable } from 'm3u-state';
import {
PlaylistActions,
selectAllPlaylistsMeta,
selectIsEpgAvailable,
} from 'm3u-state';
import { firstValueFrom, take } from 'rxjs';
import {
DatabaseService,
DbOperationEvent,
DataService,
PlaylistBackupImportSummary,
PlaylistBackupService,
@@ -56,6 +63,10 @@ import {
} from 'shared-interfaces';
import { SettingsContextService } from '@iptvnator/workspace/shell/util';
import { SettingsStore } from '../services/settings-store.service';
import {
SettingsDeleteAllPlaylistsDialogComponent,
SettingsDeleteAllPlaylistsDialogData,
} from './settings-delete-all-playlists-dialog.component';
import { SettingsService } from './../services/settings.service';
interface SettingsSection {
@@ -81,6 +92,9 @@ interface StartupBehaviorOption {
labelKey: string;
}
type SettingsPlaylistDeleteSummary =
SettingsDeleteAllPlaylistsDialogData['summary'];
@Component({
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
@@ -121,6 +135,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
private playlistBackupService = inject(PlaylistBackupService);
private readonly elementRef = inject(ElementRef<HTMLElement>);
private readonly injector = inject(Injector);
private readonly databaseService = inject(DatabaseService);
private readonly dialogData = inject<{ isDialog: boolean } | null>(
MAT_DIALOG_DATA,
{ optional: true }
@@ -177,6 +192,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
/** EPG availability flag */
epgAvailable$ = this.store.select(selectIsEpgAvailable);
readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta);
readonly themeOptions: ThemeOption[] = [
{
@@ -244,6 +260,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
readonly isRemovingAllPlaylists = signal(false);
readonly isClearingEpgData = signal(false);
readonly isExportingData = signal(false);
readonly removeAllProgress = signal<DbOperationEvent | null>(null);
private settingsStore = inject(SettingsStore);
private sectionObserver?: IntersectionObserver;
@@ -292,6 +309,50 @@ export class SettingsComponent implements OnInit, OnDestroy {
},
];
readonly playlistDeleteSummary = computed<SettingsPlaylistDeleteSummary>(
() => {
const items = this.playlists();
return {
total: items.length,
m3u: items.filter((item) => !item.serverUrl && !item.macAddress)
.length,
xtream: items.filter((item) => Boolean(item.serverUrl)).length,
stalker: items.filter((item) => Boolean(item.macAddress))
.length,
};
}
);
readonly canRemoveAllPlaylists = computed(
() =>
!this.isRemovingAllPlaylists() &&
this.playlistDeleteSummary().total > 0
);
readonly removeAllProgressLabel = computed(() => {
if (!this.isRemovingAllPlaylists()) {
return null;
}
const progress = this.removeAllProgress();
const current = progress?.current;
const total = progress?.total;
if (
typeof current === 'number' &&
typeof total === 'number' &&
total > 0
) {
return this.translate.instant('SETTINGS.REMOVE_ALL_PROGRESS', {
current,
total,
});
}
return this.translate.instant('SETTINGS.REMOVE_ALL_IN_PROGRESS');
});
constructor() {
effect(
() => {
@@ -756,35 +817,87 @@ export class SettingsComponent implements OnInit, OnDestroy {
});
}
removeAll() {
this.dialogService.openConfirmDialog({
title: this.translate.instant('SETTINGS.REMOVE_DIALOG.TITLE'),
message: this.translate.instant('SETTINGS.REMOVE_DIALOG.MESSAGE'),
onConfirm: async (): Promise<void> => {
if (this.isRemovingAllPlaylists()) {
return;
}
removeAll(): void {
if (!this.canRemoveAllPlaylists()) {
return;
}
this.isRemovingAllPlaylists.set(true);
try {
await firstValueFrom(this.playlistsService.removeAll());
this.store.dispatch(PlaylistActions.removeAllPlaylists());
this.openSettingsSnackbar(
this.translate.instant('SETTINGS.PLAYLISTS_REMOVED')
);
} catch (error) {
console.error('Error removing playlists:', error);
this.openSettingsSnackbar(
this.translate.instant('SETTINGS.IMPORT_ERROR')
);
} finally {
this.isRemovingAllPlaylists.set(false);
this.matDialog
.open<
SettingsDeleteAllPlaylistsDialogComponent,
SettingsDeleteAllPlaylistsDialogData,
boolean
>(SettingsDeleteAllPlaylistsDialogComponent, {
autoFocus: false,
data: {
summary: this.playlistDeleteSummary(),
},
maxWidth: 'calc(100vw - 32px)',
restoreFocus: true,
width: '460px',
})
.afterClosed()
.pipe(take(1))
.subscribe((confirmed) => {
if (confirmed) {
void this.removeAllConfirmed();
}
},
});
}
private async removeAllConfirmed(): Promise<void> {
if (this.isRemovingAllPlaylists()) {
return;
}
this.isRemovingAllPlaylists.set(true);
this.removeAllProgress.set(null);
await this.waitForUiFeedbackFrame();
try {
const deleted =
this.isDesktop && window.electron
? await this.deleteAllPlaylistsInElectron()
: await this.deleteAllPlaylistsInBrowser();
if (!deleted) {
throw new Error('Delete all playlists returned success=false');
}
this.store.dispatch(PlaylistActions.removeAllPlaylists());
this.openSettingsSnackbar(
this.translate.instant('SETTINGS.PLAYLISTS_REMOVED')
);
} catch (error) {
console.error('Error removing playlists:', error);
this.openSettingsSnackbar(
this.translate.instant('SETTINGS.PLAYLISTS_REMOVE_FAILED')
);
} finally {
this.removeAllProgress.set(null);
this.isRemovingAllPlaylists.set(false);
}
}
private async deleteAllPlaylistsInElectron(): Promise<boolean> {
return this.databaseService.deleteAllPlaylists({
operationId: this.databaseService.createOperationId(
'settings-delete-all-playlists'
),
onEvent: (event) => this.handleDeleteAllPlaylistsEvent(event),
});
}
private async deleteAllPlaylistsInBrowser(): Promise<boolean> {
await firstValueFrom(this.playlistsService.removeAll());
return true;
}
private handleDeleteAllPlaylistsEvent(event: DbOperationEvent): void {
this.removeAllProgress.set(event);
}
private setupSectionObserver(): void {
if (typeof IntersectionObserver === 'undefined') {
return;