First working POC
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
<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 { invoke } from "@tauri-apps/api/core";
|
||||
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";
|
||||
|
||||
let { children } = $props();
|
||||
let isInitialized = $state(false);
|
||||
let pendingSyncCount = $state(0);
|
||||
let isAndroid = $state(false);
|
||||
let shuffle = $state(false);
|
||||
let repeat = $state<"off" | "all" | "one">("off");
|
||||
let hasNext = $state(false);
|
||||
let hasPrevious = $state(false);
|
||||
let showSleepTimerModal = $state(false);
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
// Initialize auth state (restore session from secure storage)
|
||||
await auth.initialize();
|
||||
isInitialized = true;
|
||||
|
||||
// Detect platform (Android needs global mini player)
|
||||
try {
|
||||
const platformName = await platform();
|
||||
isAndroid = 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();
|
||||
|
||||
// Poll for queue status (needed for mini player controls on all platforms)
|
||||
updateQueueStatus(); // Initial update
|
||||
pollInterval = setInterval(updateQueueStatus, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
cleanupPlayerEvents();
|
||||
cleanupDownloadEvents();
|
||||
connectivity.stopMonitoring();
|
||||
syncService.stop();
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
});
|
||||
|
||||
async function updateQueueStatus() {
|
||||
try {
|
||||
const queue = await invoke<{
|
||||
items: any[];
|
||||
currentIndex: number | null;
|
||||
hasNext: boolean;
|
||||
hasPrevious: boolean;
|
||||
shuffle: boolean;
|
||||
repeat: string;
|
||||
}>("player_get_queue");
|
||||
|
||||
hasNext = queue.hasNext;
|
||||
hasPrevious = queue.hasPrevious;
|
||||
shuffle = queue.shuffle;
|
||||
repeat = queue.repeat as "off" | "all" | "one";
|
||||
} catch (e) {
|
||||
// Silently ignore polling errors
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
const session = auth.getCurrentSession();
|
||||
if (session?.serverUrl) {
|
||||
connectivity.forceCheck().catch(() => {
|
||||
// If check fails, monitoring might not be started yet, so start it
|
||||
connectivity.startMonitoring(session.serverUrl, {
|
||||
onServerReconnected: () => {
|
||||
// Retry session verification when server becomes reachable
|
||||
auth.retryVerification();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update pending sync count periodically
|
||||
$effect(() => {
|
||||
if ($isAuthenticated) {
|
||||
const updateCount = async () => {
|
||||
pendingSyncCount = await syncService.getPendingCount();
|
||||
};
|
||||
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}
|
||||
{repeat}
|
||||
{hasNext}
|
||||
{hasPrevious}
|
||||
onExpand={() => {
|
||||
// Navigate to player page when mini player is expanded
|
||||
if ($currentMedia) {
|
||||
goto(`/player/${$currentMedia.id}`);
|
||||
}
|
||||
}}
|
||||
onSleepTimerClick={() => showSleepTimerModal = true}
|
||||
/>
|
||||
|
||||
<!-- Sleep Timer Modal -->
|
||||
<SleepTimerModal
|
||||
isOpen={showSleepTimerModal}
|
||||
onClose={() => showSleepTimerModal = 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>
|
||||
Reference in New Issue
Block a user