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>
|
||||||
+1
-3
@@ -5,8 +5,6 @@ import { toLikedTrack, toPlaylist, toTrack } from '$lib/server/mappers';
|
|||||||
import { authConfig } from '$lib/server/config';
|
import { authConfig } from '$lib/server/config';
|
||||||
import { LIKED_TRACKS_PAGE_SIZE, PLAYLIST_PICKER_PAGE_SIZE } from '$lib/config';
|
import { LIKED_TRACKS_PAGE_SIZE, PLAYLIST_PICKER_PAGE_SIZE } from '$lib/config';
|
||||||
|
|
||||||
const PUBLIC_PATHS = new Set(['/auth']);
|
|
||||||
|
|
||||||
function orFallback<T>(load: Promise<T>, fallback: T): Promise<T> {
|
function orFallback<T>(load: Promise<T>, fallback: T): Promise<T> {
|
||||||
return load.catch(() => fallback);
|
return load.catch(() => fallback);
|
||||||
}
|
}
|
||||||
@@ -14,7 +12,7 @@ function orFallback<T>(load: Promise<T>, fallback: T): Promise<T> {
|
|||||||
export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
|
export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
|
||||||
const allowAnonymousListening = await getAllowAnonymousListening(fetch);
|
const allowAnonymousListening = await getAllowAnonymousListening(fetch);
|
||||||
|
|
||||||
if (!locals.user && !allowAnonymousListening && !PUBLIC_PATHS.has(url.pathname)) {
|
if (!locals.user && !allowAnonymousListening) {
|
||||||
loginRedirect(url);
|
loginRedirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="animate-enter mx-auto my-[14vh] max-w-prose-sm text-center">
|
<div class="min-h-screen bg-bg text-fg antialiased">
|
||||||
<h1 class="mb-2.5 text-display-3 text-fg">
|
<div class="mx-auto my-[14vh] max-w-prose-sm text-center">
|
||||||
{page.status === 404 ? 'Página no encontrada' : 'No se pudo cargar tu música'}
|
<h1 class="mb-2.5 text-display-3">
|
||||||
</h1>
|
{page.status === 404 ? 'Página no encontrada' : 'Algo salió mal'}
|
||||||
<p class="mb-6.5 text-body text-fg-2">
|
</h1>
|
||||||
{page.status === 404
|
<a href="/" class="text-body text-fg-2 underline">Volver al inicio</a>
|
||||||
? 'Comprueba el enlace o vuelve al inicio.'
|
</div>
|
||||||
: 'Algo salió mal al cargar la página. Inténtalo de nuevo en unos momentos.'}
|
|
||||||
</p>
|
|
||||||
<Button href="/" variant="secondary">Volver</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,134 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { LayoutData } from './$types';
|
|
||||||
import '@fontsource-variable/sora/index.css';
|
import '@fontsource-variable/sora/index.css';
|
||||||
import '@fontsource-variable/plus-jakarta-sans/index.css';
|
import '@fontsource-variable/plus-jakarta-sans/index.css';
|
||||||
import '$lib/theme/theme.css';
|
import '$lib/theme/theme.css';
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
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 DialogHost from '$lib/components/ui/overlay/DialogHost.svelte';
|
import DialogHost from '$lib/components/ui/overlay/DialogHost.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 { ACCENT_HUE } from '$lib/theme/color';
|
import { ACCENT_HUE } from '$lib/theme/color';
|
||||||
import { buildThemeTokens, tokensToCss } from '$lib/theme/tokens';
|
import { buildThemeTokens, tokensToCss } from '$lib/theme/tokens';
|
||||||
import { accent, animateThemeHue } from '$lib/theme/accent.svelte';
|
import { accent, animateThemeHue } from '$lib/theme/accent.svelte';
|
||||||
import { themeMode } from '$lib/theme/mode.svelte';
|
import { themeMode } from '$lib/theme/mode.svelte';
|
||||||
import { trackNavigation } from '$lib/state/history.svelte';
|
|
||||||
import { restoreScroll } from '$lib/state/scroll';
|
|
||||||
import { page } from '$app/state';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
data: LayoutData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children, data }: Props = $props();
|
let { children }: 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAuthPage = $derived(page.url.pathname === '/auth');
|
|
||||||
|
|
||||||
const initialThemeCss = `:root{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'dark'))}}:root[data-theme='light']{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'light'))}}`;
|
const initialThemeCss = `:root{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'dark'))}}:root[data-theme='light']{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'light'))}}`;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => animateThemeHue(accent.hue, themeMode.current));
|
$effect(() => animateThemeHue(accent.hue, themeMode.current));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={onWindowKeydown} />
|
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- CSS generado a partir de números, sin entrada de usuario -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- CSS generado a partir de números, sin entrada de usuario -->
|
||||||
{@html `<style>${initialThemeCss}</style>`}
|
{@html `<style>${initialThemeCss}</style>`}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if isAuthPage || (!data.user && !data.allowAnonymousListening)}
|
{@render children()}
|
||||||
<div class="min-h-screen bg-bg text-fg antialiased">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<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>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<DialogHost />
|
<DialogHost />
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
<meta name="description" content="Inicia sesión o crea una cuenta en Musify." />
|
<meta name="description" content="Inicia sesión o crea una cuenta en Musify." />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="relative grid min-h-screen place-items-center overflow-hidden bg-bg px-6 text-fg">
|
<div
|
||||||
|
class="relative grid min-h-screen place-items-center overflow-hidden bg-bg px-6 text-fg antialiased"
|
||||||
|
>
|
||||||
<div class="pointer-events-none absolute inset-0" style="background:var(--mf-ambient)"></div>
|
<div class="pointer-events-none absolute inset-0" style="background:var(--mf-ambient)"></div>
|
||||||
|
|
||||||
<div class="relative flex w-full max-w-4xl flex-col items-center gap-12 text-center">
|
<div class="relative flex w-full max-w-4xl flex-col items-center gap-12 text-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user