Changes
This commit is contained in:
Vendored
+2
@@ -582,6 +582,8 @@ export interface components {
|
||||
/** Format: date-time */
|
||||
updatedAt: string;
|
||||
coverTrackIds: string[];
|
||||
youTubeAlbumId: null | string;
|
||||
thumbnailUrl: null | string;
|
||||
};
|
||||
CreateAlbumRequest: {
|
||||
title: string;
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
import { enhance } from '$app/forms';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||
import { hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
menu: AlbumMenuState;
|
||||
@@ -94,7 +93,6 @@
|
||||
<PlaylistArt
|
||||
playlistId={playlist.id}
|
||||
trackIds={[]}
|
||||
hue={hueFor(playlist.id)}
|
||||
size="small"
|
||||
class="h-9 w-9 shrink-0 rounded-control"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { isSectionActive } from '$lib/navigation.svelte';
|
||||
import Home from '@lucide/svelte/icons/house';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import Music from '@lucide/svelte/icons/music';
|
||||
@@ -13,7 +14,7 @@
|
||||
];
|
||||
|
||||
function isActive(href: string) {
|
||||
return href === '/' ? page.url.pathname === '/' : page.url.pathname.startsWith(href);
|
||||
return isSectionActive(href, page.url.pathname, page.data.section);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { isSectionActive } from '$lib/navigation.svelte';
|
||||
import type { SessionUser } from '$lib/types';
|
||||
import Home from '@lucide/svelte/icons/house';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
@@ -42,7 +43,7 @@
|
||||
];
|
||||
|
||||
function isActive(href: string) {
|
||||
return href === '/' ? page.url.pathname === '/' : page.url.pathname.startsWith(href);
|
||||
return isSectionActive(href, page.url.pathname, page.data.section);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||
import { hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
menu: ContextMenuState;
|
||||
@@ -105,7 +104,6 @@
|
||||
<PlaylistArt
|
||||
playlistId={playlist.id}
|
||||
trackIds={[]}
|
||||
hue={hueFor(playlist.id)}
|
||||
size="small"
|
||||
class="h-9 w-9 shrink-0 rounded-control"
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
type="button"
|
||||
onclick={seek}
|
||||
aria-label="Barra de progreso"
|
||||
class="group relative h-1.5 flex-1 cursor-pointer rounded-full bg-track transition-[height] hover:h-2"
|
||||
class="group relative h-1.5 flex-1 cursor-pointer rounded-full bg-track"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 rounded-full bg-accent/60 group-hover:brightness-110"
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
<Cover
|
||||
trackId={player.current.id}
|
||||
src={player.current.coverUrl}
|
||||
hue={player.current.hue}
|
||||
size="small"
|
||||
alt={player.current.title}
|
||||
class="h-11 w-11 shrink-0 rounded-lg shadow-[0_6px_16px_-6px_rgba(0,0,0,0.6)] md:h-13 md:w-13"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="h-11 w-11 shrink-0 rounded-lg shadow-[0_6px_16px_-6px_rgba(0,0,0,0.6)] md:h-13 md:w-13"
|
||||
style="background:{player.artGradient}"
|
||||
class="h-11 w-11 shrink-0 rounded-lg bg-surface shadow-[0_6px_16px_-6px_rgba(0,0,0,0.6)] md:h-13 md:w-13"
|
||||
></div>
|
||||
{/if}
|
||||
<div class="min-w-0">
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import MediaCard from './MediaCard.svelte';
|
||||
import PlaylistArt from './PlaylistArt.svelte';
|
||||
import Cover from './Cover.svelte';
|
||||
import { hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -55,16 +54,9 @@
|
||||
>
|
||||
{#snippet art(artClass)}
|
||||
{#if coverSrc}
|
||||
<Cover
|
||||
trackId={id}
|
||||
src={coverSrc}
|
||||
hue={hueFor(id)}
|
||||
size="large"
|
||||
alt={title}
|
||||
class={artClass}
|
||||
/>
|
||||
<Cover trackId={id} src={coverSrc} size="large" alt={title} class={artClass} />
|
||||
{:else}
|
||||
<PlaylistArt {trackIds} hue={hueFor(id)} class={artClass} />
|
||||
<PlaylistArt {trackIds} class={artClass} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</MediaCard>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { gradientForHue, hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
let { id, name, size = 108 }: Props = $props();
|
||||
let { name, size = 108 }: Props = $props();
|
||||
|
||||
const initials = $derived(
|
||||
name
|
||||
@@ -21,8 +18,8 @@
|
||||
|
||||
<div class="flex flex-col items-center gap-2.5 text-center" style="width:{size}px">
|
||||
<div
|
||||
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/artist:shadow-art-lg"
|
||||
style="width:{size}px;height:{size}px;background:{gradientForHue(hueFor(id))}"
|
||||
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full bg-surface shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/artist:shadow-art-lg"
|
||||
style="width:{size}px;height:{size}px"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import { previousPage } from '$lib/navigation.svelte';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
let { href, label }: Props = $props();
|
||||
|
||||
const previous = $derived(previousPage());
|
||||
const target = $derived(previous ?? { url: href, label });
|
||||
|
||||
function onclick(event: MouseEvent) {
|
||||
if (!previous) return;
|
||||
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
||||
return;
|
||||
event.preventDefault();
|
||||
history.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={target.url}
|
||||
{onclick}
|
||||
class="group inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
|
||||
>
|
||||
<span class="grid h-8 w-8 place-items-center transition-colors group-hover:text-accent-soft">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</span>
|
||||
{target.label}
|
||||
</a>
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { gradientForHue } from '$lib/theme/color';
|
||||
import { thumbnailSrc } from '$lib/thumbnails';
|
||||
|
||||
interface Props {
|
||||
trackId: string | number;
|
||||
hue: number;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
@@ -16,7 +14,6 @@
|
||||
|
||||
let {
|
||||
trackId,
|
||||
hue,
|
||||
size = 'medium',
|
||||
src,
|
||||
alt = '',
|
||||
@@ -26,7 +23,6 @@
|
||||
}: Props = $props();
|
||||
|
||||
let failed = $state(false);
|
||||
let loaded = $state(false);
|
||||
|
||||
const imageSrc = $derived(
|
||||
src ? (thumbnailSrc(src, size) ?? src) : `/api/tracks/${trackId}/cover?size=${size}`
|
||||
@@ -35,16 +31,10 @@
|
||||
$effect(() => {
|
||||
imageSrc;
|
||||
failed = false;
|
||||
loaded = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative overflow-hidden {klass}"
|
||||
style={loaded && !failed ? undefined : `background:${gradientForHue(hue)}`}
|
||||
role="presentation"
|
||||
{oncontextmenu}
|
||||
>
|
||||
<div class="relative overflow-hidden bg-surface {klass}" role="presentation" {oncontextmenu}>
|
||||
{#if !failed}
|
||||
<img
|
||||
src={imageSrc}
|
||||
@@ -52,7 +42,6 @@
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
fetchpriority="low"
|
||||
onload={() => (loaded = true)}
|
||||
onerror={() => (failed = true)}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script lang="ts">
|
||||
import Cover from './Cover.svelte';
|
||||
import { gradientForHue, hueFor } from '$lib/theme/color';
|
||||
import { mixItemKey, type MixItem } from '$lib/mixes';
|
||||
|
||||
interface Props {
|
||||
items: MixItem[];
|
||||
hue: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { items, hue, class: klass = '' }: Props = $props();
|
||||
let { items, class: klass = '' }: Props = $props();
|
||||
|
||||
const tiles = $derived(items.slice(0, 4));
|
||||
</script>
|
||||
@@ -20,7 +18,6 @@
|
||||
<Cover
|
||||
trackId={item.trackId ?? item.videoId ?? ''}
|
||||
src={item.thumbnailUrl}
|
||||
hue={hueFor(mixItemKey(item))}
|
||||
size="small"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
@@ -30,10 +27,9 @@
|
||||
<Cover
|
||||
trackId={tiles[0].trackId ?? tiles[0].videoId ?? ''}
|
||||
src={tiles[0].thumbnailUrl}
|
||||
hue={hueFor(mixItemKey(tiles[0]))}
|
||||
size="medium"
|
||||
class={klass}
|
||||
/>
|
||||
{:else}
|
||||
<div class={klass} style="background:{gradientForHue(hue)}"></div>
|
||||
<div class="bg-surface {klass}"></div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Cover from './Cover.svelte';
|
||||
import { gradientForHue, hueFor } from '$lib/theme/color';
|
||||
import { mixItemKey, type Mix } from '$lib/mixes';
|
||||
import type { Mix } from '$lib/mixes';
|
||||
|
||||
interface Props {
|
||||
mix: Mix;
|
||||
@@ -25,40 +24,40 @@
|
||||
|
||||
<a
|
||||
href="/mixes/{mix.id}"
|
||||
class="group/tile animate-enter relative block overflow-hidden rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset hover:shadow-art-lg focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none {klass}"
|
||||
style="animation-delay:{Math.min(index, 10) * 45}ms;background:{gradientForHue(hueFor(mix.id))}"
|
||||
class="group/tile animate-enter relative block overflow-hidden rounded-art ring-1 ring-line transition duration-300 ease-out [clip-path:inset(0_round_var(--radius-art))] ring-inset focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none {klass}"
|
||||
style="animation-delay:{Math.min(index, 10) * 45}ms"
|
||||
>
|
||||
{#if tiles.length > 0}
|
||||
{#if tiles.length > 0}
|
||||
<div class="absolute inset-0 flex transition duration-300 ease-out">
|
||||
{#each tiles as item, i (i)}
|
||||
<Cover
|
||||
trackId={item.trackId ?? item.videoId ?? ''}
|
||||
src={item.thumbnailUrl}
|
||||
size="medium"
|
||||
class="aspect-square h-full shrink-0"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="absolute inset-0 flex transition duration-300 ease-out group-hover/tile:scale-[1.01]"
|
||||
>
|
||||
{#each tiles as item, i (i)}
|
||||
<Cover
|
||||
trackId={item.trackId ?? item.videoId ?? ''}
|
||||
src={item.thumbnailUrl}
|
||||
hue={hueFor(mixItemKey(item))}
|
||||
size="medium"
|
||||
class="aspect-square h-full shrink-0"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-black/10 transition-colors duration-300 group-hover/tile:bg-black/20"
|
||||
></div>
|
||||
class="pointer-events-none absolute inset-0 bg-linear-to-t from-scrim/85 via-scrim/65 to-scrim/40 {banner
|
||||
? 'sm:bg-linear-to-r'
|
||||
: ''}"
|
||||
></div>
|
||||
<div
|
||||
class="group relative flex h-full flex-col justify-end p-3 sm:p-4 {banner
|
||||
class="pointer-events-none absolute inset-0 bg-scrim/25 opacity-0 transition-opacity duration-300 group-hover/tile:opacity-100"
|
||||
></div>
|
||||
<div
|
||||
class="relative flex h-full flex-col justify-end p-3 sm:p-4 {banner
|
||||
? 'sm:flex-row sm:items-center sm:justify-between sm:gap-4'
|
||||
: ''}"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate text-white {featured ? 'text-xl sm:text-2xl' : 'text-base'} group-hover:text-accent-soft transition-colors"
|
||||
>
|
||||
<div class="truncate text-on-art {featured ? 'text-xl sm:text-2xl' : 'text-base'}">
|
||||
{mix.title}
|
||||
</div>
|
||||
{#if mix.subtitle}
|
||||
<div class="mt-0.5 truncate text-xs text-muted sm:text-sm">{mix.subtitle}</div>
|
||||
<div class="mt-0.5 truncate text-xs text-on-art/75 sm:text-sm">{mix.subtitle}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script lang="ts">
|
||||
import Cover from './Cover.svelte';
|
||||
import { gradientForHue, hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
playlistId?: string;
|
||||
trackIds: (string | number)[];
|
||||
hue: number;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
version?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { playlistId, trackIds, hue, size = 'medium', version, class: klass = '' }: Props = $props();
|
||||
let { playlistId, trackIds, size = 'medium', version, class: klass = '' }: Props = $props();
|
||||
|
||||
let coverFailed = $state(false);
|
||||
|
||||
@@ -20,7 +18,7 @@
|
||||
</script>
|
||||
|
||||
{#if playlistId && !coverFailed}
|
||||
<div class="relative overflow-hidden {klass}" style="background:{gradientForHue(hue)}">
|
||||
<div class="relative overflow-hidden bg-surface {klass}">
|
||||
<img
|
||||
src="/api/playlists/{playlistId}/cover?size={size}{version
|
||||
? `&v=${encodeURIComponent(version)}`
|
||||
@@ -34,11 +32,11 @@
|
||||
{:else if mosaic}
|
||||
<div class="grid grid-cols-2 grid-rows-2 overflow-hidden {klass}">
|
||||
{#each trackIds.slice(0, 4) as id (id)}
|
||||
<Cover trackId={id} hue={hueFor(id)} size="small" class="h-full w-full" />
|
||||
<Cover trackId={id} size="small" class="h-full w-full" />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if single !== null}
|
||||
<Cover trackId={single} hue={hueFor(single)} size="medium" class={klass} />
|
||||
<Cover trackId={single} size="medium" class={klass} />
|
||||
{:else}
|
||||
<div class={klass} style="background:{gradientForHue(hue)}"></div>
|
||||
<div class="bg-surface {klass}"></div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import MediaCard from './MediaCard.svelte';
|
||||
import PlaylistArt from './PlaylistArt.svelte';
|
||||
import { hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -17,6 +16,6 @@
|
||||
|
||||
<MediaCard href="/playlists/{id}" title={name} subtitle={description} {index}>
|
||||
{#snippet art(artClass)}
|
||||
<PlaylistArt playlistId={id} {trackIds} hue={hueFor(id)} version={updatedAt} class={artClass} />
|
||||
<PlaylistArt playlistId={id} {trackIds} version={updatedAt} class={artClass} />
|
||||
{/snippet}
|
||||
</MediaCard>
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
{@render actions()}
|
||||
</div>
|
||||
{:else}
|
||||
<h2
|
||||
class="mb-3 font-display font-medium text-fg sm:mb-4 sm:text-xl {klass}"
|
||||
>
|
||||
<h2 class="mb-3 font-display font-medium text-fg sm:mb-4 sm:text-xl {klass}">
|
||||
{title}
|
||||
</h2>
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
artist?: string | null;
|
||||
coverSrc?: string | null;
|
||||
coverSize?: string;
|
||||
hue: number;
|
||||
explicit?: boolean;
|
||||
titleClass?: string;
|
||||
onClick: () => void;
|
||||
@@ -23,7 +22,6 @@
|
||||
artist,
|
||||
coverSrc,
|
||||
coverSize = 'h-[42px] w-[42px]',
|
||||
hue,
|
||||
explicit = false,
|
||||
titleClass = '',
|
||||
onClick,
|
||||
@@ -36,7 +34,6 @@
|
||||
<Cover
|
||||
{trackId}
|
||||
src={coverSrc}
|
||||
{hue}
|
||||
size="small"
|
||||
alt={title}
|
||||
class="{coverSize} shrink-0 rounded-control"
|
||||
|
||||
@@ -7,6 +7,7 @@ export const ALBUM_TRACKS_PAGE_SIZE = 200;
|
||||
export const ALBUM_EARLIEST_YEAR = 1877;
|
||||
export const LIBRARY_PICKER_PAGE_SIZE = 50;
|
||||
export const EXPLORE_ALBUMS_PAGE_SIZE = 10;
|
||||
export const EXPLORE_ALBUMS_LIMIT = 18;
|
||||
export const HOME_LATEST_PAGE_SIZE = 10;
|
||||
export const HOME_ALBUMS_LIMIT = 12;
|
||||
export const HOME_MIXES_BENTO = 5;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
|
||||
const HISTORY_INDEX = 'sveltekit:history';
|
||||
|
||||
const LABELS: [RegExp, string][] = [
|
||||
[/^\/$/, 'Volver al inicio'],
|
||||
[/^\/explore\/?$/, 'Volver a explorar'],
|
||||
[/^\/playlists\/[^/]+/, 'Volver a la lista'],
|
||||
[/^\/playlists\/?$/, 'Volver a tus listas'],
|
||||
[/^\/albums\/youtube\/[^/]+/, 'Volver al álbum'],
|
||||
[/^\/albums\/[^/]+/, 'Volver al álbum'],
|
||||
[/^\/albums\/?$/, 'Volver a tus álbumes'],
|
||||
[/^\/library\/?$/, 'Volver a tus canciones'],
|
||||
[/^\/upload\/?$/, 'Volver a subir música'],
|
||||
[/^\/mixes\/[^/]+/, 'Volver a la mezcla']
|
||||
];
|
||||
|
||||
export interface PageRef {
|
||||
url: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const visited = new Map<number, PageRef>();
|
||||
let index = $state(-1);
|
||||
|
||||
function labelFor(pathname: string): string | null {
|
||||
for (const [pattern, label] of LABELS) {
|
||||
if (pattern.test(pathname)) return label;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function historyIndex(): number | null {
|
||||
const value = history.state?.[HISTORY_INDEX];
|
||||
return typeof value === 'number' ? value : null;
|
||||
}
|
||||
|
||||
export function trackNavigation() {
|
||||
afterNavigate((nav) => {
|
||||
const at = historyIndex();
|
||||
if (at === null) return;
|
||||
const url = nav.to?.url;
|
||||
const label = url ? labelFor(url.pathname) : null;
|
||||
if (url && label) visited.set(at, { url: url.pathname + url.search, label });
|
||||
else visited.delete(at);
|
||||
index = at;
|
||||
});
|
||||
}
|
||||
|
||||
export function previousPage(): PageRef | null {
|
||||
return visited.get(index - 1) ?? null;
|
||||
}
|
||||
|
||||
export function isSectionActive(href: string, pathname: string, section?: string | null): boolean {
|
||||
if (section !== undefined) return section === href;
|
||||
return href === '/' ? pathname === '/' : pathname.startsWith(href);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { HUES, accentForHue, gradientForHue, hueFor } from '$lib/theme/color';
|
||||
import { DEFAULT_ACCENT } from '$lib/theme/color';
|
||||
import { extractAccent, type Accent } from '$lib/theme/palette';
|
||||
import { thumbnailSrc } from '$lib/thumbnails';
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface PlayerTrack {
|
||||
title: string;
|
||||
artist: string;
|
||||
duration: number;
|
||||
hue: number;
|
||||
source: TrackSourceKind;
|
||||
coverUrl?: string;
|
||||
explicit?: boolean;
|
||||
@@ -91,7 +90,6 @@ const EMPTY: PlayerTrack = {
|
||||
title: '',
|
||||
artist: '',
|
||||
duration: 0,
|
||||
hue: HUES[0],
|
||||
source: 'local'
|
||||
};
|
||||
|
||||
@@ -101,7 +99,6 @@ function toTrack(item: QueueItem): PlayerTrack {
|
||||
title: item.title,
|
||||
artist: item.artist ?? '',
|
||||
duration: 0,
|
||||
hue: hueFor(item.id),
|
||||
explicit: item.explicit,
|
||||
source: item.source ?? 'local',
|
||||
coverUrl: item.coverUrl
|
||||
@@ -120,7 +117,6 @@ class PlayerState {
|
||||
playlists = $state<Playlist[]>([]);
|
||||
|
||||
accentColor = $state<string | null>(null);
|
||||
gradientColor = $state<string | null>(null);
|
||||
|
||||
#audio: HTMLAudioElement | null = null;
|
||||
#loadToken = 0;
|
||||
@@ -129,8 +125,7 @@ class PlayerState {
|
||||
#rafId: number | null = null;
|
||||
|
||||
current = $derived(this.tracks.find((t) => t.id === this.currentId) ?? EMPTY);
|
||||
accent = $derived(this.accentColor ?? accentForHue(this.current.hue));
|
||||
artGradient = $derived(this.gradientColor ?? gradientForHue(this.current.hue));
|
||||
accent = $derived(this.accentColor ?? DEFAULT_ACCENT);
|
||||
progressPercent = $derived(
|
||||
this.current.duration > 0 ? Math.min(100, (this.progress / this.current.duration) * 100) : 0
|
||||
);
|
||||
@@ -250,7 +245,6 @@ class PlayerState {
|
||||
const cached = this.#accentCache.get(key);
|
||||
if (cached) {
|
||||
this.accentColor = cached.accent;
|
||||
this.gradientColor = cached.gradient;
|
||||
return;
|
||||
}
|
||||
const result = await extractAccent(
|
||||
@@ -260,10 +254,8 @@ class PlayerState {
|
||||
if (result) {
|
||||
this.#accentCache.set(key, result);
|
||||
this.accentColor = result.accent;
|
||||
this.gradientColor = result.gradient;
|
||||
} else {
|
||||
this.accentColor = null;
|
||||
this.gradientColor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,14 +327,6 @@ class PlayerState {
|
||||
.filter((t): t is PlayerTrack => Boolean(t));
|
||||
}
|
||||
|
||||
mosaicFor(ids: (string | number)[]): string[] {
|
||||
const gradients = this.playlistTracks(ids)
|
||||
.slice(0, 4)
|
||||
.map((t) => gradientForHue(t.hue));
|
||||
while (gradients.length < 4) gradients.push('var(--mf-surface)');
|
||||
return gradients;
|
||||
}
|
||||
|
||||
playQueue(list: QueueItem[], startIndex = 0) {
|
||||
if (list.length === 0) return;
|
||||
this.tracks = list.map(toTrack);
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
export const HUES = [145, 330, 40, 152, 300, 200, 10, 120, 280];
|
||||
|
||||
export const ACCENT_LIGHTNESS = 60;
|
||||
export const ACCENT_CHROMA = 0.17;
|
||||
export const ACCENT_HUE = 145;
|
||||
|
||||
export function hueFor(id: string | number): number {
|
||||
const text = String(id);
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; i++) hash = (hash * 31 + text.charCodeAt(i)) >>> 0;
|
||||
return HUES[hash % HUES.length];
|
||||
}
|
||||
|
||||
export function accentForHue(hue: number): string {
|
||||
return `oklch(${ACCENT_LIGHTNESS}% ${ACCENT_CHROMA} ${hue})`;
|
||||
}
|
||||
|
||||
export function gradientForHue(hue: number): string {
|
||||
return `linear-gradient(135deg, oklch(70% 0.15 ${hue}), oklch(36% 0.13 ${hue + 28}))`;
|
||||
}
|
||||
export const DEFAULT_ACCENT = `oklch(${ACCENT_LIGHTNESS}% ${ACCENT_CHROMA} ${ACCENT_HUE})`;
|
||||
|
||||
Reference in New Issue
Block a user