Files
jellytau/src/routes/+layout.svelte
T
dtourolle d01c2aab9f
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 4m39s
Traceability Validation / Check Requirement Traces (pull_request) Failing after 36s
🏗️ Build and Test JellyTau / Build Android APK (pull_request) Failing after 1m57s
Migrate all IPC call sites to typed tauri-specta commands.*
Replace the remaining ~155 untyped invoke() calls across stores, services,
components, and routes with the generated commands.* wrappers from
$lib/api/bindings, so every IPC call is compile-time-checked against the
command signatures.

- Register repository_get_subtitle_url and repository_get_video_download_url
  in specta_builder() and the invoke_handler; regenerate bindings.ts.
- Source duplicated wire types (AutoplaySettings, CacheConfig, Session,
  ConnectivityStatus, audio/video settings, etc.) from bindings.
- Fix two bugs surfaced by the typed wrappers:
  - VideoDownloadButton passed an un-awaited Promise as the stream URL.
  - setAutoplaySettings omitted the required userId argument.
- Update unit tests asserting the old invoke(name, args) shape.
- Remove the five param-naming guard tests; the compiler and codegen now
  enforce what they checked.

svelte-check: 0 errors. vitest: green. cargo test --lib: green.
2026-06-21 08:47:04 +02:00

168 lines
6.5 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { platform } from "@tauri-apps/plugin-os";
import "../app.css";
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 { syncService } from "$lib/services/syncService";
import { playbackMode } from "$lib/stores/playbackMode";
import { sessions } from "$lib/stores/sessions";
import { currentMedia, isPlaying, playbackPosition, playbackDuration } from "$lib/stores/player";
import ReauthModal from "$lib/components/auth/ReauthModal.svelte";
import Toast from "$lib/components/Toast.svelte";
import MiniPlayer from "$lib/components/player/MiniPlayer.svelte";
import SleepTimerModal from "$lib/components/player/SleepTimerModal.svelte";
import BottomNav from "$lib/components/BottomNav.svelte";
import { isInitialized, pendingSyncCount, isAndroid, shuffle, repeat, hasNext, hasPrevious, showSleepTimerModal } from "$lib/stores/appState";
let { children } = $props();
onMount(async () => {
// Initialize auth state (restore session from secure storage)
await auth.initialize();
isInitialized.set(true);
// Detect platform (Android needs global mini player)
try {
const platformName = await platform();
isAndroid.set(platformName === "android");
} catch (err) {
console.error("Platform detection failed:", err);
}
// Initialize player event listener for push-based updates
await initPlayerEvents();
// Initialize download event listener
await initDownloadEvents();
// Start sync service for offline mutation queue
syncService.start();
// Initialize playback mode and session monitoring
playbackMode.initializeSessionMonitoring();
await playbackMode.refresh();
});
onDestroy(() => {
cleanupPlayerEvents();
cleanupDownloadEvents();
connectivity.stopMonitoring();
syncService.stop();
auth.cleanupEventListeners();
});
// Connectivity monitoring is now started early in auth.initialize()
// This effect is kept only for when the user logs in during the session
$effect(() => {
if ($isAuthenticated) {
// Check if monitoring is already running by attempting to get status
// If not running, start it (handles login during current session)
auth.getCurrentSession().then((session) => {
if (session?.serverUrl) {
connectivity.forceCheck().catch((error) => {
// If check fails, monitoring might not be started yet, so start it
console.debug("[Layout] Queue status check failed, starting monitoring:", error);
connectivity.startMonitoring(session.serverUrl, {
onServerReconnected: () => {
// Retry session verification when server becomes reachable
auth.retryVerification();
},
}).catch((monitorError) => {
console.error("[Layout] Failed to start connectivity monitoring:", monitorError);
});
});
}
});
}
});
// Update pending sync count periodically
$effect(() => {
if ($isAuthenticated) {
const updateCount = async () => {
const count = await syncService.getPendingCount();
pendingSyncCount.set(count);
};
updateCount();
// Update every 10 seconds
const interval = setInterval(updateCount, 10000);
return () => clearInterval(interval);
}
});
</script>
<div class="h-screen bg-[var(--color-background)] overflow-hidden flex flex-col">
{#if isInitialized}
<!-- Offline indicator banner -->
{#if $isAuthenticated && !$isConnected}
<div class="bg-amber-600/90 text-white px-4 py-2 text-sm flex items-center justify-center gap-2 shrink-0">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
</svg>
<span>You're offline. Some features may be limited.</span>
{#if $pendingSyncCount > 0}
<span class="bg-white/20 px-2 py-0.5 rounded-full text-xs">
{$pendingSyncCount} pending sync{$pendingSyncCount !== 1 ? 's' : ''}
</span>
{/if}
</div>
{/if}
<!-- Main content -->
<div class="flex-1 overflow-hidden">
{@render children()}
</div>
<!-- Re-authentication modal -->
<ReauthModal isOpen={$needsReauth} />
<!-- Toast notifications (global) -->
<Toast />
<!-- Bottom Navigation (mobile only - show everywhere except player and login) -->
{#if $isAuthenticated && !$page.url.pathname.startsWith('/player/') && !$page.url.pathname.startsWith('/login')}
<BottomNav />
{/if}
<!-- Mini Player - show everywhere except on full player page and login -->
<!-- Android: Show on all routes (except player/login) -->
<!-- Desktop: Show on non-library routes (library layout has its own MiniPlayer) -->
{#if !$page.url.pathname.startsWith('/player/') && !$page.url.pathname.startsWith('/login')}
{#if $isAndroid || !$page.url.pathname.startsWith('/library')}
<MiniPlayer
media={$currentMedia}
isPlaying={$isPlaying}
position={$playbackPosition}
duration={$playbackDuration}
shuffle={$shuffle}
repeat={$repeat}
hasNext={$hasNext}
hasPrevious={$hasPrevious}
onExpand={() => {
// Navigate to player page when mini player is expanded
if ($currentMedia) {
goto(`/player/${$currentMedia.id}`);
}
}}
onSleepTimerClick={() => showSleepTimerModal.set(true)}
/>
<!-- Sleep Timer Modal -->
<SleepTimerModal
isOpen={$showSleepTimerModal}
onClose={() => showSleepTimerModal.set(false)}
/>
{/if}
{/if}
{:else}
<div class="flex items-center justify-center h-screen">
<div class="w-8 h-8 border-2 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
</div>
{/if}
</div>