From 63ccf03dacef127ea41d69154dd7a313bf3639fe Mon Sep 17 00:00:00 2001 From: Arno <46051866+arnolicious@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:46:07 +0200 Subject: [PATCH] refactor: Prototype of tanstack-query (#34) * move getApiUrl to api folder * adjust imports * tanstack-query example with homeData * small adjustments * fix key collision * new MusicSource persistent mechanism example * add error handling & set sveltekit to SPA mode * remove unnecessary ssr test --- CONTRIBUTING.md | 2 +- docker-compose.dev.yml | 29 + ...nv.dev.example => env.development.example} | 0 frontend/eslint.config.js | 20 + frontend/package.json | 10 +- frontend/pnpm-lock.yaml | 93 +++ .../lib/{utils/api.ts => api/api-utils.ts} | 0 frontend/src/lib/api/client.ts | 2 +- frontend/src/lib/components/ArtistHero.svelte | 2 +- frontend/src/lib/components/BaseImage.svelte | 2 +- .../lib/components/DiscoverArtistHero.svelte | 2 +- frontend/src/lib/components/GenreGrid.svelte | 2 +- .../lib/components/SearchSuggestions.svelte | 2 +- .../components/SimpleSourceSwitcher.svelte | 46 ++ .../src/lib/components/TimeRangeView.svelte | 4 +- .../components/settings/SettingsCache.svelte | 2 +- .../settings/SettingsPreferences.svelte | 2 +- frontend/src/lib/constants.ts | 3 +- frontend/src/lib/queries/HomeQuery.svelte.ts | 20 + .../lib/queries/IndexedDbPersister.svelte.ts | 52 ++ frontend/src/lib/queries/QueryClient.ts | 72 ++ frontend/src/lib/queries/QueryProvider.svelte | 20 + frontend/src/lib/stores/musicSource.ts | 4 +- frontend/src/lib/stores/player.spec.ts | 1 - frontend/src/lib/stores/syncStatus.svelte.ts | 2 +- frontend/src/lib/utils/errorHandling.ts | 2 +- .../src/lib/utils/navigationAbort.test.ts | 14 - frontend/src/lib/utils/navigationAbort.ts | 4 +- frontend/src/lib/utils/typeHelpers.ts | 3 + frontend/src/routes/+layout.svelte | 657 +++++++++--------- frontend/src/routes/+layout.ts | 24 + frontend/src/routes/+page.svelte | 183 +---- .../src/routes/album/[id]/AlbumHeader.svelte | 2 +- frontend/src/routes/genre/+page.svelte | 2 +- .../src/routes/playlists/[id]/+page.svelte | 2 +- uv.lock | 3 + 36 files changed, 769 insertions(+), 521 deletions(-) create mode 100644 docker-compose.dev.yml rename frontend/{env.dev.example => env.development.example} (100%) rename frontend/src/lib/{utils/api.ts => api/api-utils.ts} (100%) create mode 100644 frontend/src/lib/components/SimpleSourceSwitcher.svelte create mode 100644 frontend/src/lib/queries/HomeQuery.svelte.ts create mode 100644 frontend/src/lib/queries/IndexedDbPersister.svelte.ts create mode 100644 frontend/src/lib/queries/QueryClient.ts create mode 100644 frontend/src/lib/queries/QueryProvider.svelte create mode 100644 frontend/src/lib/utils/typeHelpers.ts create mode 100644 frontend/src/routes/+layout.ts create mode 100644 uv.lock diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4561b89..e847e65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ Frontend: ```bash cd frontend -cp env.dev.example .env +cp env.development.example .env.development pnpm install pnpm run dev ``` diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..f51618b --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,29 @@ +services: + musicseerr: + build: + context: . + dockerfile: Dockerfile + container_name: musicseerr-dev + environment: + - PUID=1000 # User ID — run `id` on your host to find yours + - PGID=1000 # Group ID — run `id` on your host to find yours + # Unraid: use 99/100 (nobody:users) unless you changed Unraid's defaults. + # If using Docker --user, PUID/PGID are ignored (user mapping is external). + - PORT=8688 # Internal port (must match the right side of "ports" below) + - TZ=Etc/UTC # Timezone — e.g. America/New_York, Europe/London + ports: + - "8689:8688" # : — change the left side to remap + volumes: + - config:/app/config + - cache:/app/cache + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8688/health"] + interval: 30s + timeout: 10s + start_period: 15s + retries: 3 + +volumes: + config: + cache: diff --git a/frontend/env.dev.example b/frontend/env.development.example similarity index 100% rename from frontend/env.dev.example rename to frontend/env.development.example diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index e514e3b..3b131e7 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -45,5 +45,25 @@ export default defineConfig( rules: { 'svelte/no-navigation-without-resolve': 'off' } + }, + { + rules: { + 'no-restricted-syntax': [ + 'error', + // Ensure not to call queryClient.setQueriesData or queryClient.setQueryData directly, as this will bypass the persister and lead to data loss on page refresh. + { + selector: + "CallExpression[callee.object.name='queryClient'][callee.property.name='setQueriesData']", + message: + "Direct use of 'queryClient.setQueriesData' is forbidden. Please use the 'setQueriesDataWithPersister' function instead." + }, + { + selector: + "CallExpression[callee.object.name='queryClient'][callee.property.name='setQueryData']", + message: + "Direct use of 'queryClient.setQueryData' is forbidden. Please use the 'setQueryDataWithPersister' function instead." + } + ] + } } ); diff --git a/frontend/package.json b/frontend/package.json index 5c0c4b3..fed6f1a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,11 +4,12 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev": "vite dev", + "dev": "vite dev --open", "build": "vite build", "preview": "vite preview", "tailwind": "tailwindcss -i ./src/app.css -o ./static/tailwind.css --watch", "prepare": "svelte-kit sync || echo ''", + "check:ci": "pnpm check && pnpm format:check && pnpm lint", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", @@ -47,6 +48,11 @@ "vitest-browser-svelte": "^1.1.0" }, "dependencies": { - "lucide-svelte": "^0.575.0" + "@tanstack/svelte-query": "^6.1.13", + "@tanstack/svelte-query-devtools": "^6.1.13", + "@tanstack/svelte-query-persist-client": "^6.1.13", + "idb-keyval": "^6.2.2", + "lucide-svelte": "^0.575.0", + "runed": "^0.37.1" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 9fcb4f1..07d0807 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,9 +8,24 @@ importers: .: dependencies: + '@tanstack/svelte-query': + specifier: ^6.1.13 + version: 6.1.13(svelte@5.55.1) + '@tanstack/svelte-query-devtools': + specifier: ^6.1.13 + version: 6.1.13(@tanstack/svelte-query@6.1.13(svelte@5.55.1))(svelte@5.55.1) + '@tanstack/svelte-query-persist-client': + specifier: ^6.1.13 + version: 6.1.13(@tanstack/svelte-query@6.1.13(svelte@5.55.1))(svelte@5.55.1) + idb-keyval: + specifier: ^6.2.2 + version: 6.2.2 lucide-svelte: specifier: ^0.575.0 version: 0.575.0(svelte@5.55.1) + runed: + specifier: ^0.37.1 + version: 0.37.1(@sveltejs/kit@2.56.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.1)(vite@7.3.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1) devDependencies: '@eslint/compat': specifier: ^1.4.0 @@ -721,6 +736,32 @@ packages: '@tailwindcss/postcss@4.2.2': resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==} + '@tanstack/query-core@5.96.2': + resolution: {integrity: sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==} + + '@tanstack/query-devtools@5.96.2': + resolution: {integrity: sha512-vBTB1Qhbm3nHSbEUtQwks/EdcAtFfEapr1WyBW4w2ExYKuXVi3jIxUIHf5MlSltiHuL7zNyUuanqT/7sI2sb6g==} + + '@tanstack/query-persist-client-core@5.96.2': + resolution: {integrity: sha512-BYsP8folbvxzZsNnWJxSenEAdepGNfv809150U78D84yt/THi33EwfUCcdKWFbma5XKwlaFQGWMJKeWnVJ6GVA==} + + '@tanstack/svelte-query-devtools@6.1.13': + resolution: {integrity: sha512-55gMVoPRh7BcwQTogSt8pPrSsQ6lWnfJct3Yos1t2tAZ1YL9KbLEa6X4/iVfdPmxKO+czdDN6GSkMIB05vVKKg==} + peerDependencies: + '@tanstack/svelte-query': ^6.1.13 + svelte: ^5.25.0 + + '@tanstack/svelte-query-persist-client@6.1.13': + resolution: {integrity: sha512-YJIx61T3857k4o206S1y8dNruzEGL1qwN63KoE+0L8juw+4rbsN2Jzq3dCADwio5JALPW4GJa12f26C4ufujAA==} + peerDependencies: + '@tanstack/svelte-query': ^6.1.13 + svelte: ^5.25.0 + + '@tanstack/svelte-query@6.1.13': + resolution: {integrity: sha512-A/BB9BuRoPSEJZqVUU5sqcAgi13XdfldlJtd8XmbwAXr/fxEMfpSEMYmIxLmUb/s9EVYRJi3bew9OjZddkx0cg==} + peerDependencies: + svelte: ^5.25.0 + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -1156,6 +1197,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + idb-keyval@6.2.2: + resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1477,6 +1521,18 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + runed@0.37.1: + resolution: {integrity: sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + zod: ^4.1.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + zod: + optional: true + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -2177,6 +2233,32 @@ snapshots: postcss: 8.5.8 tailwindcss: 4.2.2 + '@tanstack/query-core@5.96.2': {} + + '@tanstack/query-devtools@5.96.2': {} + + '@tanstack/query-persist-client-core@5.96.2': + dependencies: + '@tanstack/query-core': 5.96.2 + + '@tanstack/svelte-query-devtools@6.1.13(@tanstack/svelte-query@6.1.13(svelte@5.55.1))(svelte@5.55.1)': + dependencies: + '@tanstack/query-devtools': 5.96.2 + '@tanstack/svelte-query': 6.1.13(svelte@5.55.1) + esm-env: 1.2.2 + svelte: 5.55.1 + + '@tanstack/svelte-query-persist-client@6.1.13(@tanstack/svelte-query@6.1.13(svelte@5.55.1))(svelte@5.55.1)': + dependencies: + '@tanstack/query-persist-client-core': 5.96.2 + '@tanstack/svelte-query': 6.1.13(svelte@5.55.1) + svelte: 5.55.1 + + '@tanstack/svelte-query@6.1.13(svelte@5.55.1)': + dependencies: + '@tanstack/query-core': 5.96.2 + svelte: 5.55.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 @@ -2661,6 +2743,8 @@ snapshots: has-flag@4.0.0: {} + idb-keyval@6.2.2: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2927,6 +3011,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 + runed@0.37.1(@sveltejs/kit@2.56.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.1)(vite@7.3.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.55.1 + optionalDependencies: + '@sveltejs/kit': 2.56.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.1)(vite@7.3.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)) + sade@1.8.1: dependencies: mri: 1.2.0 diff --git a/frontend/src/lib/utils/api.ts b/frontend/src/lib/api/api-utils.ts similarity index 100% rename from frontend/src/lib/utils/api.ts rename to frontend/src/lib/api/api-utils.ts diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 55e1c95..10e75a0 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -1,5 +1,5 @@ import { pageFetch } from '$lib/utils/navigationAbort'; -import { getApiUrl } from '$lib/utils/api'; +import { getApiUrl } from '$lib/api/api-utils'; export class ApiError extends Error { readonly status: number; diff --git a/frontend/src/lib/components/ArtistHero.svelte b/frontend/src/lib/components/ArtistHero.svelte index 45bc9b3..cc183ca 100644 --- a/frontend/src/lib/components/ArtistHero.svelte +++ b/frontend/src/lib/components/ArtistHero.svelte @@ -11,7 +11,7 @@ import ArtistMonitoringToggle from './ArtistMonitoringToggle.svelte'; import BackButton from './BackButton.svelte'; import HeroBackdrop from './HeroBackdrop.svelte'; - import { getApiUrl } from '$lib/utils/api'; + import { getApiUrl } from '$lib/api/api-utils'; interface Props { artist: ArtistInfo; diff --git a/frontend/src/lib/components/BaseImage.svelte b/frontend/src/lib/components/BaseImage.svelte index e0b38dd..0345ea1 100644 --- a/frontend/src/lib/components/BaseImage.svelte +++ b/frontend/src/lib/components/BaseImage.svelte @@ -7,7 +7,7 @@ import { isValidMbid } from '$lib/utils/formatting'; import { imageSettingsStore } from '$lib/stores/imageSettings'; import { appendAudioDBSizeSuffix } from '$lib/utils/imageSuffix'; - import { getApiUrl } from '$lib/utils/api'; + import { getApiUrl } from '$lib/api/api-utils'; interface Props { mbid: string; diff --git a/frontend/src/lib/components/DiscoverArtistHero.svelte b/frontend/src/lib/components/DiscoverArtistHero.svelte index 418ec69..391ac4c 100644 --- a/frontend/src/lib/components/DiscoverArtistHero.svelte +++ b/frontend/src/lib/components/DiscoverArtistHero.svelte @@ -1,5 +1,5 @@ + +{#if showSwitcher} +
+ + +
+{/if} diff --git a/frontend/src/lib/components/TimeRangeView.svelte b/frontend/src/lib/components/TimeRangeView.svelte index 9959ba8..bdef395 100644 --- a/frontend/src/lib/components/TimeRangeView.svelte +++ b/frontend/src/lib/components/TimeRangeView.svelte @@ -330,7 +330,7 @@ {/if}
- {#each items.slice(0, 8) as item, idx (item.mbid)} + {#each items.slice(0, 8) as item, idx (idx)} {@const rank = idx + 2} {@const itemHref = getItemHref(item)} {:else if expandedData}
- {#each expandedData.items as item, idx (item.mbid)} + {#each expandedData.items as item, idx (idx)} {@const rank = idx + 1} {@const itemHref = getItemHref(item)} - import { getApiUrl } from '$lib/utils/api'; + import { getApiUrl } from '$lib/api/api-utils'; interface CacheStats { memory_entries: number; memory_size_bytes: number; diff --git a/frontend/src/lib/components/settings/SettingsPreferences.svelte b/frontend/src/lib/components/settings/SettingsPreferences.svelte index 01d01de..aa9939b 100644 --- a/frontend/src/lib/components/settings/SettingsPreferences.svelte +++ b/frontend/src/lib/components/settings/SettingsPreferences.svelte @@ -1,5 +1,5 @@ + + + {@render children()} + {#if dev} + + {/if} + diff --git a/frontend/src/lib/stores/musicSource.ts b/frontend/src/lib/stores/musicSource.ts index 5f71a9a..f1eeafd 100644 --- a/frontend/src/lib/stores/musicSource.ts +++ b/frontend/src/lib/stores/musicSource.ts @@ -7,14 +7,14 @@ export type MusicSource = 'listenbrainz' | 'lastfm'; export type MusicSourcePage = keyof typeof PAGE_SOURCE_KEYS; const CACHED_SOURCE_KEY = 'musicseerr_primary_source'; -const DEFAULT_SOURCE: MusicSource = 'listenbrainz'; +export const DEFAULT_SOURCE: MusicSource = 'listenbrainz'; interface MusicSourceState { source: MusicSource; loaded: boolean; } -function isMusicSource(value: unknown): value is MusicSource { +export function isMusicSource(value: unknown): value is MusicSource { return value === 'listenbrainz' || value === 'lastfm'; } diff --git a/frontend/src/lib/stores/player.spec.ts b/frontend/src/lib/stores/player.spec.ts index f8288e5..342a33b 100644 --- a/frontend/src/lib/stores/player.spec.ts +++ b/frontend/src/lib/stores/player.spec.ts @@ -823,7 +823,6 @@ describe('beforeunload beacon', () => { let addEventListenerSpy: ReturnType; let removeEventListenerSpy: ReturnType; let sendBeaconMock: ReturnType; - // eslint-disable-next-line @typescript-eslint/no-unused-vars let jellyfinApi: { startSession: ReturnType; reportProgress: ReturnType; diff --git a/frontend/src/lib/stores/syncStatus.svelte.ts b/frontend/src/lib/stores/syncStatus.svelte.ts index 6f3ade3..2644ed0 100644 --- a/frontend/src/lib/stores/syncStatus.svelte.ts +++ b/frontend/src/lib/stores/syncStatus.svelte.ts @@ -1,7 +1,7 @@ import { browser } from '$app/environment'; import { api } from '$lib/api/client'; import { libraryStore } from '$lib/stores/library'; -import { getApiUrl } from '$lib/utils/api'; +import { getApiUrl } from '$lib/api/api-utils'; export type SyncStatus = { is_syncing: boolean; diff --git a/frontend/src/lib/utils/errorHandling.ts b/frontend/src/lib/utils/errorHandling.ts index 2ab43b9..e4f6796 100644 --- a/frontend/src/lib/utils/errorHandling.ts +++ b/frontend/src/lib/utils/errorHandling.ts @@ -1,5 +1,5 @@ import { isValidMbid } from '$lib/utils/formatting'; -import { getApiUrl } from '$lib/utils/api'; +import { getApiUrl } from '$lib/api/api-utils'; export function isAbortError(error: unknown): boolean { return ( diff --git a/frontend/src/lib/utils/navigationAbort.test.ts b/frontend/src/lib/utils/navigationAbort.test.ts index 90c4b0e..71bbe34 100644 --- a/frontend/src/lib/utils/navigationAbort.test.ts +++ b/frontend/src/lib/utils/navigationAbort.test.ts @@ -164,17 +164,3 @@ describe('raw fetch is not affected by navigation abort', () => { expect(callArgs[1]?.signal).toBeUndefined(); }); }); - -describe('SSR safety', () => { - it('falls back to raw fetch when window is undefined', async () => { - vi.stubGlobal('window', undefined); - const mockFetch = vi.fn().mockResolvedValue(new Response('ok')); - vi.stubGlobal('fetch', mockFetch); - - await pageFetch('/api/v1/test'); - - expect(mockFetch).toHaveBeenCalledOnce(); - const callArgs = mockFetch.mock.calls[0]; - expect(callArgs[1]?.signal).toBeUndefined(); - }); -}); diff --git a/frontend/src/lib/utils/navigationAbort.ts b/frontend/src/lib/utils/navigationAbort.ts index 2aab380..37a80ca 100644 --- a/frontend/src/lib/utils/navigationAbort.ts +++ b/frontend/src/lib/utils/navigationAbort.ts @@ -12,7 +12,9 @@ export function abortAllPageRequests(): void { } export async function pageFetch(input: RequestInfo | URL, init?: RequestInit): Promise { - if (typeof window === 'undefined') return fetch(input, init); + if (typeof window === 'undefined') { + throw new Error('Can never happen, we are running in SPA mode'); + } const navSignal = getNavigationSignal(); const existingSignal = init?.signal; const signal = existingSignal ? AbortSignal.any([navSignal, existingSignal]) : navSignal; diff --git a/frontend/src/lib/utils/typeHelpers.ts b/frontend/src/lib/utils/typeHelpers.ts new file mode 100644 index 0000000..61b7b77 --- /dev/null +++ b/frontend/src/lib/utils/typeHelpers.ts @@ -0,0 +1,3 @@ +export type Getter = () => T; + +export type MaybeGetter = T | Getter; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 32b1264..e598f5d 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -51,6 +51,7 @@ ListMusic } from 'lucide-svelte'; import type { Snippet } from 'svelte'; + import QueryProvider from '$lib/queries/QueryProvider.svelte'; let { children }: { children: Snippet } = $props(); @@ -234,344 +235,346 @@ const lidarrConfigured = $derived(integrations.current.lidarr || !integrations.current.loaded); -
- {#if showNavigationProgress} -
- -
- {/if} - - - -
- - -
-