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

@@ -0,0 +1,55 @@
<h2 mat-dialog-title>
{{ 'SETTINGS.REMOVE_DIALOG.TITLE' | translate }}
</h2>
<mat-dialog-content class="mat-typography settings-delete-dialog">
<p class="settings-delete-dialog__lead">
{{
'SETTINGS.REMOVE_DIALOG.MESSAGE'
| translate: { count: dialogData.summary.total }
}}
</p>
<section class="settings-delete-dialog__section">
<h3 class="settings-delete-dialog__section-title">
{{ 'SETTINGS.REMOVE_DIALOG.SUMMARY_TITLE' | translate }}
</h3>
<div class="settings-delete-dialog__summary">
@for (item of summaryItems(); track item.id) {
<div class="settings-delete-dialog__summary-item">
<div class="settings-delete-dialog__summary-label">
<mat-icon aria-hidden="true">{{ item.icon }}</mat-icon>
<span>{{ item.labelKey | translate }}</span>
</div>
<strong>{{ item.count }}</strong>
</div>
}
</div>
</section>
<section class="settings-delete-dialog__section">
<h3 class="settings-delete-dialog__section-title">
{{ 'SETTINGS.REMOVE_DIALOG.CONSEQUENCES_TITLE' | translate }}
</h3>
<ul class="settings-delete-dialog__list">
@for (key of consequenceKeys; track key) {
<li>{{ key | translate }}</li>
}
</ul>
</section>
<p class="settings-delete-dialog__hint">
{{ 'SETTINGS.REMOVE_DIALOG.BACKUP_HINT' | translate }}
</p>
</mat-dialog-content>
<mat-dialog-actions align="end" class="settings-delete-dialog__actions">
<button mat-button mat-dialog-close cdkFocusInitial>
{{ 'CANCEL' | translate }}
</button>
<button mat-flat-button [mat-dialog-close]="true" color="warn">
{{ 'SETTINGS.REMOVE_DIALOG.CONFIRM' | translate }}
</button>
</mat-dialog-actions>
@@ -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;
}
@@ -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<SettingsDeleteAllPlaylistsDialogComponent>;
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']);
});
});
@@ -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<SettingsDeleteAllPlaylistsDialogData>(MAT_DIALOG_DATA);
readonly summaryItems = computed<SettingsDeleteSummaryItem[]>(() => [
{
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;
}
@@ -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 @@
<p>
{{ 'SETTINGS.REMOVE_ALL_DESCRIPTION' | translate }}
</p>
<div class="danger-zone__summary" aria-live="polite">
<span class="danger-zone__summary-pill">
{{ 'HOME.PLAYLIST_TYPES.M3U' | translate }}
<strong>{{
playlistDeleteSummary().m3u
}}</strong>
</span>
<span class="danger-zone__summary-pill">
{{ 'HOME.PLAYLIST_TYPES.XTREAM' | translate }}
<strong>{{
playlistDeleteSummary().xtream
}}</strong>
</span>
<span class="danger-zone__summary-pill">
{{ 'HOME.PLAYLIST_TYPES.STALKER' | translate }}
<strong>{{
playlistDeleteSummary().stalker
}}</strong>
</span>
</div>
@if (removeAllProgressLabel(); as progressLabel) {
<p class="danger-zone__progress">
{{ progressLabel }}
</p>
}
</div>
<div class="setting-item__control">
<button
mat-button
class="danger-zone__button"
[class.danger-zone__button--busy]="
isRemovingAllPlaylists()
"
[disabled]="!canRemoveAllPlaylists()"
[attr.aria-busy]="isRemovingAllPlaylists()"
(click)="
$event.preventDefault();
$event.stopPropagation();
@@ -820,9 +852,19 @@
color="warn"
>
@if (isRemovingAllPlaylists()) {
<mat-spinner diameter="18" strokeWidth="3" />
<mat-progress-spinner
class="button-spinner"
diameter="16"
mode="indeterminate"
/>
<span class="inline-actions__label">{{
'SETTINGS.REMOVE_ALL_IN_PROGRESS'
| translate
}}</span>
} @else {
{{ 'SETTINGS.REMOVE_ALL_BUTTON' | translate }}
<span class="inline-actions__label">{{
'SETTINGS.REMOVE_ALL_BUTTON' | translate
}}</span>
}
</button>
</div>
@@ -1022,8 +1064,7 @@
<span class="support-btn__text">
<strong>Ko-fi</strong>
<small>{{
'SETTINGS.SUPPORT_KOFI_HINT'
| translate
'SETTINGS.SUPPORT_KOFI_HINT' | translate
}}</small>
</span>
</a>
@@ -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(
@@ -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> = {}
): 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<void>();
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> }) => {
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<string, number>) => {
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<boolean>((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;
})
+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;
+19 -7
View File
@@ -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": "السمة الداكنة",
+19 -7
View File
@@ -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": "المظهر الداكن",
+19 -7
View File
@@ -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": "Цёмная тэма",
+19 -7
View File
@@ -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",
+19 -7
View File
@@ -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": "Σκούρο θέμα",
+19 -7
View File
@@ -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",
+20 -9
View File
@@ -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",
+19 -7
View File
@@ -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é",
+19 -7
View File
@@ -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",
+19 -7
View File
@@ -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": "ダークテーマ",
+19 -6
View File
@@ -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": "어두운 테마",
+19 -7
View File
@@ -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",
+19 -7
View File
@@ -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",
+19 -7
View File
@@ -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",
+19 -7
View File
@@ -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": "Тёмная тема",
+19 -7
View File
@@ -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",
+19 -6
View File
@@ -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": "暗黑模式",
+19 -7
View File
@@ -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": "深色主題",
+3 -1
View File
@@ -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.