Files
minstrel/web/src/routes/+layout.svelte
T
bvandeusen 571eb2ec53 feat(web): mount <audio> at root layout and wire it to the player store
Single hidden <audio> survives navigation. Effects drive it from the
store (src, volume, play/pause) and DOM events report back (timeupdate,
loadedmetadata, playing, pause, waiting, ended, error). useMediaSession
is called once here so OS media controls light up as soon as the user
signs in.
2026-04-24 21:43:03 -04:00

85 lines
2.5 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';
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;
if (player.isPlaying) {
audioEl.play().catch(() => { /* interrupted / blocked — store gets the event */ });
} else {
audioEl.pause();
}
});
useMediaSession();
</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>