mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
refactor: improve Xtream refresh functionality with localization support
- Introduced new localization keys for Xtream refresh actions in multiple languages (ar, ary, by, de, el, en, es, fr, it, ja, ko, nl, pl, pt, ru, tr, zh, zhtw). - Implemented refresh preparation state management in PlaylistRefreshActionService. - Enhanced WorkspaceShellXtreamImportService to handle refresh preparation states and display appropriate labels. - Updated tests to cover new refresh preparation scenarios and ensure correct overlay display during refresh operations. Entire-Checkpoint: f957cd9849e0
This commit is contained in:
1 parent
e36c953865
commit
d0f590d8fe
24 files changed
+598
-44
No files matched your search
+43
-2
@@ -1,7 +1,14 @@
|
||||
import { XtreamImportPhaseTone } from './workspace-shell-constants';
|
||||
import { TranslateFn } from './workspace-shell-search-labels';
|
||||
import type { XtreamImportPhaseTone } from './workspace-shell-constants';
|
||||
import type { TranslateFn } from './workspace-shell-search-labels';
|
||||
|
||||
export type XtreamImportContentType = 'live' | 'vod' | 'series' | null;
|
||||
export type XtreamRefreshPreparationPhase =
|
||||
| 'collecting-user-data'
|
||||
| 'deleting-content'
|
||||
| 'deleting-categories'
|
||||
| string
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export function buildXtreamImportTypeLabel(
|
||||
contentType: XtreamImportContentType,
|
||||
@@ -39,6 +46,24 @@ export function buildXtreamImportPhaseTone(
|
||||
}
|
||||
}
|
||||
|
||||
export function buildXtreamRefreshPreparationPhaseLabel(
|
||||
phase: XtreamRefreshPreparationPhase,
|
||||
translate: TranslateFn
|
||||
): string {
|
||||
switch (phase) {
|
||||
case 'collecting-user-data':
|
||||
return translate('WORKSPACE.SHELL.XTREAM_REFRESH_COLLECTING_DATA');
|
||||
case 'deleting-content':
|
||||
return translate('WORKSPACE.SHELL.XTREAM_REFRESH_DELETING_CONTENT');
|
||||
case 'deleting-categories':
|
||||
return translate(
|
||||
'WORKSPACE.SHELL.XTREAM_REFRESH_DELETING_CATEGORIES'
|
||||
);
|
||||
default:
|
||||
return translate('WORKSPACE.SHELL.XTREAM_REFRESH_COLLECTING_DATA');
|
||||
}
|
||||
}
|
||||
|
||||
export function buildXtreamImportSourceLabel(
|
||||
tone: XtreamImportPhaseTone,
|
||||
translate: TranslateFn
|
||||
@@ -109,6 +134,22 @@ export function buildXtreamImportProgressLabel(
|
||||
});
|
||||
}
|
||||
|
||||
export function buildXtreamRefreshPreparationProgressLabel(
|
||||
currentCount: number,
|
||||
totalCount: number,
|
||||
translate: TranslateFn,
|
||||
formatNumber: (value: number) => string
|
||||
): string {
|
||||
if (totalCount === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return translate('WORKSPACE.SHELL.XTREAM_REFRESH_PROGRESS', {
|
||||
current: formatNumber(currentCount),
|
||||
total: formatNumber(totalCount),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatLocalizedNumber(
|
||||
value: number,
|
||||
currentLang: string | null | undefined,
|
||||
|
||||
+83
-21
@@ -2,8 +2,9 @@ import { computed, inject, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import { PlaylistRefreshActionService } from '@iptvnator/playlist/shared/util';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import { XtreamImportPhaseTone } from './helpers/workspace-shell-constants';
|
||||
import type { XtreamImportPhaseTone } from './helpers/workspace-shell-constants';
|
||||
import {
|
||||
buildXtreamImportDetailLabel,
|
||||
buildXtreamImportPhaseLabel,
|
||||
@@ -11,12 +12,17 @@ import {
|
||||
buildXtreamImportProgressLabel,
|
||||
buildXtreamImportSourceLabel,
|
||||
buildXtreamImportTypeLabel,
|
||||
buildXtreamRefreshPreparationPhaseLabel,
|
||||
buildXtreamRefreshPreparationProgressLabel,
|
||||
formatLocalizedNumber,
|
||||
} from './helpers/workspace-shell-import-labels';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceShellXtreamImportService {
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
private readonly playlistRefreshAction = inject(
|
||||
PlaylistRefreshActionService
|
||||
);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly isElectron = !!window.electron;
|
||||
|
||||
@@ -32,10 +38,22 @@ export class WorkspaceShellXtreamImportService {
|
||||
|
||||
readonly xtreamImportCount = this.xtreamStore.getImportCount;
|
||||
readonly xtreamItemsToImport = this.xtreamStore.itemsToImport;
|
||||
readonly xtreamActiveImportCount =
|
||||
this.xtreamStore.activeImportCurrentCount;
|
||||
readonly xtreamActiveItemsToImport =
|
||||
this.xtreamStore.activeImportTotalCount;
|
||||
readonly refreshPreparation = this.playlistRefreshAction.refreshPreparation;
|
||||
readonly activeRefreshPreparation = computed(
|
||||
() => this.refreshPreparation() ?? null
|
||||
);
|
||||
readonly xtreamActiveImportCount = computed(() => {
|
||||
const preparation = this.activeRefreshPreparation();
|
||||
return preparation
|
||||
? (preparation.current ?? 0)
|
||||
: this.xtreamStore.activeImportCurrentCount();
|
||||
});
|
||||
readonly xtreamActiveItemsToImport = computed(() => {
|
||||
const preparation = this.activeRefreshPreparation();
|
||||
return preparation
|
||||
? (preparation.total ?? 0)
|
||||
: this.xtreamStore.activeImportTotalCount();
|
||||
});
|
||||
readonly xtreamImportPhase = this.xtreamStore.currentImportPhase;
|
||||
readonly isCancellingXtreamImport = this.xtreamStore.isCancellingImport;
|
||||
|
||||
@@ -47,9 +65,13 @@ export class WorkspaceShellXtreamImportService {
|
||||
!this.xtreamStore.isCancellingImport()
|
||||
);
|
||||
|
||||
readonly xtreamImportTitleLabel = computed(() =>
|
||||
this.translateText('WORKSPACE.SHELL.XTREAM_IMPORT_TITLE')
|
||||
);
|
||||
readonly xtreamImportTitleLabel = computed(() => {
|
||||
if (this.activeRefreshPreparation()) {
|
||||
return this.translateText('WORKSPACE.SHELL.XTREAM_REFRESH_TITLE');
|
||||
}
|
||||
|
||||
return this.translateText('WORKSPACE.SHELL.XTREAM_IMPORT_TITLE');
|
||||
});
|
||||
|
||||
readonly xtreamImportTypeLabel = computed(() => {
|
||||
this.languageTick();
|
||||
@@ -59,24 +81,42 @@ export class WorkspaceShellXtreamImportService {
|
||||
);
|
||||
});
|
||||
|
||||
readonly xtreamImportProgressLabel = computed(() =>
|
||||
buildXtreamImportProgressLabel(
|
||||
readonly xtreamImportProgressLabel = computed(() => {
|
||||
const formatNumber = (value: number): string =>
|
||||
formatLocalizedNumber(
|
||||
value,
|
||||
this.translate.currentLang,
|
||||
this.translate.defaultLang
|
||||
);
|
||||
const preparation = this.activeRefreshPreparation();
|
||||
|
||||
if (preparation) {
|
||||
return buildXtreamRefreshPreparationProgressLabel(
|
||||
this.xtreamActiveImportCount(),
|
||||
this.xtreamActiveItemsToImport(),
|
||||
this.translateText,
|
||||
formatNumber
|
||||
);
|
||||
}
|
||||
|
||||
return buildXtreamImportProgressLabel(
|
||||
this.xtreamImportTypeLabel(),
|
||||
this.xtreamActiveImportCount(),
|
||||
this.xtreamActiveItemsToImport(),
|
||||
this.translateText,
|
||||
(value) =>
|
||||
formatLocalizedNumber(
|
||||
value,
|
||||
this.translate.currentLang,
|
||||
this.translate.defaultLang
|
||||
)
|
||||
)
|
||||
);
|
||||
formatNumber
|
||||
);
|
||||
});
|
||||
|
||||
readonly xtreamImportPhaseTone = computed<XtreamImportPhaseTone>(() =>
|
||||
buildXtreamImportPhaseTone(this.xtreamStore.currentImportPhase())
|
||||
);
|
||||
readonly xtreamImportPhaseTone = computed<XtreamImportPhaseTone>(() => {
|
||||
if (this.activeRefreshPreparation()) {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
return buildXtreamImportPhaseTone(
|
||||
this.xtreamStore.currentImportPhase()
|
||||
);
|
||||
});
|
||||
|
||||
readonly xtreamImportSourceLabel = computed(() => {
|
||||
this.languageTick();
|
||||
@@ -88,6 +128,15 @@ export class WorkspaceShellXtreamImportService {
|
||||
|
||||
readonly xtreamImportPhaseLabel = computed(() => {
|
||||
this.languageTick();
|
||||
const preparation = this.activeRefreshPreparation();
|
||||
|
||||
if (preparation) {
|
||||
return buildXtreamRefreshPreparationPhaseLabel(
|
||||
preparation.phase,
|
||||
this.translateText
|
||||
);
|
||||
}
|
||||
|
||||
return buildXtreamImportPhaseLabel(
|
||||
this.xtreamStore.currentImportPhase(),
|
||||
this.translateText
|
||||
@@ -96,6 +145,12 @@ export class WorkspaceShellXtreamImportService {
|
||||
|
||||
readonly xtreamImportDetailLabel = computed(() => {
|
||||
this.languageTick();
|
||||
if (this.activeRefreshPreparation()) {
|
||||
return this.translateText(
|
||||
'WORKSPACE.SHELL.XTREAM_REFRESH_DETAIL_LOCAL'
|
||||
);
|
||||
}
|
||||
|
||||
return buildXtreamImportDetailLabel(
|
||||
this.xtreamImportPhaseTone(),
|
||||
this.translateText
|
||||
@@ -107,6 +162,13 @@ export class WorkspaceShellXtreamImportService {
|
||||
!this.xtreamStore.contentInitBlockReason() &&
|
||||
this.xtreamStore.isImporting()
|
||||
);
|
||||
readonly isRefreshPreparationRunning = computed(() =>
|
||||
Boolean(this.activeRefreshPreparation())
|
||||
);
|
||||
|
||||
isRefreshPreparationRunningForPlaylist(playlistId: string): boolean {
|
||||
return this.refreshPreparation()?.playlistId === playlistId;
|
||||
}
|
||||
|
||||
cancelXtreamImport(): void {
|
||||
void this.xtreamStore.cancelImport();
|
||||
|
||||
+97
-1
@@ -5,7 +5,11 @@ import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
|
||||
import {
|
||||
PlaylistContextFacade,
|
||||
PlaylistRefreshActionService,
|
||||
type XtreamRefreshPreparationState,
|
||||
} from '@iptvnator/playlist/shared/util';
|
||||
import {
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
WorkspaceHeaderContextService,
|
||||
@@ -34,6 +38,7 @@ class MockXtreamStore {
|
||||
readonly categorySearchTerm = signal('');
|
||||
readonly isImporting = signal(false);
|
||||
readonly isCancellingImport = signal(false);
|
||||
readonly contentInitBlockReason = signal(null);
|
||||
readonly activeImportSessionId = signal<string | null>(null);
|
||||
readonly currentImportPhase = signal<string | null>(null);
|
||||
readonly activeImportContentType = signal<'live' | 'vod' | 'series' | null>(
|
||||
@@ -123,6 +128,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
typeof signal<PlaylistSignalMeta | null>
|
||||
>;
|
||||
let playlistsSignal: ReturnType<typeof signal<PlaylistSignalMeta[]>>;
|
||||
let refreshPreparationSignal: ReturnType<
|
||||
typeof signal<XtreamRefreshPreparationState | null>
|
||||
>;
|
||||
let stalkerStore: MockStalkerStore;
|
||||
let showDashboardSignal: ReturnType<typeof signal<boolean>>;
|
||||
let startupPreferences: {
|
||||
@@ -145,6 +153,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
{ _id: 'pl-1', serverUrl: 'http://example.com' },
|
||||
{ _id: 'pl-2', macAddress: '00:11:22:33' },
|
||||
]);
|
||||
refreshPreparationSignal = signal<XtreamRefreshPreparationState | null>(
|
||||
null
|
||||
);
|
||||
|
||||
router = {
|
||||
url: '/workspace/xtreams/pl-1/vod',
|
||||
@@ -230,6 +241,16 @@ describe('WorkspaceShellFacade', () => {
|
||||
activePlaylist: activePlaylistSignal,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PlaylistRefreshActionService,
|
||||
useValue: {
|
||||
canRefresh: jest.fn(() => true),
|
||||
isRefreshing: signal(false),
|
||||
refreshPreparation:
|
||||
refreshPreparationSignal.asReadonly(),
|
||||
refresh: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: {
|
||||
@@ -270,6 +291,14 @@ describe('WorkspaceShellFacade', () => {
|
||||
return `${params.type} imported: ${params.current} / ${params.total}`;
|
||||
}
|
||||
|
||||
if (
|
||||
key ===
|
||||
'WORKSPACE.SHELL.XTREAM_REFRESH_PROGRESS' &&
|
||||
params
|
||||
) {
|
||||
return `Local records processed: ${params.current} / ${params.total}`;
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
get: (key: string) => of(key),
|
||||
@@ -349,6 +378,73 @@ describe('WorkspaceShellFacade', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the Xtream overlay during refresh preparation for the active playlist', () => {
|
||||
refreshPreparationSignal.set({
|
||||
playlistId: 'pl-1',
|
||||
operationId: 'xtream-refresh-op',
|
||||
phase: 'collecting-user-data',
|
||||
});
|
||||
|
||||
expect(facade.showXtreamImportOverlay()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the Xtream overlay during refresh preparation on the dashboard', () => {
|
||||
facade.currentUrl.set('/workspace/dashboard');
|
||||
refreshPreparationSignal.set({
|
||||
playlistId: 'dashboard-xtream-playlist',
|
||||
operationId: 'xtream-refresh-op',
|
||||
phase: 'collecting-user-data',
|
||||
});
|
||||
|
||||
expect(facade.showXtreamImportOverlay()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the Xtream overlay for another playlist refresh preparation', () => {
|
||||
refreshPreparationSignal.set({
|
||||
playlistId: 'other-playlist',
|
||||
operationId: 'xtream-refresh-op',
|
||||
phase: 'collecting-user-data',
|
||||
});
|
||||
|
||||
expect(facade.showXtreamImportOverlay()).toBe(false);
|
||||
});
|
||||
|
||||
it('prefers refresh-preparation labels over import labels', () => {
|
||||
const xtreamStore = TestBed.inject(
|
||||
XtreamStore
|
||||
) as unknown as MockXtreamStore;
|
||||
const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService);
|
||||
|
||||
xtreamStore.isImporting.set(true);
|
||||
xtreamStore.currentImportPhase.set('loading-categories');
|
||||
xtreamStore.activeImportContentType.set('vod');
|
||||
xtreamStore.activeImportCurrentCount.set(2);
|
||||
xtreamStore.activeImportTotalCount.set(20);
|
||||
refreshPreparationSignal.set({
|
||||
playlistId: 'pl-1',
|
||||
operationId: 'xtream-refresh-op',
|
||||
phase: 'deleting-content',
|
||||
current: 5,
|
||||
total: 10,
|
||||
});
|
||||
|
||||
expect(xtreamImport.xtreamImportTitleLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_REFRESH_TITLE'
|
||||
);
|
||||
expect(xtreamImport.xtreamImportSourceLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_LOCAL_BADGE'
|
||||
);
|
||||
expect(xtreamImport.xtreamImportPhaseLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_REFRESH_DELETING_CONTENT'
|
||||
);
|
||||
expect(xtreamImport.xtreamImportDetailLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_REFRESH_DETAIL_LOCAL'
|
||||
);
|
||||
expect(xtreamImport.xtreamImportProgressLabel()).toBe(
|
||||
'Local records processed: 5 / 10'
|
||||
);
|
||||
});
|
||||
|
||||
it('builds a type-aware xtream import progress label', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService);
|
||||
|
||||
+15
-1
@@ -267,15 +267,29 @@ export class WorkspaceShellFacade {
|
||||
);
|
||||
private readonly xtreamImport = inject(WorkspaceShellXtreamImportService);
|
||||
readonly showXtreamImportOverlay = computed(() => {
|
||||
const route = this.currentRoute();
|
||||
const context = this.currentContext();
|
||||
const section = this.currentSection();
|
||||
const hasRefreshPreparation = Boolean(
|
||||
this.xtreamImport.refreshPreparation()
|
||||
);
|
||||
|
||||
if (route.kind === 'dashboard') {
|
||||
return hasRefreshPreparation;
|
||||
}
|
||||
|
||||
if (context?.provider !== 'xtreams') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPreparingCurrentPlaylist =
|
||||
this.xtreamImport.isRefreshPreparationRunningForPlaylist(
|
||||
context.playlistId
|
||||
);
|
||||
|
||||
return (
|
||||
this.xtreamImport.isImportRunning() &&
|
||||
(this.xtreamImport.isImportRunning() ||
|
||||
isPreparingCurrentPlaylist) &&
|
||||
(section === 'vod' ||
|
||||
section === 'live' ||
|
||||
section === 'series' ||
|
||||
|
||||
Reference in new issue
Block a user