mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
fix(migration): show startup preparation and recover source read failures (#1568)
* fix(migration): show startup preparation and recover source read failures * fix(migration): keep inventory reload failures recoverable * test(migration): type the deferred recovery hook * fix(migration): reconcile failed EPG cleanup before recovery * fix(migration): await settings before inventory readiness * style(migration): add a subtle theme-aware startup watermark
This commit is contained in:
1 parent
97b0264dee
commit
9f950a1530
37 files changed
+1024
-14
No files matched your search
@@ -0,0 +1,66 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
PlaylistActions,
|
||||
selectPlaylistsLoadFailed,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from '@iptvnator/m3u-state';
|
||||
import { AppStartupStatusComponent } from './app-startup-status.component';
|
||||
|
||||
describe('AppStartupStatusComponent', () => {
|
||||
beforeEach(() =>
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppStartupStatusComponent, TranslateModule.forRoot()],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
selectors: [
|
||||
{ selector: selectPlaylistsLoadingFlag, value: false },
|
||||
{ selector: selectPlaylistsLoadFailed, value: false },
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
it('keeps showing preparation while EPG reconciliation delays the route', () => {
|
||||
const store = TestBed.inject(MockStore);
|
||||
store.overrideSelector(selectPlaylistsLoadingFlag, true);
|
||||
const fixture = TestBed.createComponent(AppStartupStatusComponent);
|
||||
fixture.detectChanges();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('[role="status"]')
|
||||
).not.toBeNull();
|
||||
fixture.componentRef.setInput('routeReady', true);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('section')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not expose an empty workspace before sources finish loading', () => {
|
||||
const fixture = TestBed.createComponent(AppStartupStatusComponent);
|
||||
fixture.componentRef.setInput('routeReady', true);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.complete()).toBe(false);
|
||||
const store = TestBed.inject(MockStore);
|
||||
store.overrideSelector(selectPlaylistsLoadingFlag, true);
|
||||
store.refreshState();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.complete()).toBe(true);
|
||||
});
|
||||
|
||||
it('offers a retry on failure without rendering raw storage errors', () => {
|
||||
TestBed.inject(MockStore).overrideSelector(
|
||||
selectPlaylistsLoadFailed,
|
||||
true
|
||||
);
|
||||
const fixture = TestBed.createComponent(AppStartupStatusComponent);
|
||||
fixture.detectChanges();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('[role="alert"]')
|
||||
).not.toBeNull();
|
||||
expect(fixture.nativeElement.querySelector('mat-spinner')).toBeNull();
|
||||
const dispatch = jest.spyOn(TestBed.inject(MockStore), 'dispatch');
|
||||
fixture.nativeElement.querySelector('button').click();
|
||||
expect(dispatch).toHaveBeenCalledWith(PlaylistActions.loadPlaylists());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import {
|
||||
PlaylistActions,
|
||||
selectPlaylistsLoadFailed,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from '@iptvnator/m3u-state';
|
||||
|
||||
/** The first route also waits for settings and XMLTV source reconciliation. */
|
||||
@Component({
|
||||
selector: 'app-startup-status',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MatButtonModule, MatProgressSpinnerModule, TranslatePipe],
|
||||
template: `
|
||||
@if (!complete()) {
|
||||
<section
|
||||
[attr.role]="failed() ? 'alert' : 'status'"
|
||||
aria-live="polite"
|
||||
>
|
||||
<svg
|
||||
class="startup-watermark"
|
||||
viewBox="0 0 256 256"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<g fill="none" stroke="currentColor" stroke-width="5">
|
||||
<rect x="46" y="53" width="166" height="115" rx="2" />
|
||||
<path d="M128 168v32M67 200h122" />
|
||||
<path
|
||||
d="M94 101Q129 72 165 101M101 109Q129 85 158 109M108 117Q129 99 151 117"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M119 126Q129 120 139 126L129 138Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
@if (!failed()) {
|
||||
<mat-spinner diameter="32" aria-hidden="true" />
|
||||
}
|
||||
<h1>
|
||||
{{
|
||||
(failed() ? 'STARTUP.FAILED' : 'STARTUP.PREPARING')
|
||||
| translate
|
||||
}}
|
||||
</h1>
|
||||
<p>
|
||||
{{
|
||||
(failed()
|
||||
? 'STARTUP.FAILED_DETAIL'
|
||||
: 'STARTUP.PREPARING_DETAIL'
|
||||
) | translate
|
||||
}}
|
||||
</p>
|
||||
@if (failed()) {
|
||||
<button mat-flat-button type="button" (click)="retry()">
|
||||
{{ 'RETRY' | translate }}
|
||||
</button>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
section {
|
||||
app-region: drag;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: clip;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 24px;
|
||||
box-sizing: border-box;
|
||||
background: var(--app-content-bg);
|
||||
color: var(--app-heading-color);
|
||||
text-align: center;
|
||||
}
|
||||
.startup-watermark {
|
||||
position: absolute;
|
||||
right: -14%;
|
||||
bottom: -34%;
|
||||
width: clamp(420px, 76vw, 960px);
|
||||
height: auto;
|
||||
opacity: 0.07;
|
||||
transform: rotate(-14deg);
|
||||
mask-image: linear-gradient(135deg, transparent 15%, #000 78%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
section > :not(svg) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
max-width: 420px;
|
||||
color: var(--app-body-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
button {
|
||||
margin-top: 8px;
|
||||
app-region: no-drag;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class AppStartupStatusComponent {
|
||||
private readonly store = inject(Store);
|
||||
readonly routeReady = input(false);
|
||||
private readonly sourcesReady = this.store.selectSignal(
|
||||
selectPlaylistsLoadingFlag
|
||||
);
|
||||
readonly failed = this.store.selectSignal(selectPlaylistsLoadFailed);
|
||||
readonly complete = computed(
|
||||
() => this.routeReady() && this.sourcesReady()
|
||||
);
|
||||
|
||||
retry(): void {
|
||||
this.store.dispatch(PlaylistActions.loadPlaylists());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<router-outlet />
|
||||
<app-startup-status #startup [routeReady]="routeReady()" />
|
||||
<div [style.display]="startup.complete() ? 'contents' : 'none'">
|
||||
<router-outlet (activate)="routeReady.set(true)" />
|
||||
</div>
|
||||
|
||||
<app-epg-progress-panel />
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Component, effect, HostBinding, inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
HostBinding,
|
||||
inject,
|
||||
OnInit,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
import { Actions, ofType } from '@ngrx/effects';
|
||||
@@ -34,6 +41,7 @@ import { SettingsService } from './services/settings.service';
|
||||
import { PlaybackKeepAwakeService } from './services/playback-keep-awake.service';
|
||||
import { PlaylistOpenRequestService } from './services/playlist-open-request.service';
|
||||
import { AppUpdateNotificationPanelComponent } from './app-update-notification-panel.component';
|
||||
import { AppStartupStatusComponent } from './app-startup-status.component';
|
||||
|
||||
const debugAppComponent = createDevLogger('AppComponent');
|
||||
|
||||
@@ -41,6 +49,7 @@ const debugAppComponent = createDevLogger('AppComponent');
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
imports: [
|
||||
AppStartupStatusComponent,
|
||||
AppUpdateNotificationPanelComponent,
|
||||
EpgProgressPanelComponent,
|
||||
RouterOutlet,
|
||||
@@ -48,6 +57,7 @@ const debugAppComponent = createDevLogger('AppComponent');
|
||||
],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
readonly routeReady = signal(false);
|
||||
@HostBinding('class.macos-platform') get isMacOS() {
|
||||
return this.runtime.isMacOS;
|
||||
}
|
||||
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "لا توجد بيانات للحساب",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "لا تنشر هذه البوابة معلومات الحساب لهذا الجهاز."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "جارٍ تجهيز المكتبة",
|
||||
"PREPARING_DETAIL": "جارٍ تحميل المصادر والتحقق من دليل البرامج. قد تستغرق المكتبات الكبيرة وقتًا أطول.",
|
||||
"FAILED": "تعذر تحميل المصادر",
|
||||
"FAILED_DETAIL": "أعد محاولة قراءة المصادر المحفوظة. لم تُحذف بياناتك."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "ما كايناش بيانات الحساب",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "هاد البوابة ما كتعطيش معلومات الحساب لهاد الجهاز."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "كنوجدو المكتبة ديالك",
|
||||
"PREPARING_DETAIL": "كنحمّلو المصادر وكنراجعو دليل البرامج. المكتبات الكبيرة تقدر تاخد وقت أكثر.",
|
||||
"FAILED": "ما قدرناش نحمّلو المصادر",
|
||||
"FAILED_DETAIL": "عاود جرّب تقرا المصادر المحفوظة. البيانات ديالك ما تمسحوش."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Няма даных пра акаўнт",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Гэты партал не дае інфармацыі пра акаўнт для гэтай прылады."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Падрыхтоўка бібліятэкі",
|
||||
"PREPARING_DETAIL": "Загружаем крыніцы і правяраем тэлепраграму. Вялікая бібліятэка можа патрабаваць больш часу.",
|
||||
"FAILED": "Не ўдалося загрузіць крыніцы",
|
||||
"FAILED_DETAIL": "Паўтарыце чытанне захаваных крыніц. Вашы даныя не выдалены."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Keine Kontodaten",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Dieses Portal stellt für dieses Gerät keine Kontoinformationen bereit."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Bibliothek wird vorbereitet",
|
||||
"PREPARING_DETAIL": "Quellen werden geladen und der Programmführer geprüft. Große Bibliotheken können etwas länger dauern.",
|
||||
"FAILED": "Quellen konnten nicht geladen werden",
|
||||
"FAILED_DETAIL": "Versuche erneut, die gespeicherten Quellen zu lesen. Deine Daten wurden nicht gelöscht."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Δεν υπάρχουν στοιχεία λογαριασμού",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Αυτή η πύλη δεν δημοσιεύει πληροφορίες λογαριασμού για αυτήν τη συσκευή."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Προετοιμασία βιβλιοθήκης",
|
||||
"PREPARING_DETAIL": "Φόρτωση πηγών και έλεγχος του οδηγού προγράμματος. Οι μεγάλες βιβλιοθήκες μπορεί να χρειαστούν περισσότερο χρόνο.",
|
||||
"FAILED": "Δεν ήταν δυνατή η φόρτωση των πηγών",
|
||||
"FAILED_DETAIL": "Δοκιμάστε ξανά την ανάγνωση των αποθηκευμένων πηγών. Τα δεδομένα σας δεν διαγράφηκαν."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "No account details",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "This portal does not publish account information for this device."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Preparing your library",
|
||||
"PREPARING_DETAIL": "Loading your sources and checking the programme guide. Large libraries may take a little longer.",
|
||||
"FAILED": "Your sources could not be loaded",
|
||||
"FAILED_DETAIL": "Retry to read your saved sources again. Your data has not been deleted."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Sin datos de la cuenta",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Este portal no publica información de la cuenta para este dispositivo."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Preparando tu biblioteca",
|
||||
"PREPARING_DETAIL": "Cargando fuentes y comprobando la guía de programación. Las bibliotecas grandes pueden tardar un poco más.",
|
||||
"FAILED": "No se pudieron cargar tus fuentes",
|
||||
"FAILED_DETAIL": "Vuelve a intentar leer las fuentes guardadas. Tus datos no se han eliminado."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Aucune donnée de compte",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Ce portail ne publie pas d’informations de compte pour cet appareil."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Préparation de la bibliothèque",
|
||||
"PREPARING_DETAIL": "Chargement des sources et vérification du guide TV. Les grandes bibliothèques peuvent prendre plus de temps.",
|
||||
"FAILED": "Impossible de charger vos sources",
|
||||
"FAILED_DETAIL": "Réessayez de lire les sources enregistrées. Vos données n’ont pas été supprimées."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Nincsenek fiókadatok",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Ez a portál nem tesz közzé fiókadatokat ehhez az eszközhöz."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Könyvtár előkészítése",
|
||||
"PREPARING_DETAIL": "Források betöltése és a műsorújság ellenőrzése. Nagy könyvtárak esetén ez tovább tarthat.",
|
||||
"FAILED": "Nem sikerült betölteni a forrásokat",
|
||||
"FAILED_DETAIL": "Próbáld újra beolvasni a mentett forrásokat. Az adataid nem lettek törölve."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Nessun dato account",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Questo portale non pubblica informazioni sull’account per questo dispositivo."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Preparazione della libreria",
|
||||
"PREPARING_DETAIL": "Caricamento delle sorgenti e verifica della guida TV. Le librerie grandi possono richiedere più tempo.",
|
||||
"FAILED": "Impossibile caricare le sorgenti",
|
||||
"FAILED_DETAIL": "Riprova a leggere le sorgenti salvate. I tuoi dati non sono stati eliminati."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "アカウント情報なし",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "このポータルは、このデバイスのアカウント情報を公開していません。"
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "ライブラリを準備しています",
|
||||
"PREPARING_DETAIL": "ソースを読み込み、番組表を確認しています。大きなライブラリでは時間がかかる場合があります。",
|
||||
"FAILED": "ソースを読み込めませんでした",
|
||||
"FAILED_DETAIL": "保存したソースの読み込みを再試行してください。データは削除されていません。"
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "계정 정보 없음",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "이 포털은 이 기기에 대한 계정 정보를 제공하지 않습니다."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "라이브러리 준비 중",
|
||||
"PREPARING_DETAIL": "소스를 불러오고 편성표를 확인하고 있습니다. 라이브러리가 크면 시간이 더 걸릴 수 있습니다.",
|
||||
"FAILED": "소스를 불러오지 못했습니다",
|
||||
"FAILED_DETAIL": "저장된 소스를 다시 읽어 보세요. 데이터는 삭제되지 않았습니다."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Geen accountgegevens",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Dit portaal publiceert geen accountinformatie voor dit apparaat."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Bibliotheek voorbereiden",
|
||||
"PREPARING_DETAIL": "Bronnen laden en de programmagids controleren. Grote bibliotheken kunnen wat meer tijd kosten.",
|
||||
"FAILED": "Je bronnen konden niet worden geladen",
|
||||
"FAILED_DETAIL": "Probeer je opgeslagen bronnen opnieuw te lezen. Je gegevens zijn niet verwijderd."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Brak danych konta",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Ten portal nie udostępnia informacji o koncie dla tego urządzenia."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Przygotowywanie biblioteki",
|
||||
"PREPARING_DETAIL": "Wczytywanie źródeł i sprawdzanie programu TV. Duże biblioteki mogą wymagać więcej czasu.",
|
||||
"FAILED": "Nie udało się wczytać źródeł",
|
||||
"FAILED_DETAIL": "Spróbuj ponownie odczytać zapisane źródła. Twoje dane nie zostały usunięte."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Sem dados da conta",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Este portal não publica informações da conta para este dispositivo."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "A preparar a biblioteca",
|
||||
"PREPARING_DETAIL": "A carregar fontes e verificar o guia de programação. Bibliotecas grandes podem demorar mais.",
|
||||
"FAILED": "Não foi possível carregar as fontes",
|
||||
"FAILED_DETAIL": "Tente ler novamente as fontes guardadas. Os seus dados não foram eliminados."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Нет данных об аккаунте",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Этот портал не предоставляет информацию об аккаунте для этого устройства."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Подготовка библиотеки",
|
||||
"PREPARING_DETAIL": "Загружаем источники и проверяем телепрограмму. Большая библиотека может потребовать больше времени.",
|
||||
"FAILED": "Не удалось загрузить источники",
|
||||
"FAILED_DETAIL": "Повторите чтение сохранённых источников. Ваши данные не удалены."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "Hesap verisi yok",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "Bu portal, bu cihaz için hesap bilgisi yayınlamıyor."
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "Kitaplığınız hazırlanıyor",
|
||||
"PREPARING_DETAIL": "Kaynaklar yükleniyor ve program rehberi kontrol ediliyor. Büyük kitaplıklar biraz daha uzun sürebilir.",
|
||||
"FAILED": "Kaynaklarınız yüklenemedi",
|
||||
"FAILED_DETAIL": "Kayıtlı kaynaklarınızı okumayı yeniden deneyin. Verileriniz silinmedi."
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "无账户数据",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "该门户不为此设备提供账户信息。"
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "正在准备媒体库",
|
||||
"PREPARING_DETAIL": "正在加载来源并检查节目指南。较大的媒体库可能需要更长时间。",
|
||||
"FAILED": "无法加载来源",
|
||||
"FAILED_DETAIL": "请重试读取已保存的来源。您的数据未被删除。"
|
||||
}
|
||||
}
|
||||
@@ -1718,5 +1718,11 @@
|
||||
"NO_ACCOUNT_DATA_TITLE": "沒有帳戶資料",
|
||||
"NO_ACCOUNT_DATA_DESCRIPTION": "此入口網站不提供此裝置的帳戶資訊。"
|
||||
}
|
||||
},
|
||||
"STARTUP": {
|
||||
"PREPARING": "正在準備媒體庫",
|
||||
"PREPARING_DETAIL": "正在載入來源並檢查節目指南。較大的媒體庫可能需要更長時間。",
|
||||
"FAILED": "無法載入來源",
|
||||
"FAILED_DETAIL": "請重試讀取已儲存的來源。您的資料未被刪除。"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user