web-player: split player, navigation and accent state into focused modules
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { albumCover } from '$lib/utils/hrefs';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import Disc from '@lucide/svelte/icons/disc-3';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
@@ -42,7 +43,7 @@
|
||||
title={album.title}
|
||||
subtitle={albumMeta(album.releaseYear, album.trackCount)}
|
||||
trackIds={album.coverTrackIds}
|
||||
src="/api/albums/{album.id}/cover?size=large"
|
||||
src={albumCover(album.id, 'large')}
|
||||
index={i}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import { albumCover } from '$lib/utils/hrefs';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
@@ -59,7 +61,7 @@
|
||||
>
|
||||
{#snippet cover()}
|
||||
<Artwork
|
||||
src="/api/albums/{album.id}/cover?size=large"
|
||||
src={albumCover(album.id, 'large')}
|
||||
trackIds={tracks.map((track) => track.id)}
|
||||
size="hero"
|
||||
alt={album.title}
|
||||
@@ -116,7 +118,7 @@
|
||||
initialTitle={album.title}
|
||||
initialDescription={album.description ?? ''}
|
||||
initialReleaseYear={album.releaseYear ?? undefined}
|
||||
coverFallbackUrl="/api/albums/{album.id}/cover?size=medium"
|
||||
coverFallbackUrl={albumCover(album.id, 'medium')}
|
||||
formMessage={form?.message}
|
||||
submitLabel="Guardar"
|
||||
submittingLabel="Guardando…"
|
||||
|
||||
@@ -18,19 +18,21 @@
|
||||
import Chip from '$lib/components/ui/primitives/Chip.svelte';
|
||||
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
||||
import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
||||
import { genreHref } from '$lib/state/navigation.svelte';
|
||||
import { genreHref, playlistCover } from '$lib/utils/hrefs';
|
||||
import { createTrackMenu } from '$lib/state/menus.svelte';
|
||||
import ContextMenu, { contextMenuPosition } from '$lib/components/ui/overlay/ContextMenu.svelte';
|
||||
import ContextMenu from '$lib/components/ui/overlay/ContextMenu.svelte';
|
||||
import { contextMenuPosition } from '$lib/utils/menuPosition';
|
||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||
import ListEnd from '@lucide/svelte/icons/list-end';
|
||||
import { appendUnique } from '$lib/data/collections';
|
||||
import { appendUnique } from '$lib/utils/collections';
|
||||
import { genreInfo, genreTiles } from '$lib/data/genres';
|
||||
import {
|
||||
type SearchFilter,
|
||||
showGroup,
|
||||
capped,
|
||||
matchPlaylists,
|
||||
searchCounts,
|
||||
SEARCH_LABELS,
|
||||
searchTotal,
|
||||
searchChips,
|
||||
findTopResult
|
||||
} from '$lib/data/search';
|
||||
@@ -48,7 +50,7 @@
|
||||
|
||||
const trackMenu = createTrackMenu();
|
||||
let albumMenu = $state<AlbumMenuState | null>(null);
|
||||
let sfilter = $state<SearchFilter>('Todo');
|
||||
let sfilter = $state<SearchFilter>('all');
|
||||
|
||||
let localItems = $state<Track[]>([]);
|
||||
let localPage = $state(1);
|
||||
@@ -58,7 +60,7 @@
|
||||
$effect(() => {
|
||||
trackMenu.close();
|
||||
albumMenu = null;
|
||||
sfilter = data.genre ? 'Canciones' : 'Todo';
|
||||
sfilter = data.genre ? 'tracks' : 'all';
|
||||
localItems = data.tracks.items;
|
||||
localPage = data.tracks.pageNumber;
|
||||
hasMore = data.tracks.hasNextPage;
|
||||
@@ -82,18 +84,14 @@
|
||||
!!data.query && items.length === 0 && !hasAlbums && !hasUsers && !hasPlaylists
|
||||
);
|
||||
|
||||
const counts = $derived(
|
||||
searchCounts({
|
||||
tracks: data.tracks.totalItemCount,
|
||||
albums: data.albumsTotal,
|
||||
playlists: playlistMatches.length,
|
||||
users: data.usersTotal
|
||||
})
|
||||
);
|
||||
const counts = $derived({
|
||||
tracks: data.tracks.totalItemCount,
|
||||
albums: data.albumsTotal,
|
||||
playlists: playlistMatches.length,
|
||||
users: data.usersTotal
|
||||
});
|
||||
const chips = $derived(searchChips(counts));
|
||||
const totalHits = $derived(
|
||||
counts.Canciones + counts.Álbumes + counts.Playlists + counts.Usuarios
|
||||
);
|
||||
const totalHits = $derived(searchTotal(counts));
|
||||
|
||||
type TopResult = NonNullable<
|
||||
ReturnType<
|
||||
@@ -107,7 +105,7 @@
|
||||
>;
|
||||
|
||||
const topResult = $derived<TopResult | null>(
|
||||
sfilter === 'Todo' && data.query
|
||||
sfilter === 'all' && data.query
|
||||
? findTopResult({
|
||||
query: data.query,
|
||||
tracks: items,
|
||||
@@ -135,6 +133,7 @@
|
||||
}
|
||||
|
||||
function openAlbumMenu(event: MouseEvent, albumId: string) {
|
||||
event.preventDefault();
|
||||
albumMenu = { ...contextMenuPosition(event), albumId };
|
||||
}
|
||||
|
||||
@@ -206,11 +205,11 @@
|
||||
/>
|
||||
|
||||
<div class="mb-7 flex flex-wrap gap-2 sm:mb-8">
|
||||
{#each chips as chip (chip.label)}
|
||||
{#each chips as chip (chip.filter)}
|
||||
<Chip
|
||||
selected={sfilter === chip.label}
|
||||
selected={sfilter === chip.filter}
|
||||
count={chip.count}
|
||||
onclick={() => (sfilter = chip.label)}
|
||||
onclick={() => (sfilter = chip.filter)}
|
||||
>
|
||||
{chip.label}
|
||||
</Chip>
|
||||
@@ -230,9 +229,7 @@
|
||||
<Artwork trackIds={topResult.album.coverTrackIds} size="xl" class="shrink-0" />
|
||||
{:else if topResult.kind === 'playlist'}
|
||||
<Artwork
|
||||
src="/api/playlists/{topResult.playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
topResult.playlist.updatedAt
|
||||
)}"
|
||||
src={playlistCover(topResult.playlist, 'large')}
|
||||
trackIds={topResult.playlist.coverTrackIds}
|
||||
size="xl"
|
||||
class="shrink-0"
|
||||
@@ -322,10 +319,10 @@
|
||||
|
||||
{#if (data.query || data.genre) && !nothingFound}
|
||||
<div class="mt-9 flex flex-col gap-9">
|
||||
{#if showGroup(sfilter, 'Canciones')}
|
||||
{#if showGroup(sfilter, 'tracks')}
|
||||
<div>
|
||||
{#if items.length > 0}
|
||||
<SectionHeading title="Canciones" count={counts.Canciones} />
|
||||
<SectionHeading title={SEARCH_LABELS.tracks} count={counts.tracks} />
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(sfilter, items) as track, i (track.id)}
|
||||
<li>
|
||||
@@ -355,7 +352,7 @@
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if sfilter === 'Canciones' && hasMore}
|
||||
{#if sfilter === 'tracks' && hasMore}
|
||||
<InfiniteScroll onLoadMore={loadMore} {hasMore} loading={loadingMore} />
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -369,9 +366,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasAlbums && showGroup(sfilter, 'Álbumes')}
|
||||
{#if hasAlbums && showGroup(sfilter, 'albums')}
|
||||
<div>
|
||||
<SectionHeading title="Álbumes" count={counts.Álbumes} />
|
||||
<SectionHeading title={SEARCH_LABELS.albums} count={counts.albums} />
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(sfilter, albums) as album (album.id)}
|
||||
<li>
|
||||
@@ -394,9 +391,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasUsers && showGroup(sfilter, 'Usuarios')}
|
||||
{#if hasUsers && showGroup(sfilter, 'users')}
|
||||
<div>
|
||||
<SectionHeading title="Usuarios" count={counts.Usuarios} />
|
||||
<SectionHeading title={SEARCH_LABELS.users} count={counts.users} />
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(sfilter, users) as u (u.id)}
|
||||
<li>
|
||||
@@ -407,9 +404,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasPlaylists && showGroup(sfilter, 'Playlists')}
|
||||
{#if hasPlaylists && showGroup(sfilter, 'playlists')}
|
||||
<div>
|
||||
<SectionHeading title="Playlists" count={counts.Playlists} />
|
||||
<SectionHeading title={SEARCH_LABELS.playlists} count={counts.playlists} />
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(sfilter, playlistMatches) as playlist (playlist.id)}
|
||||
<li>
|
||||
@@ -418,9 +415,7 @@
|
||||
subtitle={playlist.description}
|
||||
href="/playlists/{playlist.id}"
|
||||
size="lg"
|
||||
coverSrc="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
coverSrc={playlistCover(playlist, 'medium')}
|
||||
trackIds={playlist.coverTrackIds}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Heart from '@lucide/svelte/icons/heart';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
|
||||
import { liked } from '$lib/player/liked.svelte';
|
||||
import { plural } from '$lib/utils/format';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Shuffle from '@lucide/svelte/icons/shuffle';
|
||||
import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/actions';
|
||||
import { fmtTime, plural } from '$lib/utils/format';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import BackLink from '$lib/components/ui/primitives/BackLink.svelte';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { playlistCover } from '$lib/utils/hrefs';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||
import MediaCard from '$lib/components/ui/media/MediaCard.svelte';
|
||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
||||
import { createPlaylistModal } from '$lib/state/playlists.svelte';
|
||||
import { createPlaylistModal } from '$lib/state/panels.svelte';
|
||||
import { fmtDurationLong, plural, playlistMeta } from '$lib/utils/format';
|
||||
|
||||
let { data } = $props();
|
||||
@@ -42,9 +43,7 @@
|
||||
title={playlist.name}
|
||||
subtitle={playlistMeta(playlist.trackCount)}
|
||||
trackIds={playlist.coverTrackIds}
|
||||
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
src={playlistCover(playlist, 'large')}
|
||||
index={i}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { playlistCover } from '$lib/utils/hrefs';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||
import Modal from '$lib/components/ui/overlay/Modal.svelte';
|
||||
@@ -56,9 +58,7 @@
|
||||
>
|
||||
{#snippet cover()}
|
||||
<Artwork
|
||||
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
src={playlistCover(playlist, 'large')}
|
||||
trackIds={tracks.map((t) => t.id)}
|
||||
size="hero"
|
||||
alt={playlist.name}
|
||||
@@ -110,9 +110,7 @@
|
||||
initialName={playlist.name}
|
||||
initialDescription={playlist.description ?? ''}
|
||||
initialVisibility={playlist.visibility === 'Public' ? 'public' : 'private'}
|
||||
coverFallbackUrl="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
coverFallbackUrl={playlistCover(playlist, 'medium')}
|
||||
formMessage={form?.message}
|
||||
submitLabel="Guardar"
|
||||
submittingLabel="Guardando…"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { playlistCover } from '$lib/utils/hrefs';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import MediaCard from '$lib/components/ui/media/MediaCard.svelte';
|
||||
@@ -62,9 +63,7 @@
|
||||
title={playlist.name}
|
||||
subtitle={playlistMeta(playlist.trackCount)}
|
||||
trackIds={playlist.coverTrackIds}
|
||||
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
src={playlistCover(playlist, 'large')}
|
||||
index={i}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||
import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte';
|
||||
import UserRow from '$lib/components/ui/media/UserRow.svelte';
|
||||
import { appendUnique } from '$lib/data/collections';
|
||||
import { appendUnique } from '$lib/utils/collections';
|
||||
import { plural } from '$lib/utils/format';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import '@fontsource-variable/sora/index.css';
|
||||
import '@fontsource-variable/plus-jakarta-sans/index.css';
|
||||
import '$lib/theme/theme.css';
|
||||
@@ -13,16 +15,23 @@
|
||||
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/player/queuePanel.svelte';
|
||||
import { queuePanel } from '$lib/state/panels.svelte';
|
||||
import { liked } from '$lib/player/liked.svelte';
|
||||
import { createPlaylistModal } from '$lib/state/playlists.svelte';
|
||||
import { createPlaylistModal } from '$lib/state/panels.svelte';
|
||||
import { ACCENT_HUE } from '$lib/theme/color';
|
||||
import { buildThemeTokens, applyThemeTokens, tokensToCss } from '$lib/theme/tokens';
|
||||
import { buildThemeTokens, tokensToCss } from '$lib/theme/tokens';
|
||||
import { accent, animateThemeHue } from '$lib/theme/accent.svelte';
|
||||
import { themeMode } from '$lib/theme/mode.svelte';
|
||||
import { trackNavigation, restoreScroll } from '$lib/state/navigation.svelte';
|
||||
import { trackNavigation } from '$lib/state/history.svelte';
|
||||
import { restoreScroll } from '$lib/state/scroll';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { children, data } = $props();
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
data: LayoutData;
|
||||
}
|
||||
|
||||
let { children, data }: Props = $props();
|
||||
|
||||
let scroller = $state<HTMLElement>();
|
||||
|
||||
@@ -60,35 +69,7 @@
|
||||
player.toggle();
|
||||
}
|
||||
|
||||
function parseHue(value: string): number | null {
|
||||
const m = /oklch\(\s*[\d.]+%?\s+[\d.]+\s+([\d.]+)/.exec(value);
|
||||
return m ? parseFloat(m[1]) : null;
|
||||
}
|
||||
|
||||
let displayedHue = ACCENT_HUE;
|
||||
let rafId = 0;
|
||||
|
||||
$effect(() => {
|
||||
const targetHue = parseHue(player.accent) ?? ACCENT_HUE;
|
||||
const mode = themeMode.current;
|
||||
cancelAnimationFrame(rafId);
|
||||
const fromHue = displayedHue;
|
||||
let dh = targetHue - fromHue;
|
||||
if (dh > 180) dh -= 360;
|
||||
else if (dh < -180) dh += 360;
|
||||
const start = performance.now();
|
||||
const duration = 2000 + Math.random() * 2000;
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min(1, (now - start) / duration);
|
||||
const eased = 1 - Math.pow(1 - t, 3);
|
||||
const hue = (((fromHue + dh * eased) % 360) + 360) % 360;
|
||||
displayedHue = hue;
|
||||
applyThemeTokens(buildThemeTokens(hue, mode));
|
||||
if (t < 1) rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
rafId = requestAnimationFrame(tick);
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
});
|
||||
$effect(() => animateThemeHue(accent.hue, themeMode.current));
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onWindowKeydown} />
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { playlistCover } from '$lib/utils/hrefs';
|
||||
import { goto } from '$app/navigation';
|
||||
import History from '@lucide/svelte/icons/history';
|
||||
import Shuffle from '@lucide/svelte/icons/shuffle';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/actions';
|
||||
import type { Track } from '$lib/types';
|
||||
import { mergeRecentlyPlayed } from '$lib/data/recentlyPlayed';
|
||||
import { fetchPlaylistTracks } from '$lib/data/albums';
|
||||
@@ -165,9 +167,7 @@
|
||||
class="flex w-fit items-center gap-5 self-start sm:gap-6"
|
||||
>
|
||||
<Artwork
|
||||
src="/api/playlists/{spotlight.id}/cover?size=large&v={encodeURIComponent(
|
||||
spotlight.updatedAt
|
||||
)}"
|
||||
src={playlistCover(spotlight, 'large')}
|
||||
trackIds={spotlight.coverTrackIds}
|
||||
size="2xl"
|
||||
alt={spotlight.name}
|
||||
@@ -261,9 +261,7 @@
|
||||
variant="card"
|
||||
title={playlist.name}
|
||||
subtitle={playlist.description}
|
||||
coverSrc="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
)}"
|
||||
coverSrc={playlistCover(playlist, 'medium')}
|
||||
trackIds={playlist.coverTrackIds}
|
||||
style="--i:{i}"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user