Move the app shell and its data loading into the (app) route group layout
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
||||
</script>
|
||||
|
||||
<div class="animate-enter mx-auto my-[14vh] max-w-prose-sm text-center">
|
||||
<h1 class="mb-2.5 text-display-3 text-fg">
|
||||
{page.status === 404 ? 'Página no encontrada' : 'No se pudo cargar tu música'}
|
||||
</h1>
|
||||
<p class="mb-6.5 text-body text-fg-2">
|
||||
{page.status === 404
|
||||
? 'Comprueba el enlace o vuelve al inicio.'
|
||||
: 'Algo salió mal al cargar la página. Inténtalo de nuevo en unos momentos.'}
|
||||
</p>
|
||||
<Button href="/" variant="secondary">Volver</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { getAllowAnonymousListening } from '$lib/server/playbackConfig';
|
||||
import { apiFor, loginRedirect } from '$lib/server/api';
|
||||
import { toLikedTrack, toPlaylist, toTrack } from '$lib/server/mappers';
|
||||
import { authConfig } from '$lib/server/config';
|
||||
import { LIKED_TRACKS_PAGE_SIZE, PLAYLIST_PICKER_PAGE_SIZE } from '$lib/config';
|
||||
|
||||
function orFallback<T>(load: Promise<T>, fallback: T): Promise<T> {
|
||||
return load.catch(() => fallback);
|
||||
}
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
|
||||
const allowAnonymousListening = await getAllowAnonymousListening(fetch);
|
||||
|
||||
if (!locals.user && !allowAnonymousListening) {
|
||||
loginRedirect(url);
|
||||
}
|
||||
|
||||
const accountUrl = `${authConfig.issuer.replace(/\/+$/, '')}/ui/console`;
|
||||
|
||||
if (!locals.user) {
|
||||
return {
|
||||
user: locals.user,
|
||||
allowAnonymousListening,
|
||||
userPlaylists: [],
|
||||
userPlaylistsTotal: 0,
|
||||
likedTracks: [],
|
||||
lastPlayedTrack: null,
|
||||
accountUrl
|
||||
};
|
||||
}
|
||||
|
||||
const api = apiFor({ fetch, locals });
|
||||
const userId = locals.user.sub;
|
||||
|
||||
const [userPlaylists, likedTracks, lastPlayedTrack] = await Promise.all([
|
||||
orFallback(
|
||||
api
|
||||
.GET('/playlists/users/{userId}', {
|
||||
params: {
|
||||
path: { userId },
|
||||
query: { pageNumber: 1, pageSize: PLAYLIST_PICKER_PAGE_SIZE }
|
||||
}
|
||||
})
|
||||
.then(({ data }) => ({
|
||||
items: (data?.items ?? []).map(toPlaylist),
|
||||
total: Number(data?.totalItemCount ?? 0)
|
||||
})),
|
||||
{ items: [], total: 0 }
|
||||
),
|
||||
orFallback(
|
||||
api
|
||||
.GET('/likes', {
|
||||
params: { query: { pageNumber: 1, pageSize: LIKED_TRACKS_PAGE_SIZE } }
|
||||
})
|
||||
.then(({ data }) => (data?.items ?? []).map(toLikedTrack)),
|
||||
[]
|
||||
),
|
||||
orFallback(
|
||||
api
|
||||
.GET('/users/{id}/last-listened-track', { params: { path: { id: userId } } })
|
||||
.then(({ data }) => (data ? toTrack(data) : null)),
|
||||
null
|
||||
)
|
||||
]);
|
||||
|
||||
return {
|
||||
user: locals.user,
|
||||
allowAnonymousListening,
|
||||
userPlaylists: userPlaylists.items,
|
||||
userPlaylistsTotal: userPlaylists.total,
|
||||
likedTracks,
|
||||
lastPlayedTrack,
|
||||
accountUrl
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import PlayerDock from '$lib/components/player/PlayerDock.svelte';
|
||||
import Queue from '$lib/components/player/Queue.svelte';
|
||||
import Sidebar from '$lib/components/layout/Sidebar.svelte';
|
||||
import TopBar from '$lib/components/layout/TopBar.svelte';
|
||||
import MobileHeader from '$lib/components/layout/MobileHeader.svelte';
|
||||
import Modal from '$lib/components/ui/overlay/Modal.svelte';
|
||||
import PlaylistForm from '$lib/components/ui/forms/PlaylistForm.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { queuePanel } from '$lib/state/panels.svelte';
|
||||
import { liked } from '$lib/player/liked.svelte';
|
||||
import { createPlaylistModal } from '$lib/state/panels.svelte';
|
||||
import { trackNavigation } from '$lib/state/history.svelte';
|
||||
import { restoreScroll } from '$lib/state/scroll';
|
||||
import { page } from '$app/state';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
data: LayoutData;
|
||||
}
|
||||
|
||||
let { children, data }: Props = $props();
|
||||
|
||||
let scroller = $state<HTMLElement>();
|
||||
|
||||
trackNavigation();
|
||||
restoreScroll(() => scroller);
|
||||
|
||||
$effect(() => {
|
||||
if (data.likedTracks?.length) liked.hydrate(data.likedTracks);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (data.lastPlayedTrack) player.hydrate(data.lastPlayedTrack);
|
||||
});
|
||||
|
||||
let queueAutoOpened = false;
|
||||
$effect(() => {
|
||||
if (!player.playing || queueAutoOpened) return;
|
||||
queueAutoOpened = true;
|
||||
queuePanel.show();
|
||||
});
|
||||
|
||||
function isTypingTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
return ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'].includes(target.tagName);
|
||||
}
|
||||
|
||||
function onWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.code !== 'Space' || isTypingTarget(event.target)) return;
|
||||
event.preventDefault();
|
||||
player.toggle();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onWindowKeydown} />
|
||||
|
||||
<div class="relative flex h-dvh overflow-hidden text-fg antialiased">
|
||||
<div class="app-backdrop"></div>
|
||||
<div class="app-vignette"></div>
|
||||
|
||||
<Sidebar
|
||||
user={data.user}
|
||||
playlists={data.userPlaylists ?? []}
|
||||
playlistCount={data.userPlaylistsTotal ?? 0}
|
||||
/>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<div bind:this={scroller} class="flex min-w-0 flex-1 app-scroll flex-col">
|
||||
<MobileHeader user={data.user} accountUrl={data.accountUrl} />
|
||||
<TopBar user={data.user} accountUrl={data.accountUrl} />
|
||||
<main class="flex-1">
|
||||
{#key page.url.pathname}
|
||||
<div class="animate-fade">{@render children()}</div>
|
||||
{/key}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Queue />
|
||||
</div>
|
||||
|
||||
<PlayerDock user={data.user} />
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={createPlaylistModal.open}
|
||||
onClose={() => createPlaylistModal.close()}
|
||||
title="Crear nueva playlist"
|
||||
>
|
||||
<PlaylistForm
|
||||
action="/playlists?/create"
|
||||
formMessage={page.form?.message}
|
||||
submitLabel="Crear"
|
||||
submittingLabel="Creando…"
|
||||
helperText="Podrás añadir canciones más tarde."
|
||||
onCancel={() => createPlaylistModal.close()}
|
||||
onSuccess={() => createPlaylistModal.close()}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
Reference in New Issue
Block a user