diff --git a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.html b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.html new file mode 100644 index 000000000..db5e7c365 --- /dev/null +++ b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.html @@ -0,0 +1,55 @@ +

+ {{ 'SETTINGS.REMOVE_DIALOG.TITLE' | translate }} +

+ + +

+ {{ + 'SETTINGS.REMOVE_DIALOG.MESSAGE' + | translate: { count: dialogData.summary.total } + }} +

+ +
+

+ {{ 'SETTINGS.REMOVE_DIALOG.SUMMARY_TITLE' | translate }} +

+ +
+ @for (item of summaryItems(); track item.id) { +
+
+ + {{ item.labelKey | translate }} +
+ {{ item.count }} +
+ } +
+
+ +
+

+ {{ 'SETTINGS.REMOVE_DIALOG.CONSEQUENCES_TITLE' | translate }} +

+ + +
+ +

+ {{ 'SETTINGS.REMOVE_DIALOG.BACKUP_HINT' | translate }} +

+
+ + + + + diff --git a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss new file mode 100644 index 000000000..9178d1806 --- /dev/null +++ b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.scss @@ -0,0 +1,94 @@ +:host { + display: block; +} + +.settings-delete-dialog { + display: flex; + flex-direction: column; + gap: 18px; + min-width: min(100%, 360px); +} + +.settings-delete-dialog__lead, +.settings-delete-dialog__hint { + margin: 0; + line-height: 1.55; + text-wrap: pretty; +} + +.settings-delete-dialog__section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.settings-delete-dialog__section-title { + margin: 0; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--mat-sys-on-surface-variant); +} + +.settings-delete-dialog__summary { + display: grid; + gap: 8px; +} + +.settings-delete-dialog__summary-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid + color-mix(in srgb, var(--mat-sys-outline) 46%, transparent); + border-radius: 14px; + background: color-mix( + in srgb, + var(--mat-sys-surface-container) 84%, + transparent + ); + + strong { + font-size: 1rem; + font-variant-numeric: tabular-nums; + } +} + +.settings-delete-dialog__summary-label { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.settings-delete-dialog__summary-label mat-icon { + width: 18px; + height: 18px; + font-size: 18px; + color: var(--mat-sys-primary); +} + +.settings-delete-dialog__list { + margin: 0; + padding-left: 20px; + display: grid; + gap: 8px; +} + +.settings-delete-dialog__hint { + padding: 12px 14px; + border-radius: 14px; + background: color-mix( + in srgb, + var(--app-selection-surface) 32%, + transparent + ); + color: var(--mat-sys-on-surface-variant); +} + +.settings-delete-dialog__actions { + padding-top: 2px; +} diff --git a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.spec.ts b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.spec.ts new file mode 100644 index 000000000..2736033e2 --- /dev/null +++ b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.spec.ts @@ -0,0 +1,96 @@ +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { SettingsDeleteAllPlaylistsDialogComponent } from './settings-delete-all-playlists-dialog.component'; + +describe('SettingsDeleteAllPlaylistsDialogComponent', () => { + let component: SettingsDeleteAllPlaylistsDialogComponent; + let fixture: ComponentFixture; + let translate: TranslateService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SettingsDeleteAllPlaylistsDialogComponent, + NoopAnimationsModule, + TranslateModule.forRoot(), + ], + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { + summary: { + total: 6, + m3u: 3, + xtream: 2, + stalker: 1, + }, + }, + }, + ], + }).compileComponents(); + + translate = TestBed.inject(TranslateService); + translate.setTranslation( + 'en', + { + CANCEL: 'Cancel', + HOME: { + PLAYLIST_TYPES: { + M3U: 'M3U', + XTREAM: 'Xtream', + STALKER: 'Stalker', + }, + }, + SETTINGS: { + REMOVE_DIALOG: { + TITLE: 'Delete all playlists?', + MESSAGE: 'Delete all {{count}} playlists from the app.', + SUMMARY_TITLE: 'Playlist sources', + CONSEQUENCES_TITLE: 'This also removes', + CONSEQUENCE_FAVORITES: 'Favorites', + CONSEQUENCE_RECENTLY_VIEWED: 'Recently viewed history', + CONSEQUENCE_PLAYBACK: 'Playback positions', + CONSEQUENCE_DOWNLOADS: 'Download history', + CONSEQUENCE_XTREAM_CACHE: 'Xtream cache', + BACKUP_HINT: 'Use Export first if you need a backup.', + CONFIRM: 'Delete everything', + }, + }, + }, + true + ); + translate.use('en'); + + fixture = TestBed.createComponent( + SettingsDeleteAllPlaylistsDialogComponent + ); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('renders the playlist type summary and destructive guidance', () => { + const text = (fixture.nativeElement as HTMLElement).textContent ?? ''; + + expect(component.summaryItems().map((item) => item.count)).toEqual([ + 3, 2, 1, + ]); + expect(text).toContain('Delete all playlists?'); + expect(text).toContain('Delete all 6 playlists from the app.'); + expect(text).toContain('Playlist sources'); + expect(text).toContain('M3U'); + expect(text).toContain('Xtream'); + expect(text).toContain('Stalker'); + expect(text).toContain('Favorites'); + expect(text).toContain('Use Export first if you need a backup.'); + }); + + it('renders cancel and confirm actions', () => { + const buttons = Array.from( + (fixture.nativeElement as HTMLElement).querySelectorAll('button') + ).map((button) => button.textContent?.trim()); + + expect(buttons).toEqual(['Cancel', 'Delete everything']); + }); +}); diff --git a/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.ts b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.ts new file mode 100644 index 000000000..79ef4f649 --- /dev/null +++ b/apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.ts @@ -0,0 +1,68 @@ +import { CommonModule } from '@angular/common'; +import { Component, computed, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { TranslateModule } from '@ngx-translate/core'; + +export interface SettingsDeleteAllPlaylistsDialogData { + summary: { + total: number; + m3u: number; + xtream: number; + stalker: number; + }; +} + +type SettingsDeleteSummaryItem = { + count: number; + icon: string; + id: 'm3u' | 'xtream' | 'stalker'; + labelKey: string; +}; + +@Component({ + selector: 'app-settings-delete-all-playlists-dialog', + templateUrl: './settings-delete-all-playlists-dialog.component.html', + styleUrls: ['./settings-delete-all-playlists-dialog.component.scss'], + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + MatIconModule, + TranslateModule, + ], +}) +export class SettingsDeleteAllPlaylistsDialogComponent { + readonly dialogData = + inject(MAT_DIALOG_DATA); + + readonly summaryItems = computed(() => [ + { + id: 'm3u', + count: this.dialogData.summary.m3u, + icon: 'playlist_play', + labelKey: 'HOME.PLAYLIST_TYPES.M3U', + }, + { + id: 'xtream', + count: this.dialogData.summary.xtream, + icon: 'cloud', + labelKey: 'HOME.PLAYLIST_TYPES.XTREAM', + }, + { + id: 'stalker', + count: this.dialogData.summary.stalker, + icon: 'router', + labelKey: 'HOME.PLAYLIST_TYPES.STALKER', + }, + ]); + + readonly consequenceKeys = [ + 'SETTINGS.REMOVE_DIALOG.CONSEQUENCE_FAVORITES', + 'SETTINGS.REMOVE_DIALOG.CONSEQUENCE_RECENTLY_VIEWED', + 'SETTINGS.REMOVE_DIALOG.CONSEQUENCE_PLAYBACK', + 'SETTINGS.REMOVE_DIALOG.CONSEQUENCE_DOWNLOADS', + 'SETTINGS.REMOVE_DIALOG.CONSEQUENCE_XTREAM_CACHE', + ] as const; +} diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html index 1ef4fb866..51743be4a 100644 --- a/apps/web/src/app/settings/settings.component.html +++ b/apps/web/src/app/settings/settings.component.html @@ -501,8 +501,7 @@ mat-button type="button" [disabled]=" - epgUrl.length === 0 || - isClearingEpgData() + epgUrl.length === 0 || isClearingEpgData() " (click)="refreshAllEpg()" > @@ -522,7 +521,9 @@ diameter="16" mode="indeterminate" /> - {{ 'SETTINGS.CLEARING_EPG_DATA' | translate }} + {{ + 'SETTINGS.CLEARING_EPG_DATA' | translate + }} } @else { {{ 'SETTINGS.CLEAR_EPG_DATA' | translate }} } @@ -808,10 +809,41 @@

