fix offline mode and layout bugs
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m32s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
Build & Release / Run Tests (push) Successful in 5m21s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m25s
Build & Release / Build Linux (push) Successful in 17m31s
Build & Release / Build Android (push) Successful in 22m5s
Build & Release / Create Release (push) Successful in 16s

This commit is contained in:
2026-07-06 20:24:46 +02:00
parent 68c8602230
commit acb7e5f221
14 changed files with 1008 additions and 28 deletions
+62 -5
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { get } from "svelte/store";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { platform } from "@tauri-apps/plugin-os";
@@ -7,8 +8,9 @@
import { auth, needsReauth, isAuthenticated } from "$lib/stores/auth";
import { connectivity, isConnected } from "$lib/stores/connectivity";
import { initPlayerEvents, cleanupPlayerEvents } from "$lib/services/playerEvents";
import { initDownloadEvents, cleanupDownloadEvents } from "$lib/stores/downloads";
import { downloads, initDownloadEvents, cleanupDownloadEvents } from "$lib/stores/downloads";
import { syncService } from "$lib/services/syncService";
import { onReconnected as onCatalogReconnected, syncCatalog, refreshSyncStatus, showServerCatalog, lastCatalogSync } from "$lib/services/offlineCatalog";
import { playbackMode } from "$lib/stores/playbackMode";
import { sessions } from "$lib/stores/sessions";
import { currentMedia, isPlaying, playbackPosition, playbackDuration } from "$lib/stores/player";
@@ -43,6 +45,20 @@
($isAndroid || !pathname.startsWith('/library'))
);
// The library and settings routes own their own full-height layout (their own
// scroll container + bottom-space reservation), so the root must leave their
// wrapper as a plain non-scrolling box. Every other top-level page (search,
// downloads, sessions, home) renders straight into the root, so the root
// wrapper has to scroll AND reserve the fixed bottom UI's height — otherwise
// the mini player / bottom nav overlay the last rows of content.
const routeOwnsLayout = $derived(
pathname.startsWith('/library') ||
pathname.startsWith('/settings') ||
pathname.startsWith('/player/') ||
pathname.startsWith('/login')
);
const showBottomUi = $derived(showBottomNav || showGlobalMiniPlayer);
$effect(() => {
const el = bottomUiEl;
if (!el) {
@@ -83,9 +99,26 @@
// Initialize download event listener
await initDownloadEvents();
// Prime the downloads store from the DB. The store starts empty each launch,
// and download badges (e.g. AlbumDownloadButton) derive purely from it, so
// without an initial refresh a previously-downloaded album shows as
// not-downloaded until the user opens the Downloads page.
const userId = get(auth).user?.id;
if (userId) {
downloads.refresh(userId).catch((err) =>
console.error("Initial downloads refresh failed:", err)
);
}
// Start sync service for offline mutation queue
syncService.start();
// Kick off a best-effort full-catalog pre-sync so the whole server catalog
// is browsable (greyed out) offline, and load the last-sync hint for the
// offline banner. Non-blocking — no-ops when not connected.
void syncCatalog();
void refreshSyncStatus();
// Initialize playback mode and session monitoring
playbackMode.initializeSessionMonitoring();
await playbackMode.refresh();
@@ -114,6 +147,8 @@
onServerReconnected: () => {
// Retry session verification when server becomes reachable
auth.retryVerification();
// Resume offline-queued downloads and refresh the catalog.
void onCatalogReconnected();
},
}).catch((monitorError) => {
console.error("[Layout] Failed to start connectivity monitoring:", monitorError);
@@ -153,13 +188,35 @@
{$pendingSyncCount} pending sync{$pendingSyncCount !== 1 ? 's' : ''}
</span>
{/if}
<button
type="button"
onclick={() => showServerCatalog.update((v) => !v)}
class="ml-1 bg-white/20 hover:bg-white/30 px-2 py-0.5 rounded-full text-xs transition-colors"
aria-pressed={$showServerCatalog}
title={$lastCatalogSync ? `Catalog last synced ${new Date($lastCatalogSync).toLocaleString()}` : 'Catalog not yet synced'}
>
{$showServerCatalog ? 'Hide server media' : 'Show all server media'}
</button>
</div>
{/if}
<!-- Main content -->
<div class="flex-1 overflow-hidden">
{@render children()}
</div>
<!-- Main content. Routes that own their full-height layout (library,
settings, player, login) get a plain clipped box and manage their own
scrolling internally. All other top-level pages render directly here, so
this wrapper must scroll and reserve the fixed bottom UI's measured
height so the mini player / bottom nav never overlap the last rows. -->
{#if routeOwnsLayout}
<div class="flex-1 overflow-hidden">
{@render children()}
</div>
{:else}
<div
class="flex-1 overflow-y-auto min-h-0"
style="padding-bottom: {showBottomUi ? `${$bottomUiHeight}px` : '0'}; overscroll-behavior: contain"
>
{@render children()}
</div>
{/if}
<!-- Re-authentication modal -->
<ReauthModal isOpen={$needsReauth} />