6ff00ea24d
Single mount point alongside useMediaSession. Once called, the dispatcher's \$effect watchers run for the lifetime of the SPA.
90 lines
2.8 KiB
Svelte
90 lines
2.8 KiB
Svelte
<script lang="ts">
|
|
import '../app.css';
|
|
import { page } from '$app/state';
|
|
import { goto } from '$app/navigation';
|
|
import { QueryClientProvider } from '@tanstack/svelte-query';
|
|
import { queryClient } from '$lib/query/client';
|
|
import { user } from '$lib/auth/store.svelte';
|
|
import Shell from '$lib/components/Shell.svelte';
|
|
import {
|
|
registerAudioEl,
|
|
reportTimeUpdate,
|
|
reportDuration,
|
|
reportStateFromAudio,
|
|
player
|
|
} from '$lib/player/store.svelte';
|
|
import { useMediaSession } from '$lib/player/mediaSession.svelte';
|
|
import { useEventsDispatcher } from '$lib/player/events.svelte';
|
|
|
|
let { children } = $props<{ children: import('svelte').Snippet }>();
|
|
let audioEl: HTMLAudioElement | undefined = $state();
|
|
|
|
// Reactive guard: runs every time page.url or user.value changes.
|
|
$effect(() => {
|
|
const path = page.url.pathname;
|
|
const isLogin = path === '/login';
|
|
if (user.value === null && !isLogin) {
|
|
const returnTo = path + page.url.search;
|
|
goto('/login?returnTo=' + encodeURIComponent(returnTo), { replaceState: true });
|
|
} else if (user.value !== null && isLogin) {
|
|
goto('/', { replaceState: true });
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (audioEl) registerAudioEl(audioEl);
|
|
return () => registerAudioEl(null);
|
|
});
|
|
|
|
$effect(() => {
|
|
if (!audioEl) return;
|
|
const src = player.current?.stream_url;
|
|
if (src) {
|
|
const absolute = new URL(src, window.location.origin).href;
|
|
if (audioEl.src !== absolute) audioEl.src = src;
|
|
} else {
|
|
audioEl.removeAttribute('src');
|
|
audioEl.load();
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (audioEl) audioEl.volume = player.volume;
|
|
});
|
|
|
|
$effect(() => {
|
|
if (!audioEl) return;
|
|
// "Intent to play" = loading or playing. During 'loading' the DOM hasn't
|
|
// emitted 'playing' yet, but we still need to call play() to get there.
|
|
const intent = player.state === 'playing' || player.state === 'loading';
|
|
if (intent) {
|
|
audioEl.play().catch(() => { /* interrupted / blocked — store gets the event */ });
|
|
} else {
|
|
audioEl.pause();
|
|
}
|
|
});
|
|
|
|
useMediaSession();
|
|
useEventsDispatcher();
|
|
</script>
|
|
|
|
<audio
|
|
bind:this={audioEl}
|
|
preload="metadata"
|
|
ontimeupdate={() => audioEl && reportTimeUpdate(audioEl.currentTime)}
|
|
onloadedmetadata={() => audioEl && reportDuration(audioEl.duration)}
|
|
onplaying={() => reportStateFromAudio('playing')}
|
|
onpause={() => reportStateFromAudio('paused')}
|
|
onwaiting={() => reportStateFromAudio('waiting')}
|
|
onended={() => reportStateFromAudio('ended')}
|
|
onerror={() => reportStateFromAudio('error', audioEl?.error?.message)}
|
|
></audio>
|
|
|
|
<QueryClientProvider client={queryClient}>
|
|
{#if user.value !== null && page.url.pathname !== '/login'}
|
|
<Shell>{@render children()}</Shell>
|
|
{:else}
|
|
{@render children()}
|
|
{/if}
|
|
</QueryClientProvider>
|