Files
minstrel/web/src/routes/+layout.svelte
T

106 lines
3.3 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 QueueDrawer from '$lib/components/QueueDrawer.svelte';
import {
registerAudioEl,
reportTimeUpdate,
reportDuration,
reportStateFromAudio,
player,
closeQueueDrawer
} from '$lib/player/store.svelte';
import { useMediaSession } from '$lib/player/mediaSession.svelte';
import { useEventsDispatcher } from '$lib/player/events.svelte';
import { applyMetaThemeColor } from '$lib/theme/applyMetaThemeColor.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();
}
});
$effect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape' && player.queueDrawerOpen) {
closeQueueDrawer();
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
});
useMediaSession();
useEventsDispatcher();
applyMetaThemeColor();
</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>
<QueueDrawer />
<QueryClientProvider client={queryClient}>
{#if user.value !== null && page.url.pathname !== '/login'}
<Shell>{@render children()}</Shell>
{:else}
{@render children()}
{/if}
</QueryClientProvider>