diff --git a/src/App.svelte b/src/App.svelte index e2137c4..ec11eac 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -52,6 +52,21 @@ let InfoModal = $state(null); let UnsyncedChangesModal = $state(null); let AuthModal = $state(null); +const authModalOpen = $derived( + !auth.loggedIn && !auth.autoLogin && !auth.authFailed, +); +const playlistModalOpen = $derived( + !!(ui.playlistForm || ui.confirmDeletePlaylist), +); +const radioModalOpen = $derived( + !offlineManager.isOfflineMode && + !!(ui.stationForm || ui.confirmDeleteStation), +); +const shareModalOpen = $derived( + !offlineManager.isOfflineMode && !!ui.shareItemId, +); +const infoModalOpen = $derived(!offlineManager.isOfflineMode && !!ui.infoItem); + let preMiniplayerSize: { width: number; height: number } | null = null; let miniplayerSize: { width: number; height: number } | null = null; @@ -107,7 +122,7 @@ $effect(() => { }); $effect(() => { - if (!auth.loggedIn && !auth.autoLogin && !AuthModal) + if (authModalOpen && !AuthModal) void import("./lib/dialogs/AuthModal.svelte").then( (mod) => (AuthModal = mod.default), ); @@ -119,23 +134,19 @@ $effect(() => { void import("./lib/dialogs/ShortcutsModal.svelte").then( (mod) => (ShortcutsModal = mod.default), ); - if ((ui.playlistForm || ui.confirmDeletePlaylist) && !PlaylistModals) + if (playlistModalOpen && !PlaylistModals) void import("./lib/dialogs/PlaylistModals.svelte").then( (mod) => (PlaylistModals = mod.default), ); - if ( - !offlineManager.isOfflineMode && - (ui.stationForm || ui.confirmDeleteStation) && - !RadioModals - ) + if (radioModalOpen && !RadioModals) void import("./lib/dialogs/RadioModals.svelte").then( (mod) => (RadioModals = mod.default), ); - if (!offlineManager.isOfflineMode && ui.shareItemId && !ShareModal) + if (shareModalOpen && !ShareModal) void import("./lib/dialogs/ShareModal.svelte").then( (mod) => (ShareModal = mod.default), ); - if (!offlineManager.isOfflineMode && ui.infoItem && !InfoModal) + if (infoModalOpen && !InfoModal) void import("./lib/dialogs/InfoModal.svelte").then( (mod) => (InfoModal = mod.default), ); @@ -203,7 +214,7 @@ $effect(() => { -{#if !auth.loggedIn && AuthModal} +{#if authModalOpen && AuthModal} {/if} {#if ui.showSettings && SettingsModal} @@ -212,16 +223,16 @@ $effect(() => { {#if ui.showKeys && ShortcutsModal} {/if} -{#if (ui.playlistForm || ui.confirmDeletePlaylist) && PlaylistModals} +{#if playlistModalOpen && PlaylistModals} {/if} -{#if !offlineManager.isOfflineMode && (ui.stationForm || ui.confirmDeleteStation) && RadioModals} +{#if radioModalOpen && RadioModals} {/if} -{#if !offlineManager.isOfflineMode && ui.shareItemId && ShareModal} +{#if shareModalOpen && ShareModal} {/if} -{#if !offlineManager.isOfflineMode && ui.infoItem && InfoModal} +{#if infoModalOpen && InfoModal} {/if} {#if ui.showUnsyncedChanges && UnsyncedChangesModal} diff --git a/src/lib/CoverBackground.svelte b/src/lib/CoverBackground.svelte index f720d37..a9fbc08 100644 --- a/src/lib/CoverBackground.svelte +++ b/src/lib/CoverBackground.svelte @@ -42,7 +42,9 @@ let { {#key src} 0 && !src.startsWith("blob:") + ? "anonymous" + : undefined} {src} loading="lazy" fetchpriority="low" diff --git a/src/lib/Library.svelte b/src/lib/Library.svelte index e02bf9b..1fb5108 100644 --- a/src/lib/Library.svelte +++ b/src/lib/Library.svelte @@ -850,15 +850,19 @@ function onKey(event: KeyboardEvent) { onAdd={row.onAdd} /> {:else if row.kind === "offline-status"} -
- {t( - settings.downloadsAutoSyncMode === "none" - ? "offline.offline_notice" - : "offline.offline_notice_auto_sync", - )} +
+ {#if auth.authFailed} + {t("offline.auth_failed_notice")} + {:else} + {t( + settings.downloadsAutoSyncMode === "none" + ? "offline.offline_notice" + : "offline.offline_notice_auto_sync", + )} + {/if}
{:else if row.kind === "download-status"} -
+
{t("offline.download_progress", { current: offlineManager.downloadProgress.current, @@ -969,14 +973,14 @@ input { color: var(--text); opacity: 0.5; } -.status { +[role="status"] { padding-block-start: 0.5rem; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; } -.status > button { +[role="status"] > button { display: flex; align-items: center; } diff --git a/src/lib/auth.svelte.ts b/src/lib/auth.svelte.ts index 4aa72c1..8a76733 100644 --- a/src/lib/auth.svelte.ts +++ b/src/lib/auth.svelte.ts @@ -4,6 +4,7 @@ import { createToken, detectExtensions, extensions, + isAuthenticationError, isNetworkError, persistCredentials, setCredentials, @@ -12,9 +13,6 @@ import { deploymentConfig } from "./deploymentConfig.js"; import { loadLib } from "./library.svelte.js"; import { offlineManager } from "./offline/offlineManager.svelte.js"; import { syncQueue } from "./queue.svelte.js"; -import { persistSettings, settings } from "./settings.svelte.js"; - -const isAdminRole = (role: unknown) => role === true || role === "true"; const getStoredCredentials = (): Credentials | null => { if (typeof localStorage === "undefined") return null; @@ -51,7 +49,7 @@ const loadSessionData = async (username: string) => { api .getUser(username) .then((user) => { - admin = isAdminRole(user.adminRole); + admin = user.adminRole === true || user.adminRole === "true"; }) .catch((err) => { console.error("failed to fetch user info:", err); @@ -69,9 +67,21 @@ export const auth = $state({ server: "", user: "", admin: false, + lastError: null as unknown, + authFailed: false, autoLogin: !!getSessionCredentials(), }); +export const markAuthFailed = () => { + Object.assign(auth, { + loggedIn: false, + admin: false, + autoLogin: false, + authFailed: true, + }); + offlineManager.setOffline(); +}; + export const login = async ( server: string, username: string, @@ -88,6 +98,8 @@ export const login = async ( username = username.trim(); auth.busy = true; auth.err = null; + auth.lastError = null; + auth.authFailed = false; try { if (secret) { const tryAuth = async (mode: "token" | "password" | "apikey") => { @@ -136,6 +148,7 @@ export const login = async ( Object.assign(auth, { server, user: username, loggedIn: true, admin }); return true; } catch (err) { + auth.lastError = err; auth.err = err instanceof Error ? err.message : String(err); return false; } finally { @@ -146,13 +159,7 @@ export const login = async ( export const logout = async () => { setCredentials(null); - persistCredentials(); - Object.assign(settings, { - downloads: false, - downloadsAuto: false, - downloadsAutoSyncMode: "none", - }); - persistSettings(); + localStorage.clear(); try { await offlineManager.wipeOfflineData(); } catch (err) { @@ -172,21 +179,27 @@ export const reconnectSession = async () => { await offlineManager.autoSyncQueuedRequests(); auth.admin = await loadSessionData(auth.user); } catch (err) { - console.error("failed to reconnect session:", err); + if (isAuthenticationError(err)) { + markAuthFailed(); + } else { + console.error("failed to reconnect session:", err); + } } }; -const tryStoredLogin = (credentials: Credentials) => - login( - credentials.server, - credentials.username || "", - credentials.password || credentials.apiKey || "", - credentials.authMode || "token", - ); - -const handleNetworkError = (ok: boolean) => { - if (ok) return true; - if (isNetworkError(auth.err)) { +const tryStoredSession = async (credentials: Credentials) => { + if ( + await login( + credentials.server, + credentials.username || "", + credentials.password || credentials.apiKey || "", + credentials.authMode || "token", + ) + ) + return true; + if (isAuthenticationError(auth.lastError)) { + markAuthFailed(); + } else if (isNetworkError(auth.lastError)) { auth.err = null; auth.autoLogin = true; } @@ -200,6 +213,7 @@ export const restoreSession = async (credentials: Credentials) => { user: credentials.username || "", err: null, autoLogin: true, + authFailed: false, }); offlineManager.setOffline(); @@ -209,8 +223,8 @@ export const restoreSession = async (credentials: Credentials) => { console.error("failed to load offline library during bootstrap:", err); } - return handleNetworkError(await tryStoredLogin(credentials)); + return tryStoredSession(credentials); }; export const retrySession = async (credentials: Credentials) => - handleNetworkError(await tryStoredLogin(credentials)); + tryStoredSession(credentials); diff --git a/src/lib/client.svelte.ts b/src/lib/client.svelte.ts index d5c2df5..96203e5 100644 --- a/src/lib/client.svelte.ts +++ b/src/lib/client.svelte.ts @@ -284,6 +284,21 @@ export const isNetworkError = (err: unknown) => { ); }; +class ApiError extends Error { + constructor( + message: string, + readonly code?: number, + readonly httpStatus?: number, + ) { + super(message); + this.name = "ApiError"; + } +} + +export const isAuthenticationError = (err: unknown) => + err instanceof ApiError && + (err.httpStatus === 401 || [40, 41, 42, 43, 44].includes(err.code ?? -1)); + const request = async ( method: string, params: Record = {}, @@ -304,14 +319,25 @@ const request = async ( : {}, ); if (!response.ok) { - throw new Error(`server returned HTTP ${response.status}`); + throw new ApiError( + `server returned HTTP ${response.status}`, + undefined, + response.status, + ); } const body = await response.json(); const data = body?.["subsonic-response"]; if (!data || typeof data !== "object" || typeof data.status !== "string") { - throw new Error("invalid Subsonic response"); + throw new ApiError("invalid Subsonic response"); + } + if (data.status === "failed") { + throw new ApiError( + typeof data.error?.message === "string" + ? data.error.message + : "API error", + typeof data.error?.code === "number" ? data.error.code : undefined, + ); } - if (data.status === "failed") throw data.error?.message || "API error"; isOpenSubsonic = !!data.openSubsonic; return data; } catch (err) { diff --git a/src/lib/components/ProgressBar.svelte b/src/lib/components/ProgressBar.svelte index da00b6a..580c844 100644 --- a/src/lib/components/ProgressBar.svelte +++ b/src/lib/components/ProgressBar.svelte @@ -28,7 +28,9 @@ let { } = $props(); const percentage = $derived( - Math.max(0, Math.min(100, ((value - min) / (max - min || 1)) * 100)), + variant === "native" + ? undefined + : `${Math.max(0, Math.min(100, ((value - min) / (max - min || 1)) * 100))}%`, ); @@ -38,7 +40,7 @@ const percentage = $derived( class:triangle={variant === "triangle"} class:disabled class:loading={loading} - style:--percentage={`${percentage}%`} + style:--percentage={percentage} > {#if variant !== "native"}
@@ -70,7 +72,10 @@ const percentage = $derived( inline-size: 100%; } } -:global(html.dynamic-colors) input:not(.custom) { +.progress-bar.loading > input:not(.custom) { + accent-color: orange; +} +:global(html.dynamic-colors) .progress-bar:not(.loading) > input:not(.custom) { accent-color: var(--text); } .simple, diff --git a/src/lib/dialogs/AuthModal.svelte b/src/lib/dialogs/AuthModal.svelte index 96f2540..99e6efd 100644 --- a/src/lib/dialogs/AuthModal.svelte +++ b/src/lib/dialogs/AuthModal.svelte @@ -74,7 +74,7 @@ $effect(() => { diff --git a/src/lib/languages/locales/ar.ts b/src/lib/languages/locales/ar.ts index fbae4b7..0f4593f 100644 --- a/src/lib/languages/locales/ar.ts +++ b/src/lib/languages/locales/ar.ts @@ -154,6 +154,8 @@ const ar: BaseDict = { favorite_artist: 'الفنان المفضل: "{name}"', favorite_song: 'مفضل: "{title}"', no_changes: "لا توجد تغييرات معلقة.", + auth_failed_notice: + "فشل تسجيل الدخول. ربما تغيّرت بيانات تسجيل الدخول الخاصة بك. يمكنك تسجيل الخروج من الإعدادات.", offline_notice: "أنت غير متصل! ستحتاج إلى مزامنة التغييرات يدويًا بمجرد عودتك للاتصال.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/bn.ts b/src/lib/languages/locales/bn.ts index 98bc993..91eb149 100644 --- a/src/lib/languages/locales/bn.ts +++ b/src/lib/languages/locales/bn.ts @@ -154,6 +154,8 @@ const bn: BaseDict = { favorite_artist: 'প্রিয় শিল্পী: "{name}"', favorite_song: 'প্রিয়: "{title}"', no_changes: "কোনো মুলতুবি পরিবর্তন নেই।", + auth_failed_notice: + "লগইন ব্যর্থ হয়েছে। আপনার লগইন তথ্য পরিবর্তিত হয়ে থাকতে পারে। আপনি সেটিংস থেকে লগআউট করতে পারেন।", offline_notice: "আপনি অফলাইনে আছেন! অনলাইনে ফিরলে পরিবর্তনগুলি আপনাকে হাতে সিঙ্ক করতে হবে।", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/cs.ts b/src/lib/languages/locales/cs.ts index ceaf653..88827c1 100644 --- a/src/lib/languages/locales/cs.ts +++ b/src/lib/languages/locales/cs.ts @@ -154,6 +154,8 @@ const cs: BaseDict = { favorite_artist: 'Oblíbený umělec: "{name}"', favorite_song: 'Oblíbeno: "{title}"', no_changes: "Žádné nevyřízené změny.", + auth_failed_notice: + "Přihlášení selhalo. Vaše přihlašovací údaje se mohly změnit. Můžete se odhlásit v Nastavení.", offline_notice: "Jste offline! Jakmile budete online, budete muset změny synchronizovat ručně.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/da.ts b/src/lib/languages/locales/da.ts index 202327f..d993929 100644 --- a/src/lib/languages/locales/da.ts +++ b/src/lib/languages/locales/da.ts @@ -154,6 +154,8 @@ const da: BaseDict = { favorite_artist: 'Yndlingskunstner: "{name}"', favorite_song: 'Yndling: "{title}"', no_changes: "Ingen ventende ændringer.", + auth_failed_notice: + "Login mislykkedes. Dine loginoplysninger kan være ændret. Du kan logge ud i Indstillinger.", offline_notice: "Du er offline! Du skal synkronisere ændringer manuelt, når du er online igen.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/de.ts b/src/lib/languages/locales/de.ts index e883dbf..b21ce02 100644 --- a/src/lib/languages/locales/de.ts +++ b/src/lib/languages/locales/de.ts @@ -154,6 +154,8 @@ const de: BaseDict = { favorite_artist: 'Künstler favorisiert: "{name}"', favorite_song: 'Favorisiert: "{title}"', no_changes: "Keine ausstehenden Änderungen.", + auth_failed_notice: + "Anmeldung fehlgeschlagen. Ihre Anmeldedaten wurden möglicherweise geändert. Sie können sich in den Einstellungen abmelden.", offline_notice: "Sie sind offline! Sobald Sie wieder online sind, müssen Sie Änderungen manuell synchronisieren.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/el.ts b/src/lib/languages/locales/el.ts index db69579..3791ebc 100644 --- a/src/lib/languages/locales/el.ts +++ b/src/lib/languages/locales/el.ts @@ -155,6 +155,8 @@ const el: BaseDict = { favorite_artist: 'Αγαπημένος καλλιτέχνης: "{name}"', favorite_song: 'Αγαπημένο: "{title}"', no_changes: "Δεν υπάρχουν εκκρεμείς αλλαγές.", + auth_failed_notice: + "Η σύνδεση απέτυχε. Τα στοιχεία σύνδεσής σας μπορεί να έχουν αλλάξει. Μπορείτε να αποσυνδεθείτε από τις Ρυθμίσεις.", offline_notice: "Είστε εκτός σύνδεσης! Θα πρέπει να συγχρονίσετε τις αλλαγές χειροκίνητα όταν συνδεθείτε ξανά.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/en.ts b/src/lib/languages/locales/en.ts index 84169cb..21851d7 100644 --- a/src/lib/languages/locales/en.ts +++ b/src/lib/languages/locales/en.ts @@ -152,6 +152,8 @@ const en = { favorite_artist: 'Favorite artist: "{name}"', favorite_song: 'Favorite: "{title}"', no_changes: "No pending changes.", + auth_failed_notice: + "Login failed. Your credentials may have changed. You can log out in Settings.", offline_notice: "You're offline! You will need to manually sync changes once online.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/es.ts b/src/lib/languages/locales/es.ts index 4cb2055..f858556 100644 --- a/src/lib/languages/locales/es.ts +++ b/src/lib/languages/locales/es.ts @@ -154,6 +154,8 @@ const es: BaseDict = { favorite_artist: 'Artista favorito: "{name}"', favorite_song: 'Favorito: "{title}"', no_changes: "No hay cambios pendientes.", + auth_failed_notice: + "El inicio de sesión falló. Es posible que tus credenciales hayan cambiado. Puedes cerrar sesión en Ajustes.", offline_notice: "¡Estás sin conexión! Tendrás que sincronizar los cambios manualmente cuando vuelvas a estar en línea.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/fi.ts b/src/lib/languages/locales/fi.ts index 30804a0..5ac018e 100644 --- a/src/lib/languages/locales/fi.ts +++ b/src/lib/languages/locales/fi.ts @@ -155,6 +155,8 @@ const fi: BaseDict = { favorite_artist: 'Lempiartisti: "{name}"', favorite_song: 'Suosikki: "{title}"', no_changes: "Ei odottavia muutoksia.", + auth_failed_notice: + "Kirjautuminen epäonnistui. Kirjautumistietosi ovat ehkä muuttuneet. Voit kirjautua ulos asetuksista.", offline_notice: "Olet offline-tilassa! Muutokset on synkronoitava käsin, kun olet jälleen verkossa.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/fr.ts b/src/lib/languages/locales/fr.ts index 41bdbf2..6dd2b12 100644 --- a/src/lib/languages/locales/fr.ts +++ b/src/lib/languages/locales/fr.ts @@ -154,6 +154,8 @@ const fr: BaseDict = { favorite_artist: 'Artiste ajouté aux favoris : "{name}"', favorite_song: 'Ajouter aux favoris : "{title}"', no_changes: "Aucune modification en attente.", + auth_failed_notice: + "Échec de la connexion. Vos identifiants ont peut-être changé. Vous pouvez vous déconnecter dans les paramètres.", offline_notice: "Vous êtes hors ligne ! Vous devrez synchroniser les modifications manuellement une fois reconnecté.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/he.ts b/src/lib/languages/locales/he.ts index ddcad66..e5b4bfd 100644 --- a/src/lib/languages/locales/he.ts +++ b/src/lib/languages/locales/he.ts @@ -154,6 +154,8 @@ const he: BaseDict = { favorite_artist: 'אמן אהוב: "{name}"', favorite_song: 'אהוב: "{title}"', no_changes: "אין שינויים ממתינים.", + auth_failed_notice: + "ההתחברות נכשלה. ייתכן שפרטי ההתחברות שלך השתנו. ניתן להתנתק מההגדרות.", offline_notice: "אתה במצב לא מקוון! יהיה עליך לסנכרן שינויים ידנית כשתתחבר שוב.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/hi.ts b/src/lib/languages/locales/hi.ts index a4bda26..9ade2f8 100644 --- a/src/lib/languages/locales/hi.ts +++ b/src/lib/languages/locales/hi.ts @@ -153,6 +153,8 @@ const hi: BaseDict = { favorite_artist: 'पसंदीदा कलाकार: "{name}"', favorite_song: 'पसंदीदा: "{title}"', no_changes: "कोई लंबित बदलाव नहीं।", + auth_failed_notice: + "लॉगिन विफल हुआ। आपके क्रेडेंशियल बदल गए हो सकते हैं। आप सेटिंग्स में जाकर लॉग आउट कर सकते हैं।", offline_notice: "आप ऑफ़लाइन हैं! ऑनलाइन होने पर आपको बदलावों को मैन्युअल रूप से सिंक करना होगा।", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/id.ts b/src/lib/languages/locales/id.ts index d1251be..3859e40 100644 --- a/src/lib/languages/locales/id.ts +++ b/src/lib/languages/locales/id.ts @@ -154,6 +154,8 @@ const id: BaseDict = { favorite_artist: 'Artis favorit: "{name}"', favorite_song: 'Favorit: "{title}"', no_changes: "Tidak ada perubahan tertunda.", + auth_failed_notice: + "Login gagal. Kredensial Anda mungkin telah berubah. Anda dapat keluar melalui Pengaturan.", offline_notice: "Anda sedang offline! Anda perlu menyinkronkan perubahan secara manual setelah online lagi.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/it.ts b/src/lib/languages/locales/it.ts index 93e828d..df5f158 100644 --- a/src/lib/languages/locales/it.ts +++ b/src/lib/languages/locales/it.ts @@ -154,6 +154,8 @@ const it: BaseDict = { favorite_artist: 'Artista aggiunto ai preferiti: "{name}"', favorite_song: 'Aggiungi ai preferiti: "{title}"', no_changes: "Nessuna modifica in sospeso.", + auth_failed_notice: + "Accesso non riuscito. Le tue credenziali potrebbero essere cambiate. Puoi uscire dalle Impostazioni.", offline_notice: "Sei offline! Dovrai sincronizzare manualmente le modifiche quando tornerai online.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/ja.ts b/src/lib/languages/locales/ja.ts index fce961e..8a710c4 100644 --- a/src/lib/languages/locales/ja.ts +++ b/src/lib/languages/locales/ja.ts @@ -154,6 +154,8 @@ const ja: BaseDict = { favorite_artist: "好きなアーティスト: 「{name}」", favorite_song: "お気に入り: 「{title}」", no_changes: "保留中の変更はありません。", + auth_failed_notice: + "ログインに失敗しました。認証情報が変更された可能性があります。設定からログアウトできます。", offline_notice: "オフラインです。オンラインに戻ったら変更を手動で同期する必要があります。", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/ko.ts b/src/lib/languages/locales/ko.ts index 2adabed..88d8e1e 100644 --- a/src/lib/languages/locales/ko.ts +++ b/src/lib/languages/locales/ko.ts @@ -153,6 +153,8 @@ const ko: BaseDict = { favorite_artist: '좋아하는 아티스트: "{name}"', favorite_song: '즐겨찾기: "{title}"', no_changes: "대기 중인 변경 사항이 없습니다.", + auth_failed_notice: + "로그인에 실패했습니다. 로그인 정보가 변경되었을 수 있습니다. 설정에서 로그아웃할 수 있습니다.", offline_notice: "오프라인 상태입니다! 다시 온라인이 되면 변경 사항을 수동으로 동기화해야 합니다.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/nl.ts b/src/lib/languages/locales/nl.ts index 5837742..df75ae1 100644 --- a/src/lib/languages/locales/nl.ts +++ b/src/lib/languages/locales/nl.ts @@ -155,6 +155,8 @@ const nl: BaseDict = { favorite_artist: 'Artiest toegevoegd aan favorieten: "{name}"', favorite_song: 'Toegevoegd aan favorieten: "{title}"', no_changes: "Geen openstaande wijzigingen.", + auth_failed_notice: + "Inloggen mislukt. Je inloggegevens zijn mogelijk gewijzigd. Je kunt uitloggen via Instellingen.", offline_notice: "Je bent offline! Zodra je weer online bent, moet je wijzigingen handmatig synchroniseren.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/no.ts b/src/lib/languages/locales/no.ts index 1bd38f5..2291b65 100644 --- a/src/lib/languages/locales/no.ts +++ b/src/lib/languages/locales/no.ts @@ -154,6 +154,8 @@ const no: BaseDict = { favorite_artist: "Favorittartist: «{name}»", favorite_song: 'Favoritt: "{title}"', no_changes: "Ingen ventende endringer.", + auth_failed_notice: + "Innlogging mislyktes. Påloggingsopplysningene dine kan ha blitt endret. Du kan logge ut i Innstillinger.", offline_notice: "Du er frakoblet! Du må synkronisere endringene manuelt når du er på nett igjen.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/pl.ts b/src/lib/languages/locales/pl.ts index e4cd935..b598170 100644 --- a/src/lib/languages/locales/pl.ts +++ b/src/lib/languages/locales/pl.ts @@ -154,6 +154,8 @@ const pl: BaseDict = { favorite_artist: "Ulubiony artysta: “{name}”", favorite_song: "Ulubiona: “{title}”", no_changes: "Brak oczekujących zmian.", + auth_failed_notice: + "Logowanie nie powiodło się. Twoje dane logowania mogły się zmienić. Możesz wylogować się w Ustawieniach.", offline_notice: "Jesteś offline! Po powrocie do sieci trzeba będzie ręcznie zsynchronizować zmiany.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/pt.ts b/src/lib/languages/locales/pt.ts index ab7ac41..14a3452 100644 --- a/src/lib/languages/locales/pt.ts +++ b/src/lib/languages/locales/pt.ts @@ -154,6 +154,8 @@ const pt: BaseDict = { favorite_artist: 'Artista adicionado aos favoritos: "{name}"', favorite_song: 'Adicionar aos favoritos: "{title}"', no_changes: "Nenhuma alteração pendente.", + auth_failed_notice: + "Falha no login. Suas credenciais podem ter mudado. Você pode sair pelas Configurações.", offline_notice: "Está offline! Terá de sincronizar manualmente as alterações quando voltar a estar online.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/ro.ts b/src/lib/languages/locales/ro.ts index fe9267a..3194ba0 100644 --- a/src/lib/languages/locales/ro.ts +++ b/src/lib/languages/locales/ro.ts @@ -154,6 +154,8 @@ const ro: BaseDict = { favorite_artist: 'Artist preferat: "{name}"', favorite_song: 'Preferat: "{title}"', no_changes: "Nu există modificări în așteptare.", + auth_failed_notice: + "Autentificarea a eșuat. Datele tale de autentificare s-ar fi putut schimba. Te poți deconecta din Setări.", offline_notice: "Ești offline! Va trebui să sincronizezi manual modificările după ce revii online.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/ru.ts b/src/lib/languages/locales/ru.ts index 80579a2..05a497e 100644 --- a/src/lib/languages/locales/ru.ts +++ b/src/lib/languages/locales/ru.ts @@ -154,6 +154,8 @@ const ru: BaseDict = { favorite_artist: 'Любимый исполнитель: "{name}"', favorite_song: 'Понравилось: "{title}"', no_changes: "Нет ожидающих изменений.", + auth_failed_notice: + "Не удалось войти. Возможно, ваши данные для входа изменились. Вы можете выйти в настройках.", offline_notice: "Вы не в сети! Когда снова появится подключение, изменения нужно будет синхронизировать вручную.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/sv.ts b/src/lib/languages/locales/sv.ts index 1c03278..d90ad27 100644 --- a/src/lib/languages/locales/sv.ts +++ b/src/lib/languages/locales/sv.ts @@ -154,6 +154,8 @@ const sv: BaseDict = { favorite_artist: 'Favoritartist: "{name}"', favorite_song: 'Favorit: "{title}"', no_changes: "Inga väntande ändringar.", + auth_failed_notice: + "Inloggningen misslyckades. Dina inloggningsuppgifter kan ha ändrats. Du kan logga ut i Inställningar.", offline_notice: "Du är offline! Du måste synkronisera ändringar manuellt när du är online igen.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/th.ts b/src/lib/languages/locales/th.ts index 585148f..f82de4a 100644 --- a/src/lib/languages/locales/th.ts +++ b/src/lib/languages/locales/th.ts @@ -153,6 +153,8 @@ const th: BaseDict = { favorite_artist: 'ศิลปินคนโปรด: "{name}"', favorite_song: 'โปรด: "{title}"', no_changes: "ไม่มีการเปลี่ยนแปลงที่รอดำเนินการ", + auth_failed_notice: + "เข้าสู่ระบบไม่สำเร็จ ข้อมูลเข้าสู่ระบบของคุณอาจมีการเปลี่ยนแปลง คุณสามารถออกจากระบบในการตั้งค่าได้", offline_notice: "คุณออฟไลน์อยู่! เมื่อกลับมาออนไลน์แล้ว คุณจะต้องซิงก์การเปลี่ยนแปลงด้วยตนเอง", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/tl.ts b/src/lib/languages/locales/tl.ts index 4ae0e32..092b805 100644 --- a/src/lib/languages/locales/tl.ts +++ b/src/lib/languages/locales/tl.ts @@ -154,6 +154,8 @@ const tl: BaseDict = { favorite_artist: 'Paboritong artist: "{name}"', favorite_song: 'Paborito: "{title}"', no_changes: "Walang nakabinbing pagbabago.", + auth_failed_notice: + "Hindi matagumpay ang pag-login. Maaaring nagbago ang iyong mga kredensyal. Maaari kang mag-logout sa Mga setting.", offline_notice: "Offline ka! Kakailanganin mong i-sync nang manu-mano ang mga pagbabago kapag online ka na muli.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/tr.ts b/src/lib/languages/locales/tr.ts index 10e59e5..aed297b 100644 --- a/src/lib/languages/locales/tr.ts +++ b/src/lib/languages/locales/tr.ts @@ -154,6 +154,8 @@ const tr: BaseDict = { favorite_artist: 'Favori sanatçı: "{name}"', favorite_song: 'Favorilere ekle: "{title}"', no_changes: "Bekleyen değişiklik yok.", + auth_failed_notice: + "Giriş başarısız. Kimlik bilgileriniz değişmiş olabilir. Ayarlar'dan çıkış yapabilirsiniz.", offline_notice: "Çevrimdışısınız! Yeniden çevrimiçi olduğunuzda değişiklikleri elle senkronize etmeniz gerekecek.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/uk.ts b/src/lib/languages/locales/uk.ts index 8f839a3..67befb6 100644 --- a/src/lib/languages/locales/uk.ts +++ b/src/lib/languages/locales/uk.ts @@ -154,6 +154,8 @@ const uk: BaseDict = { favorite_artist: 'Улюблений виконавець: "{name}"', favorite_song: 'Улюблено: "{title}"', no_changes: "Немає незавершених змін.", + auth_failed_notice: + "Не вдалося ввійти. Можливо, ваші облікові дані змінилися. Ви можете вийти в налаштуваннях.", offline_notice: "Ви офлайн! Коли знову з’явиться з’єднання, зміни доведеться синхронізувати вручну.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/vi.ts b/src/lib/languages/locales/vi.ts index 791a1aa..a3a1c62 100644 --- a/src/lib/languages/locales/vi.ts +++ b/src/lib/languages/locales/vi.ts @@ -154,6 +154,8 @@ const vi: BaseDict = { favorite_artist: 'Nghệ sĩ yêu thích: "{name}"', favorite_song: 'Yêu thích: "{title}"', no_changes: "Không có thay đổi đang chờ xử lý.", + auth_failed_notice: + "Đăng nhập không thành công. Thông tin đăng nhập của bạn có thể đã thay đổi. Bạn có thể đăng xuất trong Cài đặt.", offline_notice: "Bạn đang ngoại tuyến! Bạn sẽ cần đồng bộ thay đổi thủ công khi trực tuyến trở lại.", offline_notice_auto_sync: diff --git a/src/lib/languages/locales/zhcn.ts b/src/lib/languages/locales/zhcn.ts index b45ebc1..8faaac7 100644 --- a/src/lib/languages/locales/zhcn.ts +++ b/src/lib/languages/locales/zhcn.ts @@ -151,6 +151,8 @@ const zhcn: BaseDict = { favorite_artist: "最喜欢的艺术家:「{name}」", favorite_song: "最喜欢:「{title}」", no_changes: "没有待处理的更改。", + auth_failed_notice: + "登录失败。您的凭据可能已更改。您可以在设置中退出登录。", offline_notice: "你当前处于离线状态!恢复联网后需要手动同步更改。", offline_notice_auto_sync: "你当前处于离线状态!所选更改将在恢复联网后自动同步。", diff --git a/src/lib/languages/locales/zhtw.ts b/src/lib/languages/locales/zhtw.ts index 24d993f..75583aa 100644 --- a/src/lib/languages/locales/zhtw.ts +++ b/src/lib/languages/locales/zhtw.ts @@ -151,6 +151,8 @@ const zhtw: BaseDict = { favorite_artist: "最喜歡的藝術家:「{name}」", favorite_song: "最喜歡:「{title}」", no_changes: "沒有待處理的變更。", + auth_failed_notice: + "登入失敗。您的登入資訊可能已變更。您可以在設定中登出。", offline_notice: "你目前處於離線狀態!恢復連線後需要手動同步變更。", offline_notice_auto_sync: "你目前處於離線狀態!所選變更會在恢復連線後自動同步。", diff --git a/src/lib/offline/offlineManager.svelte.ts b/src/lib/offline/offlineManager.svelte.ts index 5f7d380..866f35c 100644 --- a/src/lib/offline/offlineManager.svelte.ts +++ b/src/lib/offline/offlineManager.svelte.ts @@ -1,9 +1,10 @@ import { SvelteMap, SvelteSet } from "svelte/reactivity"; import { songArtId } from "../artwork.js"; -import { auth } from "../auth.svelte.js"; +import { auth, markAuthFailed } from "../auth.svelte.js"; import { api, getOnlineCoverArt, + isAuthenticationError, isNetworkError, type LyricsResponse, type Playlist, @@ -119,7 +120,6 @@ class OfflineManager { downloading: false, total: 0, current: 0, - currentSongId: "", }); queuedRequests = $state([]); @@ -140,10 +140,7 @@ class OfflineManager { if (this.online === online) return; this.online = online; if (online) { - if (this.reconnectTimer !== null) { - clearInterval(this.reconnectTimer); - this.reconnectTimer = null; - } + this.stopReconnectTimer(); void this.loadSyncQueue(); } else if (this.reconnectTimer === null) { this.reconnectTimer = setInterval(() => void this.tryReconnect(), 30_000); @@ -152,18 +149,32 @@ class OfflineManager { setOffline() { this.setOnline(false); + if (auth.authFailed) this.stopReconnectTimer(); } setConnected() { this.setOnline(true); } + private stopReconnectTimer() { + if (this.reconnectTimer === null) return; + clearInterval(this.reconnectTimer); + this.reconnectTimer = null; + } + private async tryReconnect() { + if (auth.busy || auth.authFailed) return; try { await api.forcePing(); this.setConnected(); } catch (err) { - if (!isNetworkError(err)) { + if (isAuthenticationError(err)) { + if (!auth.loggedIn && !auth.autoLogin) { + this.setConnected(); + return; + } + markAuthFailed(); + } else if (!isNetworkError(err)) { this.setConnected(); } } @@ -411,7 +422,6 @@ class OfflineManager { downloading: false, current: 0, total: 0, - currentSongId: "", }; this.cancelRequested = false; this.downloadQueueActive = false; @@ -473,25 +483,6 @@ class OfflineManager { this.cancelRequested = true; } - async autoDownloadSong(song: Song) { - if ( - !settings.downloads || - this.isOfflineMode || - this.downloadedSongIds.has(song.id) || - this.activeDownloadIds.has(song.id) - ) - return; - this.activeDownloadIds.add(song.id); - try { - await this.downloadSingleTrack(song); - } catch (err) { - console.error("auto download failed:", song.title, err); - } finally { - this.activeDownloadIds.delete(song.id); - await this.refreshDownloadsList(); - } - } - private async processDownloadQueue() { if (this.downloadQueueActive) return; @@ -513,7 +504,6 @@ class OfflineManager { this.queuedDownloadIds.delete(song.id); this.activeDownloadIds.add(song.id); - this.downloadProgress.currentSongId = song.id; try { await this.downloadSingleTrack(song); @@ -556,7 +546,6 @@ class OfflineManager { for (const song of list) this.queuedDownloadIds.add(song.id); this.downloadBatchQueue.push({ songs: list, onBeforeDownload }); - this.downloadProgress.downloading = true; this.downloadProgress.total += list.length; await this.processDownloadQueue(); @@ -963,7 +952,10 @@ class OfflineManager { async wipeOfflineData(): Promise { this.cancelDownloads(); - await store.clearAllData(); + await Promise.all([ + store.clearAllData(), + globalThis.caches?.delete("getCoverArt"), + ]); await this.loadSyncQueue(); await storage.clear(); for (const url of offlineImages.values()) { diff --git a/src/lib/player.svelte.ts b/src/lib/player.svelte.ts index 18634b4..49b5d14 100644 --- a/src/lib/player.svelte.ts +++ b/src/lib/player.svelte.ts @@ -830,12 +830,12 @@ $effect.root(() => { ) { autoDownloadedTrackId = player.track.id; - offlineManager.autoDownloadSong(player.track).catch(() => {}); + void offlineManager.downloadSong(player.track); for (let i = 1; i <= settings.downloadsAutoSongs; i++) { const nextSong = queue.tracks[queue.pos + i]; if (nextSong) { - offlineManager.autoDownloadSong(nextSong).catch(() => {}); + void offlineManager.downloadSong(nextSong); } } } diff --git a/src/lib/settings.svelte.ts b/src/lib/settings.svelte.ts index 3448cb1..e38da6c 100644 --- a/src/lib/settings.svelte.ts +++ b/src/lib/settings.svelte.ts @@ -157,7 +157,7 @@ export const settings = $state( ) as SettingsState, ); -export const persistSettings = () => +const persistSettings = () => localStorage.setItem( "tinysub_settings", JSON.stringify($state.snapshot(settings)), diff --git a/vite.config.ts b/vite.config.ts index 39362db..bc3cb07 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,6 +13,42 @@ export default defineConfig(({ mode }) => { injectRegister: false, workbox: { globPatterns: ["**/*.{js,css,html,ico,png,svg}"], + runtimeCaching: [ + { + urlPattern: ({ request, url }) => + request.cache !== "no-store" && + url.pathname.endsWith("/rest/getCoverArt"), + handler: "CacheFirst", + options: { + cacheName: "getCoverArt", + cacheableResponse: { + statuses: [0, 200], + }, + plugins: [ + { + cacheKeyWillBeUsed: async ({ request }) => { + const url = new URL(request.url); + const key = new URL(url.origin + url.pathname); + + for (const name of ["id", "size"]) { + const value = url.searchParams.get(name); + if (value !== null) key.searchParams.set(name, value); + } + key.searchParams.set( + "__tinysub_request_mode", + request.mode, + ); + return key.href; + }, + }, + ], + expiration: { + maxEntries: 500, + maxAgeSeconds: 7 * 24 * 60 * 60, + }, + }, + }, + ], }, manifest: { name: "tinysub",