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.
This commit is contained in:
2026-04-24 21:43:03 -04:00
parent ccef9be2d3
commit 571eb2ec53
+53
View File
@@ -6,8 +6,17 @@
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(() => {
@@ -20,8 +29,52 @@
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>