feat(web): shared collection header, Play and Shuffle, and the centred page column (M494)
release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m48s
release / integration (push) Successful in 5m51s
release / android (push) Successful in 6m53s
release / Build signed APK (releases and dev) (push) Successful in 7m14s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m29s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m48s
release / integration (push) Successful in 5m51s
release / android (push) Successful in 6m53s
release / Build signed APK (releases and dev) (push) Successful in 7m14s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m29s
release / Verify release artifacts (tag releases only) (push) Skipped
- CollectionHeader holds cover, title, facts, then Play and Shuffle, for album and playlist. The playlist page gains Play and Shuffle; it had neither. The cover uses the shared Cover component, so the playlist header also gets the fallback image. - PlayActions is the one Play and Shuffle pair: filled Play, outlined Shuffle, on album, playlist and artist pages (operator, 2026-10-08). The artist page's round icon button said Play but shuffled; Play now keeps the server's order, and Shuffle shuffles. - PageColumn is the playlist page's centred column, now used by album, playlist, Liked, History and Search → Tracks. - The playlist page builds its queue with playlistTrackToRef, the shared converter, in place of its own copy. Its copy skipped only removed tracks, so a missing file (#2527) could still be queued from there. - shuffled() in lib/utils is the one shuffle, replacing three private copies (album page, artist page, ArtistCard). - The album's refetch error now uses text-error-fg. `text-oxblood` is not a colour in the theme, so it never rendered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { shuffled } from '#lib/utils/shuffle.js';
|
||||
import type { ArtistRef, TrackRef } from '#lib/api/types.js';
|
||||
import { api } from '#lib/api/client.js';
|
||||
import { playQueue, enqueueTracks } from '#lib/player/store.svelte.js';
|
||||
@@ -12,21 +13,12 @@
|
||||
|
||||
let queueing = $state(false);
|
||||
|
||||
function shuffle<T>(items: T[]): T[] {
|
||||
const arr = items.slice();
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
async function onPlayClick(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const tracks = await api.get<TrackRef[]>(`/api/artists/${artist.id}/tracks`);
|
||||
if (tracks.length === 0) return;
|
||||
playQueue(shuffle(tracks), 0);
|
||||
playQueue(shuffled(tracks), 0);
|
||||
}
|
||||
|
||||
async function onAddToQueue(e: MouseEvent) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
// The header of every page that is one collection of tracks, album and
|
||||
// playlist (milestone 494): cover, title, a line or two of facts, then
|
||||
// Play and Shuffle (PlayActions).
|
||||
import type { Snippet } from 'svelte';
|
||||
import PlayActions from './PlayActions.svelte';
|
||||
import Cover from './Cover.svelte';
|
||||
|
||||
let {
|
||||
title,
|
||||
coverUrl,
|
||||
coverAlt = '',
|
||||
playDisabled = false,
|
||||
onPlay,
|
||||
onShuffle,
|
||||
titleExtra,
|
||||
tools,
|
||||
children,
|
||||
actions
|
||||
}: {
|
||||
title: string;
|
||||
coverUrl: string | null | undefined;
|
||||
coverAlt?: string;
|
||||
playDisabled?: boolean;
|
||||
onPlay: () => void;
|
||||
onShuffle: () => void;
|
||||
/** Beside the title, e.g. the album's like button. */
|
||||
titleExtra?: Snippet;
|
||||
/** Top right: refresh, link, edit, delete. */
|
||||
tools?: Snippet;
|
||||
/** The facts under the title. */
|
||||
children?: Snippet;
|
||||
/** After Play and Shuffle, e.g. an admin's Refetch cover. */
|
||||
actions?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<header class="mb-6 flex items-start gap-4">
|
||||
<div class="h-32 w-32 shrink-0 overflow-hidden rounded-md md:h-40 md:w-40">
|
||||
<Cover src={coverUrl ?? undefined} alt={coverAlt} />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<div class="flex items-start gap-3">
|
||||
<h1 class="min-w-0 truncate font-display text-2xl font-medium text-text-primary">{title}</h1>
|
||||
{#if titleExtra}{@render titleExtra()}{/if}
|
||||
{#if tools}
|
||||
<div class="ml-auto flex shrink-0 items-center gap-1">{@render tools()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if children}{@render children()}{/if}
|
||||
|
||||
<div class="pt-1">
|
||||
<PlayActions name={title} disabled={playDisabled} {onPlay} {onShuffle}>
|
||||
{#if actions}{@render actions()}{/if}
|
||||
</PlayActions>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -3,6 +3,7 @@
|
||||
import { playQueue } from '#lib/player/store.svelte.js';
|
||||
import { coverUrl } from '#lib/media/covers.js';
|
||||
import TrackListRow from './TrackListRow.svelte';
|
||||
import Cover from './Cover.svelte';
|
||||
|
||||
let { event } = $props<{ event: HistoryEvent }>();
|
||||
|
||||
@@ -50,12 +51,9 @@
|
||||
onActivate={handleClick}
|
||||
>
|
||||
{#snippet lead()}
|
||||
<img
|
||||
src={coverUrl(event.track.album_id)}
|
||||
alt=""
|
||||
class="h-8 w-8 rounded object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="block h-8 w-8 overflow-hidden rounded">
|
||||
<Cover src={coverUrl(event.track.album_id)} />
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet trailing()}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
// The centred column every track-list page sits in: album, playlist,
|
||||
// Liked, History and Search → Tracks (milestone 494). The playlist page
|
||||
// had it alone from May 2026; the operator picked it as the page shape
|
||||
// on 2026-10-08. Shelf and grid pages (Home, Library, Discover, Artist,
|
||||
// Search) stay full width, since a narrow column would cost them cards.
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="mx-auto w-full max-w-4xl space-y-4 px-4 py-6">
|
||||
{@render children()}
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
// Play and Shuffle, the same on every page that has them: album,
|
||||
// playlist and artist (milestone 494). Play is filled and Shuffle
|
||||
// outlined. The fill is the accent: the operator's call on 2026-10-08,
|
||||
// made with the house rule against accent on action buttons stated to
|
||||
// them.
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Play, Shuffle } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
name,
|
||||
disabled = false,
|
||||
onPlay,
|
||||
onShuffle,
|
||||
children
|
||||
}: {
|
||||
/** What is played, for the buttons' accessible names. */
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
onPlay: () => void;
|
||||
onShuffle: () => void;
|
||||
/** Further actions after Shuffle. */
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Play ${name}`}
|
||||
onclick={onPlay}
|
||||
{disabled}
|
||||
class="flex items-center gap-2 rounded-md bg-accent px-4 py-2 text-sm font-medium
|
||||
text-text-primary hover:opacity-90 focus-visible:ring-2 focus-visible:ring-accent
|
||||
disabled:opacity-50"
|
||||
>
|
||||
<Play size={16} strokeWidth={1.5} fill="currentColor" />
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Shuffle ${name}`}
|
||||
onclick={onShuffle}
|
||||
{disabled}
|
||||
class="flex items-center gap-2 rounded-md border border-border bg-surface px-4 py-2
|
||||
text-sm font-medium text-text-primary hover:border-accent
|
||||
focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-50"
|
||||
>
|
||||
<Shuffle size={16} strokeWidth={1.5} />
|
||||
Shuffle
|
||||
</button>
|
||||
{#if children}{@render children()}{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user