mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -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
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "تبديل المشغل إلى {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "تعيين {{name}} كمشغل الفيديو النشط",
|
||||
"SWITCH_PLAYER_FEEDBACK": "تم تعيين المشغل إلى {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "جارٍ تحديث قائمة التشغيل",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "جارٍ حفظ بيانات مكتبتك...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "جارٍ إزالة البث المخزّن مؤقتًا...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "جارٍ إزالة الفئات المخزّنة مؤقتًا...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "نحتفظ بالمفضلة وسجل المشاهدة والفئات المخفية وتقدم التشغيل لاستعادتها بعد المزامنة.",
|
||||
"XTREAM_REFRESH_PROGRESS": "السجلات المحلية المعالجة: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "المصادر",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "بدل المشغل ل{{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "حدد {{name}} كمشغل الفيديو النشط",
|
||||
"SWITCH_PLAYER_FEEDBACK": "المشغل تحدد ل{{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "كنحدّثو البلايليست",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "كنحافظو على معطيات المكتبة ديالك...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "كنحيدو الستريمات المخزنة...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "كنحيدو التصنيفات المخزنة...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "كنخليو المفضلات، تاريخ المشاهدة، التصنيفات المخفية، وتقدم التشغيل واجدين باش يرجعو من بعد المزامنة.",
|
||||
"XTREAM_REFRESH_PROGRESS": "السجلات المحلية اللي تجهزات: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "المصادر",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Пераключыць прайгравальнік на {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Усталяваць {{name}} як актыўны відэапрайгравальнік",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Прайгравальнік усталяваны: {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Абнаўленне плэйліста",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Захаванне даных вашай бібліятэкі...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Выдаленне кэшаваных патокаў...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Выдаленне кэшаваных катэгорый...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Выбранае, гісторыя прагляду, схаваныя катэгорыі і прагрэс прайгравання будуць падрыхтаваны для аднаўлення пасля сінхранізацыі.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Апрацавана лакальных запісаў: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Крыніцы",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Player auf {{name}} wechseln",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "{{name}} als aktiven Videoplayer festlegen",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Player auf {{name}} festgelegt"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Playlist wird aktualisiert",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Mediathekdaten werden gesichert...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Zwischengespeicherte Streams werden entfernt...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Zwischengespeicherte Kategorien werden entfernt...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Favoriten, Wiedergabeverlauf, ausgeblendete Kategorien und Wiedergabefortschritt werden für die Wiederherstellung nach der Synchronisierung vorbereitet.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Lokale Einträge verarbeitet: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Quellen",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Εναλλαγή προγράμματος αναπαραγωγής σε {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Ορισμός του {{name}} ως ενεργό πρόγραμμα αναπαραγωγής βίντεο",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Το πρόγραμμα αναπαραγωγής ορίστηκε σε {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Ανανέωση λίστας αναπαραγωγής",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Διατήρηση των δεδομένων της βιβλιοθήκης σας...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Αφαίρεση αποθηκευμένων ροών...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Αφαίρεση αποθηκευμένων κατηγοριών...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Τα αγαπημένα, το ιστορικό προβολής, οι κρυφές κατηγορίες και η πρόοδος αναπαραγωγής ετοιμάζονται για επαναφορά μετά τον συγχρονισμό.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Τοπικές εγγραφές που επεξεργάστηκαν: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Πηγές",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Switch player to {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Set {{name}} as the active video player",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Player set to {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Refreshing playlist",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Preserving your library data...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Removing cached streams...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Removing cached categories...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Keeping favorites, watch history, hidden categories, and playback progress ready for restore after sync.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Local records processed: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Sources",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Cambiar reproductor a {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Establecer {{name}} como reproductor de vídeo activo",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Reproductor establecido a {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Actualizando lista de reproducción",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Conservando los datos de tu biblioteca...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Eliminando streams en caché...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Eliminando categorías en caché...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Guardamos favoritos, historial de reproducción, categorías ocultas y progreso para restaurarlos después de la sincronización.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Registros locales procesados: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Fuentes",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Basculer le lecteur sur {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Définir {{name}} comme lecteur vidéo actif",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Lecteur défini sur {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Actualisation de la playlist",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Préservation des données de votre bibliothèque...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Suppression des flux en cache...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Suppression des catégories en cache...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Les favoris, l’historique de lecture, les catégories masquées et la progression sont préparés pour être restaurés après la synchronisation.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Enregistrements locaux traités : {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Sources",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Passa al lettore {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Imposta {{name}} come lettore video attivo",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Lettore impostato su {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Aggiornamento playlist",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Conservazione dei dati della libreria...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Rimozione degli stream in cache...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Rimozione delle categorie in cache...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Preferiti, cronologia di visione, categorie nascoste e avanzamento di riproduzione vengono preparati per il ripristino dopo la sincronizzazione.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Record locali elaborati: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Sorgenti",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "プレーヤーを{{name}}に切り替え",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "{{name}}をアクティブなビデオプレーヤーに設定",
|
||||
"SWITCH_PLAYER_FEEDBACK": "プレーヤーを{{name}}に設定しました"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "プレイリストを更新中",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "ライブラリデータを保持しています...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "キャッシュ済みストリームを削除しています...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "キャッシュ済みカテゴリを削除しています...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "お気に入り、視聴履歴、非表示カテゴリ、再生進捗を同期後に復元できるよう準備しています。",
|
||||
"XTREAM_REFRESH_PROGRESS": "処理済みローカルレコード: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "ソース",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "플레이어를 {{name}}(으)로 전환",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "{{name}}을(를) 활성 비디오 플레이어로 설정",
|
||||
"SWITCH_PLAYER_FEEDBACK": "플레이어가 {{name}}(으)로 설정되었습니다"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "재생목록 새로 고침 중",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "라이브러리 데이터를 보존하는 중...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "캐시된 스트림을 제거하는 중...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "캐시된 카테고리를 제거하는 중...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "즐겨찾기, 시청 기록, 숨긴 카테고리, 재생 진행률을 동기화 후 복원할 수 있도록 준비합니다.",
|
||||
"XTREAM_REFRESH_PROGRESS": "처리된 로컬 레코드: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "소스",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Speler wisselen naar {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Stel {{name}} in als de actieve videospeler",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Speler ingesteld op {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Afspeellijst vernieuwen",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Bibliotheekgegevens worden bewaard...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Gecachte streams worden verwijderd...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Gecachte categorieën worden verwijderd...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Favorieten, kijkgeschiedenis, verborgen categorieën en afspeelvoortgang worden klaargezet voor herstel na de synchronisatie.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Lokale records verwerkt: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Bronnen",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Przełącz odtwarzacz na {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Ustaw {{name}} jako aktywny odtwarzacz wideo",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Odtwarzacz ustawiony na {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Odświeżanie listy odtwarzania",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Zachowywanie danych biblioteki...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Usuwanie strumieni z pamięci podręcznej...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Usuwanie kategorii z pamięci podręcznej...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Ulubione, historia oglądania, ukryte kategorie i postęp odtwarzania są przygotowywane do przywrócenia po synchronizacji.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Przetworzone rekordy lokalne: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Źródła",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Alternar reprodutor para {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Definir {{name}} como reprodutor de vídeo ativo",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Reprodutor definido como {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Atualizando playlist",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Preservando os dados da sua biblioteca...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Removendo streams em cache...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Removendo categorias em cache...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Favoritos, histórico de visualização, categorias ocultas e progresso de reprodução serão mantidos para restauração após a sincronização.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Registros locais processados: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Fontes",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Переключить плеер на {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "Сделать {{name}} активным видеоплеером",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Плеер переключен на {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Обновление плейлиста",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Сохраняем данные вашей библиотеки...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Удаляем кэшированные потоки...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Удаляем кэшированные категории...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Избранное, история просмотра, скрытые категории и прогресс воспроизведения будут подготовлены для восстановления после синхронизации.",
|
||||
"XTREAM_REFRESH_PROGRESS": "Обработано локальных записей: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Источники",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "Oynatıcıyı {{name}} olarak değiştir",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "{{name}} oynatıcısını etkin video oynatıcı olarak ayarla",
|
||||
"SWITCH_PLAYER_FEEDBACK": "Oynatıcı {{name}} olarak ayarlandı"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "Oynatma listesi yenileniyor",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "Kitaplık verileriniz korunuyor...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "Önbelleğe alınmış yayınlar kaldırılıyor...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "Önbelleğe alınmış kategoriler kaldırılıyor...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "Favoriler, izleme geçmişi, gizli kategoriler ve oynatma ilerlemesi eşitlemeden sonra geri yüklenmek üzere hazırlanıyor.",
|
||||
"XTREAM_REFRESH_PROGRESS": "İşlenen yerel kayıtlar: {{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "Kaynaklar",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "切换播放器为 {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "将 {{name}} 设为当前视频播放器",
|
||||
"SWITCH_PLAYER_FEEDBACK": "播放器已设为 {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "正在刷新播放列表",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "正在保留你的媒体库数据...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "正在移除缓存的流...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "正在移除缓存的分类...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "收藏、观看历史、隐藏分类和播放进度会保留,以便同步后恢复。",
|
||||
"XTREAM_REFRESH_PROGRESS": "已处理本地记录:{{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "源",
|
||||
|
||||
@@ -891,7 +891,13 @@
|
||||
"SWITCH_PLAYER_LABEL": "切換播放器至 {{name}}",
|
||||
"SWITCH_PLAYER_DESCRIPTION": "將 {{name}} 設為使用中的影片播放器",
|
||||
"SWITCH_PLAYER_FEEDBACK": "播放器已設為 {{name}}"
|
||||
}
|
||||
},
|
||||
"XTREAM_REFRESH_TITLE": "正在重新整理播放清單",
|
||||
"XTREAM_REFRESH_COLLECTING_DATA": "正在保留你的媒體庫資料...",
|
||||
"XTREAM_REFRESH_DELETING_CONTENT": "正在移除快取的串流...",
|
||||
"XTREAM_REFRESH_DELETING_CATEGORIES": "正在移除快取的分類...",
|
||||
"XTREAM_REFRESH_DETAIL_LOCAL": "我的最愛、觀看記錄、隱藏分類和播放進度會保留,以便同步後還原。",
|
||||
"XTREAM_REFRESH_PROGRESS": "已處理本機記錄:{{current}} / {{total}}"
|
||||
},
|
||||
"SOURCES": {
|
||||
"TITLE": "來源",
|
||||
|
||||
@@ -15,6 +15,27 @@ import { Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
import { PlaylistContextFacade } from './playlist-context.facade';
|
||||
import { PlaylistRefreshActionService } from './playlist-refresh-action.service';
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function createAbortError(): Error {
|
||||
const error = new Error('Cancelled');
|
||||
error.name = 'AbortError';
|
||||
return error;
|
||||
}
|
||||
|
||||
async function waitForRefreshPreparationPaint(): Promise<void> {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 160));
|
||||
}
|
||||
|
||||
function createPlaylistMeta(
|
||||
overrides: Partial<PlaylistMeta> = {}
|
||||
): PlaylistMeta {
|
||||
@@ -239,6 +260,141 @@ describe('PlaylistRefreshActionService', () => {
|
||||
dateNowSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('sets refresh-preparation state immediately after Xtream refresh confirmation', async () => {
|
||||
const item = createPlaylistMeta();
|
||||
const refresh = createDeferred<{
|
||||
success: boolean;
|
||||
favorites: [];
|
||||
recentlyViewed: [];
|
||||
hiddenCategories: [];
|
||||
}>();
|
||||
let confirmPromise: Promise<void> | undefined;
|
||||
|
||||
databaseService.deleteXtreamPlaylistContent.mockReturnValue(
|
||||
refresh.promise
|
||||
);
|
||||
dialogService.openConfirmDialog.mockImplementation(
|
||||
({ onConfirm }: { onConfirm?: () => Promise<void> }) => {
|
||||
confirmPromise = onConfirm?.();
|
||||
}
|
||||
);
|
||||
|
||||
service.refresh(item);
|
||||
|
||||
expect(service.refreshPreparation()).toEqual({
|
||||
playlistId: item._id,
|
||||
operationId: 'xtream-refresh-op',
|
||||
phase: 'collecting-user-data',
|
||||
});
|
||||
|
||||
refresh.resolve({
|
||||
success: true,
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
hiddenCategories: [],
|
||||
});
|
||||
await confirmPromise;
|
||||
|
||||
expect(service.refreshPreparation()).toBeNull();
|
||||
});
|
||||
|
||||
it('updates refresh-preparation phase and progress from worker events', async () => {
|
||||
const item = createPlaylistMeta();
|
||||
const refresh = createDeferred<{
|
||||
success: boolean;
|
||||
favorites: [];
|
||||
recentlyViewed: [];
|
||||
hiddenCategories: [];
|
||||
}>();
|
||||
let confirmPromise: Promise<void> | undefined;
|
||||
|
||||
databaseService.deleteXtreamPlaylistContent.mockImplementation(
|
||||
(
|
||||
_playlistId: string,
|
||||
options?: {
|
||||
operationId?: string;
|
||||
onEvent?: (event: any) => void;
|
||||
}
|
||||
) => {
|
||||
options?.onEvent?.({
|
||||
operation: 'delete-xtream-content',
|
||||
operationId: 'xtream-refresh-op',
|
||||
status: 'progress',
|
||||
phase: 'deleting-content',
|
||||
current: 50,
|
||||
total: 100,
|
||||
});
|
||||
|
||||
return refresh.promise;
|
||||
}
|
||||
);
|
||||
dialogService.openConfirmDialog.mockImplementation(
|
||||
({ onConfirm }: { onConfirm?: () => Promise<void> }) => {
|
||||
confirmPromise = onConfirm?.();
|
||||
}
|
||||
);
|
||||
|
||||
service.refresh(item);
|
||||
await waitForRefreshPreparationPaint();
|
||||
|
||||
expect(service.refreshPreparation()).toEqual({
|
||||
playlistId: item._id,
|
||||
operationId: 'xtream-refresh-op',
|
||||
phase: 'deleting-content',
|
||||
current: 50,
|
||||
total: 100,
|
||||
});
|
||||
|
||||
refresh.resolve({
|
||||
success: true,
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
hiddenCategories: [],
|
||||
});
|
||||
await confirmPromise;
|
||||
});
|
||||
|
||||
it.each([
|
||||
['abort', createAbortError()],
|
||||
['error', new Error('Refresh failed')],
|
||||
])(
|
||||
'clears refresh-preparation state after Xtream refresh %s',
|
||||
async (_caseName, error) => {
|
||||
const item = createPlaylistMeta();
|
||||
const consoleErrorSpy = jest
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation();
|
||||
const refresh = createDeferred<{
|
||||
success: boolean;
|
||||
favorites: [];
|
||||
recentlyViewed: [];
|
||||
hiddenCategories: [];
|
||||
}>();
|
||||
let confirmPromise: Promise<void> | undefined;
|
||||
|
||||
databaseService.deleteXtreamPlaylistContent.mockReturnValue(
|
||||
refresh.promise
|
||||
);
|
||||
dialogService.openConfirmDialog.mockImplementation(
|
||||
({ onConfirm }: { onConfirm?: () => Promise<void> }) => {
|
||||
confirmPromise = onConfirm?.();
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
service.refresh(item);
|
||||
expect(service.refreshPreparation()).not.toBeNull();
|
||||
|
||||
refresh.reject(error);
|
||||
await confirmPromise;
|
||||
|
||||
expect(service.refreshPreparation()).toBeNull();
|
||||
} finally {
|
||||
consoleErrorSpy.mockRestore();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it('marks the active M3U route as loading before refreshing and clears loading after the update action', async () => {
|
||||
const item = createPlaylistMeta({
|
||||
_id: 'playlist-1',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { DialogService } from 'components';
|
||||
import {
|
||||
DatabaseService,
|
||||
type DbOperationEvent,
|
||||
isDbAbortError,
|
||||
PlaybackPositionService,
|
||||
PlaylistRefreshService,
|
||||
@@ -15,6 +16,14 @@ import { ChannelActions, PlaylistActions } from 'm3u-state';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
import { PlaylistContextFacade } from './playlist-context.facade';
|
||||
|
||||
export interface XtreamRefreshPreparationState {
|
||||
playlistId: string;
|
||||
operationId: string;
|
||||
phase: string;
|
||||
current?: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PlaylistRefreshActionService {
|
||||
private readonly router = inject(Router);
|
||||
@@ -30,7 +39,11 @@ export class PlaylistRefreshActionService {
|
||||
XtreamPendingRestoreService
|
||||
);
|
||||
|
||||
private readonly refreshPreparationState =
|
||||
signal<XtreamRefreshPreparationState | null>(null);
|
||||
|
||||
readonly isRefreshing = signal(false);
|
||||
readonly refreshPreparation = this.refreshPreparationState.asReadonly();
|
||||
|
||||
canRefresh(playlist: PlaylistMeta | null): boolean {
|
||||
if (!playlist || !window.electron) {
|
||||
@@ -69,6 +82,11 @@ export class PlaylistRefreshActionService {
|
||||
this.isRefreshing.set(true);
|
||||
const operationId =
|
||||
this.databaseService.createOperationId('xtream-refresh');
|
||||
this.refreshPreparationState.set({
|
||||
playlistId: item._id,
|
||||
operationId,
|
||||
phase: 'collecting-user-data',
|
||||
});
|
||||
|
||||
try {
|
||||
this.snackBar.open(
|
||||
@@ -78,13 +96,22 @@ export class PlaylistRefreshActionService {
|
||||
undefined,
|
||||
{ duration: 2000 }
|
||||
);
|
||||
await this.waitForRefreshPreparationPaint();
|
||||
|
||||
const updateDate = Date.now();
|
||||
const [restoreState, playbackPositions] = await Promise.all(
|
||||
[
|
||||
this.databaseService.deleteXtreamPlaylistContent(
|
||||
item._id,
|
||||
{ operationId }
|
||||
{
|
||||
operationId,
|
||||
onEvent: (event) =>
|
||||
this.updateRefreshPreparationFromEvent(
|
||||
item._id,
|
||||
operationId,
|
||||
event
|
||||
),
|
||||
}
|
||||
),
|
||||
this.playbackPositionService.getAllPlaybackPositions(
|
||||
item._id
|
||||
@@ -127,6 +154,7 @@ export class PlaylistRefreshActionService {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this.clearRefreshPreparation(operationId);
|
||||
this.isRefreshing.set(false);
|
||||
}
|
||||
},
|
||||
@@ -208,4 +236,53 @@ export class PlaylistRefreshActionService {
|
||||
|
||||
return this.translate.instant('HOME.PLAYLISTS.PLAYLIST_UPDATE_ERROR');
|
||||
}
|
||||
|
||||
private updateRefreshPreparationFromEvent(
|
||||
playlistId: string,
|
||||
operationId: string,
|
||||
event: DbOperationEvent
|
||||
): void {
|
||||
if (event.operationId && event.operationId !== operationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshPreparationState.update((current) => {
|
||||
if (!current || current.operationId !== operationId) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
playlistId,
|
||||
operationId,
|
||||
phase: event.phase ?? current.phase,
|
||||
current: event.current ?? current.current,
|
||||
total: event.total ?? current.total,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private clearRefreshPreparation(operationId: string): void {
|
||||
this.refreshPreparationState.update((current) =>
|
||||
current?.operationId === operationId ? null : current
|
||||
);
|
||||
}
|
||||
|
||||
private async waitForRefreshPreparationPaint(): Promise<void> {
|
||||
const minimumVisibleDelayMs = 120;
|
||||
|
||||
// Give Angular a paint opportunity before small cached playlists can
|
||||
// finish cleanup and clear the preparation state.
|
||||
const resolveAfterDelay = (resolve: () => void): void => {
|
||||
setTimeout(resolve, minimumVisibleDelayMs);
|
||||
};
|
||||
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
await new Promise<void>(resolveAfterDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => resolveAfterDelay(resolve));
|
||||
});
|
||||
}
|
||||
}
|
||||
+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