This commit is contained in:
2026-07-25 01:46:59 +02:00
parent a58464d523
commit f54ebec0eb
66 changed files with 3245 additions and 416 deletions
@@ -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 -12
View File
@@ -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"