{{ 'SETTINGS.REMOVE_ALL_DESCRIPTION' | translate }}

+
+ + {{ 'HOME.PLAYLIST_TYPES.M3U' | translate }} + {{ + playlistDeleteSummary().m3u + }} + + + {{ 'HOME.PLAYLIST_TYPES.XTREAM' | translate }} + {{ + playlistDeleteSummary().xtream + }} + + + {{ 'HOME.PLAYLIST_TYPES.STALKER' | translate }} + {{ + playlistDeleteSummary().stalker + }} + +
+ @if (removeAllProgressLabel(); as progressLabel) { +

+ {{ progressLabel }} +

+ }
@@ -1022,8 +1064,7 @@ Ko-fi {{ - 'SETTINGS.SUPPORT_KOFI_HINT' - | translate + 'SETTINGS.SUPPORT_KOFI_HINT' | translate }} diff --git a/apps/web/src/app/settings/settings.component.scss b/apps/web/src/app/settings/settings.component.scss index 2b54e0be6..637f195d9 100644 --- a/apps/web/src/app/settings/settings.component.scss +++ b/apps/web/src/app/settings/settings.component.scss @@ -113,11 +113,8 @@ width: 40px; height: 40px; flex-shrink: 0; - border: 1px solid color-mix( - in srgb, - var(--mat-sys-outline-variant) 74%, - transparent - ); + border: 1px solid + color-mix(in srgb, var(--mat-sys-outline-variant) 74%, transparent); border-radius: 12px; background: color-mix( in srgb, @@ -229,11 +226,8 @@ gap: 8px; min-height: 54px; padding: 12px 14px; - border: 1px solid color-mix( - in srgb, - var(--mat-sys-outline-variant) 70%, - transparent - ); + border: 1px solid + color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent); border-radius: 16px; background: color-mix( in srgb, @@ -291,11 +285,8 @@ ); color: var(--app-selection-color); box-shadow: - inset 0 0 0 1px color-mix( - in srgb, - var(--app-selection-border) 54%, - transparent - ), + inset 0 0 0 1px + color-mix(in srgb, var(--app-selection-border) 54%, transparent), 0 10px 18px -22px var(--settings-group-active-glow); } @@ -325,12 +316,12 @@ justify-content: flex-end; flex-wrap: wrap; gap: 6px; +} - .button-spinner { - display: inline-block; - margin-right: 6px; - vertical-align: middle; - } +.button-spinner { + display: inline-block; + margin-right: 6px; + vertical-align: middle; } .inline-actions__button { @@ -345,11 +336,8 @@ ); color: var(--app-selection-color); box-shadow: - inset 0 0 0 1px color-mix( - in srgb, - var(--app-selection-border) 48%, - transparent - ), + inset 0 0 0 1px + color-mix(in srgb, var(--app-selection-border) 48%, transparent), 0 8px 18px -18px var(--settings-group-active-glow); } @@ -377,7 +365,8 @@ } .remote-control-url { - font-family: 'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas, monospace; + font-family: + 'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas, monospace; color: inherit; text-decoration: none; font-size: 0.84rem; @@ -423,11 +412,8 @@ align-items: center; gap: 12px; padding: 12px 14px; - border: 1px solid color-mix( - in srgb, - var(--mat-sys-outline-variant) 70%, - transparent - ); + border: 1px solid + color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent); border-radius: 16px; background: color-mix( in srgb, @@ -498,11 +484,7 @@ } .link-card--github .link-card__glyph { - background: color-mix( - in srgb, - var(--mat-sys-on-surface) 10%, - transparent - ); + background: color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent); color: var(--mat-sys-on-surface); } @@ -557,7 +539,8 @@ border-radius: 14px; font-size: 0.84rem; background: color-mix(in srgb, var(--mat-sys-primary) 8%, transparent); - border: 1px solid color-mix(in srgb, var(--mat-sys-primary) 30%, transparent); + border: 1px solid + color-mix(in srgb, var(--mat-sys-primary) 30%, transparent); line-height: 1.45; a { @@ -575,6 +558,54 @@ border-radius: 16px; } +.danger-zone__summary { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.danger-zone__summary-pill { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 32px; + padding: 6px 10px; + border-radius: 999px; + background: color-mix( + in srgb, + var(--mat-sys-surface-container-high) 78%, + transparent + ); + border: 1px solid + color-mix(in srgb, var(--settings-item-danger-border) 72%, transparent); + font-size: 0.82rem; + line-height: 1.2; + + strong { + font-variant-numeric: tabular-nums; + } +} + +.danger-zone__progress { + margin-top: 12px; + margin-bottom: 0; + color: var(--mat-sys-on-surface-variant); + font-size: 0.84rem; +} + +.danger-zone__button { + min-width: 138px; +} + +.danger-zone__button--busy { + background: color-mix( + in srgb, + var(--settings-item-danger-border) 28%, + transparent + ); +} + .action-bar { position: sticky; bottom: calc( diff --git a/apps/web/src/app/settings/settings.component.spec.ts b/apps/web/src/app/settings/settings.component.spec.ts index afe317efe..58aa34d47 100644 --- a/apps/web/src/app/settings/settings.component.spec.ts +++ b/apps/web/src/app/settings/settings.component.spec.ts @@ -8,6 +8,7 @@ import { import { MatCardModule } from '@angular/material/card'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDividerModule } from '@angular/material/divider'; +import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; @@ -17,14 +18,20 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { Store } from '@ngrx/store'; -import { provideMockStore } from '@ngrx/store/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { EpgService } from '@iptvnator/epg/data-access'; import { MockModule, MockProvider } from 'ng-mocks'; import { DialogService } from 'components'; -import { DataService, PlaylistBackupService, PlaylistsService } from 'services'; +import { + DatabaseService, + DataService, + PlaylistBackupService, + PlaylistsService, +} from 'services'; import { Language, + PlaylistMeta, StartupBehavior, StreamFormat, Theme, @@ -34,12 +41,16 @@ import { SettingsComponent } from './settings.component'; import { signal } from '@angular/core'; import { NgxIndexedDBService } from 'ngx-indexed-db'; -import { from, of, Subject } from 'rxjs'; +import { from, of } from 'rxjs'; import { ElectronServiceStub } from '../services/electron.service.stub'; import { SettingsStore } from '../services/settings-store.service'; import { SettingsService } from '../services/settings.service'; import { SettingsContextService } from '@iptvnator/workspace/shell/util'; -import { PlaylistActions } from 'm3u-state'; +import { + PlaylistActions, + selectAllPlaylistsMeta, + selectIsEpgAvailable, +} from 'm3u-state'; class MatSnackBarStub { open = jest.fn(); @@ -110,8 +121,22 @@ describe('SettingsComponent', () => { let playlistsService: PlaylistsService; let playlistBackupService: PlaylistBackupService; let store: Store; + let mockStore: MockStore; + let databaseService: DatabaseService; let snackBar: MatSnackBarStub; const originalElectron = window.electron; + const importDate = '2026-04-21T00:00:00.000Z'; + + const createPlaylistMeta = ( + overrides: Partial = {} + ): PlaylistMeta => ({ + _id: overrides._id ?? 'playlist-id', + title: overrides.title ?? 'Playlist', + count: overrides.count ?? 10, + importDate: overrides.importDate ?? importDate, + autoRefresh: overrides.autoRefresh ?? false, + ...overrides, + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -124,6 +149,9 @@ describe('SettingsComponent', () => { MockProvider(DialogService, { openConfirmDialog: jest.fn(), }), + MockProvider(MatDialog, { + open: jest.fn(), + }), { provide: SettingsService, useClass: MockSettingsService }, { provide: MatSnackBar, useClass: MatSnackBarStub }, { provide: DataService, useClass: ElectronServiceStub }, @@ -131,7 +159,12 @@ describe('SettingsComponent', () => { provide: Router, useClass: MockRouter, }, - provideMockStore(), + provideMockStore({ + selectors: [ + { selector: selectAllPlaylistsMeta, value: [] }, + { selector: selectIsEpgAvailable, value: false }, + ], + }), { provide: NgxIndexedDBService, useValue: {}, @@ -140,6 +173,12 @@ describe('SettingsComponent', () => { getAllData: jest.fn().mockReturnValue(of([])), removeAll: jest.fn(), }), + MockProvider(DatabaseService, { + createOperationId: jest + .fn() + .mockReturnValue('delete-all-op'), + deleteAllPlaylists: jest.fn().mockResolvedValue(true), + }), MockProvider(PlaylistBackupService, { exportBackup: jest.fn().mockResolvedValue({ defaultFileName: @@ -209,9 +248,15 @@ describe('SettingsComponent', () => { playlistsService = TestBed.inject(PlaylistsService); playlistBackupService = TestBed.inject(PlaylistBackupService); store = TestBed.inject(Store); + mockStore = TestBed.inject(MockStore); + databaseService = TestBed.inject(DatabaseService); snackBar = TestBed.inject(MatSnackBar) as unknown as MatSnackBarStub; component = fixture.componentInstance; + component.checkAppVersion = jest.fn(); + component.fetchLocalIpAddresses = jest + .fn() + .mockResolvedValue(undefined); fixture.detectChanges(); }); @@ -219,6 +264,12 @@ describe('SettingsComponent', () => { window.electron = originalElectron; }); + function setPlaylists(playlists: PlaylistMeta[]): void { + mockStore.overrideSelector(selectAllPlaylistsMeta, playlists); + mockStore.refreshState(); + fixture.detectChanges(); + } + it('should create and init component', () => { expect(component).toBeTruthy(); }); @@ -397,30 +448,129 @@ describe('SettingsComponent', () => { }); }); - it('removes all playlists with a busy state and dispatches store cleanup after success', async () => { - const removal$ = new Subject(); - const dispatchSpy = jest.spyOn(store, 'dispatch'); + it('disables the global wipe action when there are no playlists', () => { + setPlaylists([]); - (dialogService.openConfirmDialog as jest.Mock).mockImplementation( - ({ onConfirm }: { onConfirm: () => Promise }) => { - void onConfirm(); - } - ); - (playlistsService.removeAll as jest.Mock).mockReturnValue( - removal$.asObservable() - ); - jest.spyOn(translate, 'instant').mockImplementation((key) => key); + const deleteButton = ( + fixture.nativeElement as HTMLElement + ).querySelector('.danger-zone__button') as HTMLButtonElement | null; + + expect(component.canRemoveAllPlaylists()).toBe(false); + expect(deleteButton?.disabled).toBe(true); + }); + + it('opens the dedicated delete-all dialog with the current playlist type summary', () => { + setPlaylists([ + createPlaylistMeta({ _id: 'm3u-1' }), + createPlaylistMeta({ + _id: 'xtream-1', + serverUrl: 'http://xtream.example', + }), + createPlaylistMeta({ + _id: 'stalker-1', + macAddress: '00:11:22:33:44:55', + }), + ]); + + const openSpy = jest + .spyOn((component as any).matDialog, 'open') + .mockReturnValue({ + afterClosed: () => of(false), + } as any); component.removeAll(); - expect(component.isRemovingAllPlaylists()).toBe(true); - expect(playlistsService.removeAll).toHaveBeenCalled(); + expect(component.playlistDeleteSummary()).toEqual({ + total: 3, + m3u: 1, + xtream: 1, + stalker: 1, + }); + expect(openSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ + data: { + summary: { + total: 3, + m3u: 1, + xtream: 1, + stalker: 1, + }, + }, + }) + ); + }); - removal$.next(); - removal$.complete(); + it('tracks Electron delete-all progress and dispatches store cleanup after success', async () => { + setPlaylists([ + createPlaylistMeta({ _id: 'm3u-1' }), + createPlaylistMeta({ + _id: 'xtream-1', + serverUrl: 'http://xtream.example', + }), + ]); + + const dispatchSpy = jest.spyOn(store, 'dispatch'); + (databaseService.deleteAllPlaylists as jest.Mock).mockClear(); + jest.spyOn((component as any).matDialog, 'open').mockReturnValue({ + afterClosed: () => of(true), + } as any); + jest.spyOn(translate, 'instant').mockImplementation( + (key: string, params?: Record) => { + if (key === 'SETTINGS.REMOVE_ALL_PROGRESS') { + return `${params?.current}/${params?.total}`; + } + return key; + } + ); + jest.spyOn( + component as any, + 'waitForUiFeedbackFrame' + ).mockResolvedValue(undefined); + + let resolveDelete: (value: boolean) => void = () => undefined; + (databaseService.deleteAllPlaylists as jest.Mock).mockImplementation( + ({ + onEvent, + }: { + onEvent?: (event: { + operation: string; + status: string; + current?: number; + total?: number; + }) => void; + }) => { + onEvent?.({ + operation: 'delete-all-playlists', + status: 'progress', + current: 3, + total: 7, + }); + + return new Promise((resolve) => { + resolveDelete = resolve; + }); + } + ); + + component.removeAll(); + await Promise.resolve(); + fixture.detectChanges(); + + expect(component.isRemovingAllPlaylists()).toBe(true); + expect(component.removeAllProgressLabel()).toBe('3/7'); + expect(databaseService.deleteAllPlaylists).toHaveBeenCalledWith( + expect.objectContaining({ + operationId: 'delete-all-op', + onEvent: expect.any(Function), + }) + ); + + resolveDelete(true); await fixture.whenStable(); expect(component.isRemovingAllPlaylists()).toBe(false); + expect(component.removeAllProgress()).toBeNull(); expect(dispatchSpy).toHaveBeenCalledWith( PlaylistActions.removeAllPlaylists() ); @@ -436,6 +586,50 @@ describe('SettingsComponent', () => { ); }); + it('falls back to PlaylistsService.removeAll outside Electron', async () => { + fixture.destroy(); + window.electron = undefined as unknown as typeof window.electron; + + const browserFixture = TestBed.createComponent(SettingsComponent); + const browserComponent = browserFixture.componentInstance; + browserComponent.checkAppVersion = jest.fn(); + browserComponent.fetchLocalIpAddresses = jest + .fn() + .mockResolvedValue(undefined); + browserFixture.detectChanges(); + + mockStore.overrideSelector(selectAllPlaylistsMeta, [ + createPlaylistMeta({ _id: 'browser-m3u' }), + ]); + mockStore.refreshState(); + browserFixture.detectChanges(); + + jest.spyOn((browserComponent as any).matDialog, 'open').mockReturnValue( + { + afterClosed: () => of(true), + } as any + ); + jest.spyOn( + browserComponent as any, + 'waitForUiFeedbackFrame' + ).mockResolvedValue(undefined); + (databaseService.deleteAllPlaylists as jest.Mock).mockClear(); + (playlistsService.removeAll as jest.Mock).mockClear(); + (playlistsService.removeAll as jest.Mock).mockReturnValue( + of(undefined) + ); + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + browserComponent.removeAll(); + await browserFixture.whenStable(); + + expect(playlistsService.removeAll).toHaveBeenCalled(); + expect(databaseService.deleteAllPlaylists).not.toHaveBeenCalled(); + expect(dispatchSpy).toHaveBeenCalledWith( + PlaylistActions.removeAllPlaylists() + ); + }); + it('shows the save confirmation snackbar at the bottom center with the settings offset class', () => { jest.spyOn(translate, 'instant').mockReturnValue('Settings saved'); @@ -493,23 +687,19 @@ describe('SettingsComponent', () => { }); it('shows an export busy state until the backup file has been written', async () => { - let resolveExport: ( - value: { - defaultFileName: string; - json: string; - manifest: { - kind: string; - version: number; - exportedAt: string; - includeSecrets: boolean; - playlists: never[]; - }; - } - ) => void = () => undefined; + let resolveExport: (value: { + defaultFileName: string; + json: string; + manifest: { + kind: string; + version: number; + exportedAt: string; + includeSecrets: boolean; + playlists: never[]; + }; + }) => void = () => undefined; - ( - playlistBackupService.exportBackup as jest.Mock - ).mockReturnValueOnce( + (playlistBackupService.exportBackup as jest.Mock).mockReturnValueOnce( new Promise((resolve) => { resolveExport = resolve; }) diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index 136e7c6d7..0611da0f4 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -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); 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(null); private settingsStore = inject(SettingsStore); private sectionObserver?: IntersectionObserver; @@ -292,6 +309,50 @@ export class SettingsComponent implements OnInit, OnDestroy { }, ]; + readonly playlistDeleteSummary = computed( + () => { + 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 => { - 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 { + 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 { + return this.databaseService.deleteAllPlaylists({ + operationId: this.databaseService.createOperationId( + 'settings-delete-all-playlists' + ), + onEvent: (event) => this.handleDeleteAllPlaylistsEvent(event), }); } + private async deleteAllPlaylistsInBrowser(): Promise { + await firstValueFrom(this.playlistsService.removeAll()); + return true; + } + + private handleDeleteAllPlaylistsEvent(event: DbOperationEvent): void { + this.removeAllProgress.set(event); + } + private setupSectionObserver(): void { if (typeof IntersectionObserver === 'undefined') { return; diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 5f75e700b..6e271f170 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -149,15 +149,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "تصدير أو استيراد القوائم بتنسيق JSON.", "IMPORT_DATA": "استيراد", "EXPORT_DATA": "تصدير", - "REMOVE_ALL": "حذف جميع القوائم", - "REMOVE_ALL_BUTTON": "حذف", - "REMOVE_ALL_DESCRIPTION": "سيتم حذف جميع القوائم من التطبيق.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "حذف جميع القوائم", - "MESSAGE": "هل أنت متأكد من رغبتك في حذف جميع القوائم؟" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "خطأ في الاستيراد، يرجى تجربة ملف آخر.", - "PLAYLISTS_REMOVED": "تم حذف جميع القوائم.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "مسار مشغل MPV", "MPV_PLAYER_PATH_DESCRIPTION": "حدد مسار مشغل MPV في نظامك.", "MPV_PLAYER_PATH": "مسار مشغل MPV", @@ -167,7 +176,10 @@ "REMOTE_CONTROL": "التحكم عن بعد", "REMOTE_CONTROL_DESCRIPTION": "تمكين التحكم عن بعد للتطبيق.", "REMOTE_CONTROL_PORT": "منفذ التحكم عن بعد", - "REMOTE_CONTROL_PORT_DESCRIPTION": "المنفذ المستخدم للتحكم عن بعد (الافتراضي: 8765)." + "REMOTE_CONTROL_PORT_DESCRIPTION": "المنفذ المستخدم للتحكم عن بعد (الافتراضي: 8765).", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "السمة الداكنة", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index d749608fa..46cf1a57e 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -141,15 +141,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "صدّر ولا استورد قوائم التشغيل بصيغة JSON اللي كيستعملها التطبيق.", "IMPORT_DATA": "استورد", "EXPORT_DATA": "صدّر", - "REMOVE_ALL": "حيد قوائم التشغيل", - "REMOVE_ALL_BUTTON": "حيد", - "REMOVE_ALL_DESCRIPTION": "غادي يحيد جميع قوائم التشغيل الموجودة من التطبيق.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "حيد جميع قوائم التشغيل", - "MESSAGE": "واش متأكد بغيتي تحيد جميع قوائم التشغيل من التطبيق؟" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "خطأ في الاستيراد، عفاك جرب بملف آخر.", - "PLAYLISTS_REMOVED": "جميع قوائم التشغيل تحيدو.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "مسار mpv", "MPV_PLAYER_PATH_DESCRIPTION": "حدد مسار مشغل mpv في السيستيم ديالك. غادي يستعمل المسار اللي عطيتيه لملف mpv بدل اللي كيتلقى في $PATH.", "MPV_PLAYER_PATH": "مسار مشغل mpv", @@ -159,7 +168,10 @@ "REMOTE_CONTROL": "التحكم عن بعد", "REMOTE_CONTROL_DESCRIPTION": "فعل التحكم عن بعد للتطبيق.", "REMOTE_CONTROL_PORT": "منفذ التحكم عن بعد", - "REMOTE_CONTROL_PORT_DESCRIPTION": "المنفذ اللي كيتستعمل للتحكم عن بعد للتطبيق. المنفذ 3000 كيتستعمل افتراضياً." + "REMOTE_CONTROL_PORT_DESCRIPTION": "المنفذ اللي كيتستعمل للتحكم عن بعد للتطبيق. المنفذ 3000 كيتستعمل افتراضياً.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "المظهر الداكن", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 20c145ad2..90ced9a34 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -148,20 +148,32 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Экспарт або імпарт плэйлістоў у фармаце JSON, які выкарыстоўваецца ў праграме.", "IMPORT_DATA": "Імпарт", "EXPORT_DATA": "Экспарт", - "REMOVE_ALL": "Выдаліць плэйліст", - "REMOVE_ALL_BUTTON": "Выдаліць", - "REMOVE_ALL_DESCRIPTION": "Гэта дзеянне выдаліць усе існуючыя плэйлісты з праграмы.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Выдаліць усе плэйлісты", - "MESSAGE": "Вы ўпэўнены, што хочаце выдаліць усе плэйлісты з праграмы?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Памылка імпарту, калі ласка, паспрабуйце імпартаваць іншы файл.", - "PLAYLISTS_REMOVED": "Усе плэйлісты былі выдалены.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "VLC_PLAYER_PATH_LABEL": "Шлях да VLC прайгравальніка", "VLC_PLAYER_PATH": "Шлях да VLC прайгравальніка", "MPV_PLAYER_PATH_LABEL": "Шлях да mpv прайгравальніка", "MPV_PLAYER_PATH": "Шлях да mpv прайгравальніка", - "VLC_PLAYER_PATH_DESCRIPTION": "Усталюйце шлях да прайгравальніка VLC у вашай сістэме. \nЁн будзе выкарыстоўваць прадастаўлены шлях да бінарнага файла VLC замест шляху па змаўчанні." + "VLC_PLAYER_PATH_DESCRIPTION": "Усталюйце шлях да прайгравальніка VLC у вашай сістэме. \nЁн будзе выкарыстоўваць прадастаўлены шлях да бінарнага файла VLC замест шляху па змаўчанні.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Цёмная тэма", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index eeece5cb1..fa330f36c 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -264,8 +264,8 @@ "SHOW_DASHBOARD": "Dashboard anzeigen", "SHOW_DASHBOARD_DESCRIPTION": "Dashboard in der Workspace-Leiste verfügbar halten. Wenn deaktiviert, wird Quellen zur ersten Workspace-Seite.", "STREAM_FORMAT_DESCRIPTION": "Standard-Streamformat auswählen (Xtream)", - "REMOVE_ALL": "Playlist entfernen", - "REMOVE_ALL_BUTTON": "Entfernen", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", "CLEAR_EPG_DATA": "EPG-Daten löschen", "CLEAR_EPG_DIALOG": { "TITLE": "EPG-Daten löschen", @@ -275,11 +275,20 @@ "EPG_DATA_CLEAR_FAILED": "EPG-Daten konnten nicht gelöscht werden. Bitte erneut versuchen.", "CLEARING_EPG_DATA": "Wird gelöscht…", "REMOVE_DIALOG": { - "TITLE": "Аlle Wiedergabelisten löschen", - "MESSAGE": "Sind Sie sicher, dass Sie alle Wiedergabelisten aus der Anwendung löschen möchten?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "LANGUAGE_DESCRIPTION": "Wählen Sie die Sprache der Anwendung aus", - "PLAYLISTS_REMOVED": "Alle Playlists wurden entfernt.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "VERSION_DESCRIPTION": "Aktuelle Version der Anwendung", "ABOUT_SUBTITLE": "Version, Projekt-Links und Möglichkeiten, IPTVnator zu unterstützen.", "COMMUNITY": "Projekt & Community", @@ -292,7 +301,7 @@ "SUPPORT_DESCRIPTION": "IPTVnator ist kostenlose Open-Source-Software, die im Offenen entwickelt wird. Wenn sie Teil deines Alltags ist, hilft ein kleiner Beitrag, das Projekt am Leben zu halten und die nächsten Schritte zu gestalten — danke!", "SUPPORT_SPONSORS_HINT": "Wiederkehrend oder einmalig", "SUPPORT_KOFI_HINT": "Spendiere einen Kaffee", - "REMOVE_ALL_DESCRIPTION": "Dadurch werden alle vorhandenen Wiedergabelisten aus der Anwendung entfernt.", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "MPV_PLAYER_PATH": "MPV-Player-Pfad", "SHOW_CAPTIONS_DESCRIPTION": "Untertitel standardmäßig ein- oder ausblenden", "SHOW_EXTERNAL_PLAYBACK_BAR": "Now-Playing-Fußleiste anzeigen", @@ -334,7 +343,10 @@ "PORT_REQUIRED": "Port ist erforderlich", "PORT_MIN": "Port muss mindestens 1 sein", "PORT_MAX": "Port darf maximal 65535 sein", - "PORT_PATTERN": "Nur Ziffern sind erlaubt" + "PORT_PATTERN": "Nur Ziffern sind erlaubt", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Dunkel", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 1537b1943..622695194 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -185,15 +185,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Εξαγωγή ή εισαγωγή λιστών αναπαραγωγής στη μορφή JSON που χρησιμοποιείται στην εφαρμογή.", "IMPORT_DATA": "Εισαγωγή", "EXPORT_DATA": "Εξαγωγή", - "REMOVE_ALL": "Κατάργηση λιστών αναπαραγωγής", - "REMOVE_ALL_BUTTON": "Διαγραφή", - "REMOVE_ALL_DESCRIPTION": "Θα καταργήσει όλες τις υπάρχουσες λίστες αναπαραγωγής από την εφαρμογή.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Κατάργηση όλων των λιστών αναπαραγωγής", - "MESSAGE": "Είστε σίγουροι ότι θέλετε να διαγράψετε όλες τις λίστες αναπαραγωγής από την εφαρμογή;" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Σφάλμα εισαγωγής, δοκιμάστε με άλλο αρχείο.", - "PLAYLISTS_REMOVED": "Όλες οι λίστες αναπαραγωγής αφαιρέθηκαν.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "διαδρομή mpv", "MPV_PLAYER_PATH_DESCRIPTION": "Ορίστε τη διαδρομή προς το mpv player στο σύστημά σας. Θα χρησιμοποιήσει την παρεχόμενη διαδρομή προς ένα δυαδικό αρχείο mpv αντί για αυτήν που βρίσκεται στο $PATH.", "MPV_PLAYER_PATH": "διαδρομή mpv προγράμματος αναπαραγωγής", @@ -212,7 +221,10 @@ "PORT_REQUIRED": "Απαιτείται θύρα", "PORT_MIN": "Η θύρα πρέπει να είναι τουλάχιστον 1", "PORT_MAX": "Η θύρα πρέπει να είναι το πολύ 65535", - "PORT_PATTERN": "Επιτρέπονται μόνο αριθμητικά ψηφία" + "PORT_PATTERN": "Επιτρέπονται μόνο αριθμητικά ψηφία", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Σκούρο θέμα", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 8778cf513..fbfc4a89f 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -251,15 +251,24 @@ "IMPORT_DATA": "Import", "EXPORT_DATA": "Export", "EXPORTING_DATA": "Exporting…", - "REMOVE_ALL": "Remove playlists", - "REMOVE_ALL_BUTTON": "Remove", - "REMOVE_ALL_DESCRIPTION": "It will remove all existing playlists from the application.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Remove all playlists", - "MESSAGE": "Are you sure you want to delete all playlist from the application?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Import error, please try with another file.", - "PLAYLISTS_REMOVED": "All playlists were removed.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "mpv path", "MPV_PLAYER_PATH_DESCRIPTION": "Set path to mpv player in your system. It will use the provided path to a mpv binary instead of using the one found in $PATH.", "MPV_PLAYER_PATH": "mpv player path", @@ -283,7 +292,10 @@ "PORT_REQUIRED": "Port is required", "PORT_MIN": "Port must be at least 1", "PORT_MAX": "Port must be at most 65535", - "PORT_PATTERN": "Only digits are allowed" + "PORT_PATTERN": "Only digits are allowed", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Dark theme", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index d77e0b694..d28980c92 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -134,8 +134,8 @@ "SETTINGS_SAVED": "¡Completado! La configuración se guardó.", "THEME": "Tema visual", "SHOW_CAPTIONS": "Mostrar subtítulos", - "REMOVE_ALL": "Eliminar lista de reproducción", - "REMOVE_ALL_BUTTON": "Eliminar", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", "CLEAR_EPG_DATA": "Borrar datos EPG", "CLEAR_EPG_DIALOG": { "TITLE": "Borrar datos EPG", @@ -145,11 +145,20 @@ "EPG_DATA_CLEAR_FAILED": "No se pudieron borrar los datos EPG. Inténtalo de nuevo.", "CLEARING_EPG_DATA": "Borrando…", "REMOVE_DIALOG": { - "TITLE": "Eliminar todas las listas de reproducción", - "MESSAGE": "¿Estás seguro de que deseas eliminar todas las listas de reproducción de la aplicación?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "LANGUAGE_DESCRIPTION": "Seleccione el idioma de la aplicación", - "PLAYLISTS_REMOVED": "Se eliminaron todas las listas de reproducción.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "VERSION_DESCRIPTION": "Versión actual de la aplicación.", "MPV_PLAYER_PATH": "ruta del reproductor mpv", "MPV_PLAYER_PATH_DESCRIPTION": "Establece la ruta al reproductor mpv en tu sistema. Se usará la ruta proporcionada al binario de mpv en lugar de la encontrada en $PATH.", @@ -163,17 +172,16 @@ "EXPORT_DATA": "Exportar", "EXPORTING_DATA": "Exportando…", "VIDEO_PLAYER_DESCRIPTION": "Seleccionar reproductor de vídeo predeterminado", - "IMPORT_EXPORT_DATA": "Importar/exportar listas de reproducción", + "IMPORT_EXPORT_DATA": "Importar/exportar copias de seguridad", "VLC_PLAYER_PATH_LABEL": "ruta VLC", "VLC_PLAYER_PATH_DESCRIPTION": "Establezca la ruta al reproductor VLC en su sistema. \nUtilizará la ruta proporcionada a un binario VLC en lugar de la ruta predeterminada para nuestro sistema.", "THEME_DESCRIPTION": "Seleccione el tema visual de la aplicación.", "EPG_NOTE": "Actualmente, EPG está disponible sólo en la versión electrónica de IPTVnator.", "VLC_PLAYER_PATH": "Ruta del reproductor VLC", - "IMPORT_EXPORT_DATA": "Importar/exportar copias de seguridad", "IMPORT_EXPORT_DATA_DESCRIPTION": "Exporte o restaure copias de seguridad con instantáneas M3U, metadatos de conexión de Xtream/Stalker, estado del usuario y URLs de EPG.", "EPG_NOTE_URL_TEXT": "Compruébalo aquí", "MPV_PLAYER_PATH_LABEL": "Ruta de mpv", - "REMOVE_ALL_DESCRIPTION": "Esto eliminará todas las listas de reproducción existentes de la aplicación.", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOTE_CONTROL": "Control remoto", "REMOTE_CONTROL_DESCRIPTION": "Habilita el control remoto de la aplicación.", "REMOTE_CONTROL_PORT": "Puerto de control remoto", @@ -190,7 +198,10 @@ "PORT_MIN": "El puerto debe ser al menos 1", "PORT_MAX": "El puerto debe ser como máximo 65535", "PORT_PATTERN": "Solo se permiten digitos", - "STREAM_FORMAT_DESCRIPTION": "Selecciona el formato de transmision predeterminado (Xtream)" + "STREAM_FORMAT_DESCRIPTION": "Selecciona el formato de transmision predeterminado (Xtream)", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Tema oscuro", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index f1cfe61f6..8f35ace8d 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -162,15 +162,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Exportez ou importez des listes au format JSON utilisé dans l'application.", "IMPORT_DATA": "Importer", "EXPORT_DATA": "Exporter", - "REMOVE_ALL": "Supprimer les listes de lecture", - "REMOVE_ALL_BUTTON": "Supprimer", - "REMOVE_ALL_DESCRIPTION": "Cela supprimera toutes les listes de lecture existantes de l'application.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Supprimer toutes les listes", - "MESSAGE": "Êtes-vous sûr de vouloir supprimer toutes les listes de l'application ?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Erreur d'importation, veuillez essayer avec un autre fichier.", - "PLAYLISTS_REMOVED": "Toutes les listes de lecture ont été supprimées.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "Chemin mpv", "MPV_PLAYER_PATH_DESCRIPTION": "Définissez le chemin d'accès au lecteur mpv dans votre système. Il utilisera le chemin fourni vers un binaire mpv au lieu de celui trouvé dans $PATH.", "MPV_PLAYER_PATH": "Chemin du lecteur mpv", @@ -180,7 +189,10 @@ "REMOTE_CONTROL": "Télécommande", "REMOTE_CONTROL_DESCRIPTION": "Activer la télécommande de l'application.", "REMOTE_CONTROL_PORT": "Port de télécommande", - "REMOTE_CONTROL_PORT_DESCRIPTION": "Port utilisé pour la télécommande de l'application. Le port 8765 est utilisé par défaut." + "REMOTE_CONTROL_PORT_DESCRIPTION": "Port utilisé pour la télécommande de l'application. Le port 8765 est utilisé par défaut.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Thème foncé", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 399cb8888..d2ec2f35a 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -169,15 +169,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Esporta/importa playlist nel formato JSON usato all'interno dell'applicazione.", "IMPORT_DATA": "Importa", "EXPORT_DATA": "Esporta", - "REMOVE_ALL": "Rimuovi playlist", - "REMOVE_ALL_BUTTON": "Rimuovi", - "REMOVE_ALL_DESCRIPTION": "Verranno rimosse tutte le playlist esistenti nell'applicazione.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Rimuovi tutte le playlist", - "MESSAGE": "Sei sicuro di voler rimuovere tutte le playlist dall'applicazione?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Errore di importazione, prova con un altro file.", - "PLAYLISTS_REMOVED": "Tutte le playlist sono state rimosse.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "percorso mpv", "MPV_PLAYER_PATH_DESCRIPTION": "Imposta il percorso per il lettore mpv in questo tuo sistema. Verrà usato il percorso fornito per un eseguibile mpv invece di usare quello trovato in $PATH.", "MPV_PLAYER_PATH": "percorso riproduttore mpv", @@ -201,7 +210,10 @@ "PORT_REQUIRED": "La porta è obbligatoria", "PORT_MIN": "La porta deve essere almeno 1", "PORT_MAX": "La porta deve essere al massimo 65535", - "PORT_PATTERN": "Sono consentite solo cifre" + "PORT_PATTERN": "Sono consentite solo cifre", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Tema scuro", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index dc97ac931..563682518 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -129,15 +129,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "アプリケーション内で使用されているJSONフォーマットでプレイリストをエクスポートまたはインポート。", "IMPORT_DATA": "インポート", "EXPORT_DATA": "エクスポート", - "REMOVE_ALL": "プレイリストを削除", - "REMOVE_ALL_BUTTON": "削除", - "REMOVE_ALL_DESCRIPTION": "アプリケーションから既存のプレイリストをすべて削除します。", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "すべてのプレイリストを削除", - "MESSAGE": "アプリケーションからすべてのプレイリストを削除してもよろしいですか?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "インポートエラー。別のファイルで試してください。", - "PLAYLISTS_REMOVED": "すべてのプレイリストが削除されました。", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "mpvのパス", "MPV_PLAYER_PATH_DESCRIPTION": "システム内のmpvプレーヤーのパスを設定します。$PATHで見つかったものの代わりに、指定されたmpvバイナリのパスを使用します。", "MPV_PLAYER_PATH": "mpvプレーヤーのパス", @@ -147,7 +156,10 @@ "REMOTE_CONTROL": "リモートコントロール", "REMOTE_CONTROL_DESCRIPTION": "アプリケーションのリモートコントロールを有効にします。", "REMOTE_CONTROL_PORT": "リモートコントロールポート", - "REMOTE_CONTROL_PORT_DESCRIPTION": "アプリケーションのリモートコントロールに使用されるポート。デフォルトではポート3000が使用されます。" + "REMOTE_CONTROL_PORT_DESCRIPTION": "アプリケーションのリモートコントロールに使用されるポート。デフォルトではポート3000が使用されます。", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "ダークテーマ", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index add55b268..0d549b2ae 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -94,14 +94,23 @@ "SETTINGS_SAVED": "설정이 저장되었습니다.", "THEME": "테마", "SHOW_CAPTIONS": "Show subtitles", - "REMOVE_ALL": "재생목록 삭제", - "REMOVE_ALL_BUTTON": "제거하다", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", "REMOVE_DIALOG": { - "TITLE": "모든 재생목록 삭제", - "MESSAGE": "응용 프로그램에서 모든 재생 목록을 삭제하시겠습니까?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "LANGUAGE_DESCRIPTION": "애플리케이션 언어 선택", - "PLAYLISTS_REMOVED": "모든 재생목록이 삭제되었습니다.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "VERSION_DESCRIPTION": "애플리케이션의 현재 버전", "IMPORT_DATA": "수입", "ADD_EPG_SOURCE": "EPG 소스 추가", @@ -117,7 +126,11 @@ "VLC_PLAYER_PATH": "VLC 플레이어 경로", "IMPORT_EXPORT_DATA_DESCRIPTION": "애플리케이션 내에서 사용되는 JSON 형식으로 재생 목록을 내보내거나 가져옵니다.", "EPG_NOTE_URL_TEXT": "여기에서 확인하세요", - "MPV_PLAYER_PATH_LABEL": "mpv 경로" + "MPV_PLAYER_PATH_LABEL": "mpv 경로", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "어두운 테마", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 5a971586e..993597c03 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -141,15 +141,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Exporteer of importeer afspeellijsten in JSON-formaat.", "IMPORT_DATA": "Importeren", "EXPORT_DATA": "Exporteren", - "REMOVE_ALL": "Afspeellijsten verwijderen", - "REMOVE_ALL_BUTTON": "Verwijderen", - "REMOVE_ALL_DESCRIPTION": "Hiermee verwijder je alle afspeellijsten uit de applicatie.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Alle afspeellijsten verwijderen", - "MESSAGE": "Weet je zeker dat je alle afspeellijsten wilt verwijderen?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Fout bij importeren, probeer een ander bestand.", - "PLAYLISTS_REMOVED": "Alle afspeellijsten zijn verwijderd.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "mpv-bestandspad", "MPV_PLAYER_PATH_DESCRIPTION": "Stel het bestandspad in naar de mpv-speler op je systeem. Dit bestandspad wordt gebruikt in plaats van de standaard mpv-bestand uit $PATH.", "MPV_PLAYER_PATH": "mpv-speler bestandspad", @@ -159,7 +168,10 @@ "REMOTE_CONTROL": "Afstandsbediening", "REMOTE_CONTROL_DESCRIPTION": "Schakel de afstandsbediening voor de applicatie in.", "REMOTE_CONTROL_PORT": "Afstandsbedieningspoort", - "REMOTE_CONTROL_PORT_DESCRIPTION": "Poort voor de afstandsbediening van de applicatie. Standaard wordt poort 3000 gebruikt." + "REMOTE_CONTROL_PORT_DESCRIPTION": "Poort voor de afstandsbediening van de applicatie. Standaard wordt poort 3000 gebruikt.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Donker thema", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 458126048..863d4f161 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -141,15 +141,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Eksportuj lub importuj listy odtwarzania w formacie JSON używanym w aplikacji.", "IMPORT_DATA": "Importuj", "EXPORT_DATA": "Eksportuj", - "REMOVE_ALL": "Usuń listy odtwarzania", - "REMOVE_ALL_BUTTON": "Usuń", - "REMOVE_ALL_DESCRIPTION": "Spowoduje to usunięcie wszystkich istniejących list odtwarzania z aplikacji.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Usuń wszystkie listy odtwarzania", - "MESSAGE": "Czy na pewno chcesz usunąć wszystkie listy odtwarzania z aplikacji?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Błąd importu, spróbuj z innym plikiem.", - "PLAYLISTS_REMOVED": "Wszystkie listy odtwarzania zostały usunięte.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "Ścieżka mpv", "MPV_PLAYER_PATH_DESCRIPTION": "Ustaw ścieżkę do odtwarzacza mpv w systemie. Będzie używać podanej ścieżki do pliku binarnego mpv zamiast tego znalezionego w $PATH.", "MPV_PLAYER_PATH": "Ścieżka odtwarzacza mpv", @@ -159,7 +168,10 @@ "REMOTE_CONTROL": "Zdalne sterowanie", "REMOTE_CONTROL_DESCRIPTION": "Włącz zdalne sterowanie aplikacją.", "REMOTE_CONTROL_PORT": "Port zdalnego sterowania", - "REMOTE_CONTROL_PORT_DESCRIPTION": "Port używany do zdalnego sterowania aplikacją. Domyślnie używany jest port 3000." + "REMOTE_CONTROL_PORT_DESCRIPTION": "Port używany do zdalnego sterowania aplikacją. Domyślnie używany jest port 3000.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Ciemny motyw", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 01596cf14..06d8fbc92 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -160,15 +160,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Exportar ou importar playlists no formato JSON usado no aplicativo.", "IMPORT_DATA": "Importar", "EXPORT_DATA": "Exportar", - "REMOVE_ALL": "Remover playlists", - "REMOVE_ALL_BUTTON": "Remover", - "REMOVE_ALL_DESCRIPTION": "Isso removerá todas as playlists existentes do aplicativo.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Remover todas as playlists", - "MESSAGE": "Tem certeza de que deseja excluir todas as playlists do aplicativo?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Erro de importação, tente com outro arquivo.", - "PLAYLISTS_REMOVED": "Todas as playlists foram removidas.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "Caminho do mpv", "MPV_PLAYER_PATH_DESCRIPTION": "Defina o caminho para o reprodutor mpv em seu sistema. Ele usará o caminho fornecido para um binário mpv em vez de usar o encontrado em $PATH.", "MPV_PLAYER_PATH": "Caminho do reprodutor mpv", @@ -178,7 +187,10 @@ "REMOTE_CONTROL": "Controle remoto", "REMOTE_CONTROL_DESCRIPTION": "Ativar controle remoto do aplicativo.", "REMOTE_CONTROL_PORT": "Porta de controle remoto", - "REMOTE_CONTROL_PORT_DESCRIPTION": "Porta usada para controle remoto do aplicativo. A porta 8765 é usada por padrão." + "REMOTE_CONTROL_PORT_DESCRIPTION": "Porta usada para controle remoto do aplicativo. A porta 8765 é usada por padrão.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Tema escuro", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 804d1ecf5..7e9b4434f 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -225,15 +225,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Экспорт или импорт плейлистов в формате JSON, используемом в приложении.", "IMPORT_DATA": "Импорт", "EXPORT_DATA": "Экспорт", - "REMOVE_ALL": "Удалить плейлист", - "REMOVE_ALL_BUTTON": "Удалить", - "REMOVE_ALL_DESCRIPTION": " Это действие удалит все существующие плейлисты из приложения.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Удалить все плейлисты", - "MESSAGE": "Вы уверены что хотите удалить все плейлисты из приложения?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "Ошибка импорта, пожалуйста попробуйте импортировать другой файл.", - "PLAYLISTS_REMOVED": "Все плейлисты были удалены.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "VLC_PLAYER_PATH_LABEL": "Путь к VLC плееру", "VLC_PLAYER_PATH": "Путь к VLC плееру", "VLC_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю VLC в вашей системе. \nОн будет использовать предоставленный путь к binary файлу VLC вместо пути по умолчанию.", @@ -257,7 +266,10 @@ "PORT_REQUIRED": "Порт обязателен", "PORT_MIN": "Порт должен быть не менее 1", "PORT_MAX": "Порт должен быть не более 65535", - "PORT_PATTERN": "Разрешены только цифры" + "PORT_PATTERN": "Разрешены только цифры", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Тёмная тема", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 6716209d8..f060cf1d7 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -186,15 +186,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "Uygulama içi kullanılan JSON formatında oynatma listelerini dışa aktar veya içe aktar.", "IMPORT_DATA": "İçe Aktar", "EXPORT_DATA": "Dışa Aktar", - "REMOVE_ALL": "Tüm oynatma listelerini kaldır", - "REMOVE_ALL_BUTTON": "Kaldır", - "REMOVE_ALL_DESCRIPTION": "Bu, uygulamadaki tüm oynatma listelerini kaldıracaktır.", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "Tüm oynatma listelerini kaldır", - "MESSAGE": "Uygulamadaki tüm oynatma listelerini silmek istediğinizden emin misiniz?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "İçe aktarma hatası, lütfen başka bir dosyayla deneyin.", - "PLAYLISTS_REMOVED": "Tüm oynatma listeleri kaldırıldı.", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "mpv yolu", "MPV_PLAYER_PATH_DESCRIPTION": "Sisteminizdeki mpv oynatıcı yolunu ayarlayın. $PATH'de bulunanı kullanmak yerine, sağlanan mpv ikili dosyası yolunu kullanacaktır.", "MPV_PLAYER_PATH": "mpv oynatıcı yolu", @@ -213,7 +222,10 @@ "PORT_REQUIRED": "Port gereklidir", "PORT_MIN": "Port en az 1 olmalıdır", "PORT_MAX": "Port en fazla 65535 olmalıdır", - "PORT_PATTERN": "Sadece rakamlara izin verilir" + "PORT_PATTERN": "Sadece rakamlara izin verilir", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "Koyu tema", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 4bb1ac2b2..cc692e567 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -94,14 +94,23 @@ "SETTINGS_SAVED": "配置已成功保存", "THEME": "主题", "SHOW_CAPTIONS": "显示字幕", - "REMOVE_ALL": "删除播放列表", - "REMOVE_ALL_BUTTON": "消除", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", "REMOVE_DIALOG": { - "TITLE": "删除所有播放列表", - "MESSAGE": "您确定要从应用程序中删除所有播放列表吗?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "LANGUAGE_DESCRIPTION": "选择应用程序的语言", - "PLAYLISTS_REMOVED": "所有播放列表均已删除。", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "VERSION_DESCRIPTION": "应用程序的当前版本", "IMPORT_DATA": "进口", "ADD_EPG_SOURCE": "添加EPG源", @@ -117,7 +126,11 @@ "VLC_PLAYER_PATH": "VLC播放器路径", "IMPORT_EXPORT_DATA_DESCRIPTION": "以应用程序中使用的 JSON 格式导出或导入播放列表。", "EPG_NOTE_URL_TEXT": "在这里检查一下", - "MPV_PLAYER_PATH_LABEL": "多用途车路径" + "MPV_PLAYER_PATH_LABEL": "多用途车路径", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "暗黑模式", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 51083f527..efc241ec0 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -146,15 +146,24 @@ "IMPORT_EXPORT_DATA_DESCRIPTION": "以應用程式內使用的 JSON 格式匯出或匯入播放清單。", "IMPORT_DATA": "匯入", "EXPORT_DATA": "匯出", - "REMOVE_ALL": "移除播放清單", - "REMOVE_ALL_BUTTON": "移除", - "REMOVE_ALL_DESCRIPTION": "這將從應用程式中移除所有現有的播放清單。", + "REMOVE_ALL": "Delete all playlists and playlist data", + "REMOVE_ALL_BUTTON": "Delete all", + "REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.", "REMOVE_DIALOG": { - "TITLE": "移除所有播放清單", - "MESSAGE": "您確定要從應用程式中刪除所有播放清單嗎?" + "TITLE": "Delete all playlists?", + "MESSAGE": "Delete all {{count}} playlists from the app. This also removes stored playlist data on this device.", + "SUMMARY_TITLE": "Playlist sources", + "CONSEQUENCES_TITLE": "This also removes", + "CONSEQUENCE_FAVORITES": "Favorites saved for these playlists", + "CONSEQUENCE_RECENTLY_VIEWED": "Recently viewed history", + "CONSEQUENCE_PLAYBACK": "Saved playback positions", + "CONSEQUENCE_DOWNLOADS": "Download history entries linked to those playlists", + "CONSEQUENCE_XTREAM_CACHE": "Imported Xtream categories and locally cached content", + "BACKUP_HINT": "Use Export first if you want a restorable backup before deleting.", + "CONFIRM": "Delete everything" }, "IMPORT_ERROR": "匯入錯誤,請嘗試另外一個檔案。", - "PLAYLISTS_REMOVED": "所有播放清單已移除。", + "PLAYLISTS_REMOVED": "All playlists and playlist data were removed.", "MPV_PLAYER_PATH_LABEL": "mpv 路徑", "MPV_PLAYER_PATH_DESCRIPTION": "設定您系統中 mpv 播放器的路徑。將使用提供的 mpv 二進位檔路徑,而非 $PATH 中的預設路徑。", "MPV_PLAYER_PATH": "mpv 播放器路徑", @@ -164,7 +173,10 @@ "REMOTE_CONTROL": "遠端控制", "REMOTE_CONTROL_DESCRIPTION": "啟用應用程式遠端控制功能。", "REMOTE_CONTROL_PORT": "遠端控制連接埠", - "REMOTE_CONTROL_PORT_DESCRIPTION": "用於遠端控制應用程式的連接埠。預設使用連接埠 8765。" + "REMOTE_CONTROL_PORT_DESCRIPTION": "用於遠端控制應用程式的連接埠。預設使用連接埠 8765。", + "REMOVE_ALL_IN_PROGRESS": "Deleting playlist data…", + "REMOVE_ALL_PROGRESS": "Deleting playlist data… {{current}} / {{total}}", + "PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again." }, "THEMES": { "DARK_THEME": "深色主題", diff --git a/docs/architecture/sqlite-db-worker.md b/docs/architecture/sqlite-db-worker.md index ab5f2837b..53b20596f 100644 --- a/docs/architecture/sqlite-db-worker.md +++ b/docs/architecture/sqlite-db-worker.md @@ -319,7 +319,9 @@ The UI now has explicit long-running state for destructive operations: 2. Xtream import overlay shows phase text and a cancel action 3. Xtream playlist rows show request-scoped progress and cancel actions 4. busy rows block repeat clicks while an operation is in flight -5. settings "remove all playlists" owns its own spinner/disabled state +5. settings "remove all playlists" owns its own spinner/disabled state and + consumes request-scoped DB operation events for progress text while the + worker deletes playlist data These changes matter because once SQLite work leaves the main thread, the renderer can actually paint the loading state instead of freezing.