571eb2ec53
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.
85 lines
2.5 KiB
Svelte
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>
|