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>
65 lines
2.2 KiB
Svelte
65 lines
2.2 KiB
Svelte
<script lang="ts">
|
|
import type { HistoryEvent } from '#lib/api/history.js';
|
|
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 }>();
|
|
|
|
function handleClick() {
|
|
playQueue([event.track], 0);
|
|
}
|
|
|
|
// Format played_at as a human-readable timestamp.
|
|
// Bands: <1h "23m ago" / <24h "3h ago" / <7d "Tue 14:32" /
|
|
// older same-year "May 1" / older diff-year "May 1, 2025".
|
|
function relativeTime(playedAtIso: string): string {
|
|
const playedAt = new Date(playedAtIso);
|
|
const now = new Date();
|
|
const ms = now.getTime() - playedAt.getTime();
|
|
const minutes = Math.floor(ms / 60000);
|
|
if (minutes < 60) return `${Math.max(1, minutes)}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
const days = Math.floor(hours / 24);
|
|
if (days < 7) {
|
|
const dayOfWeek = playedAt.toLocaleDateString(undefined, { weekday: 'short' });
|
|
const time = playedAt.toLocaleTimeString(undefined, {
|
|
hour: '2-digit', minute: '2-digit', hour12: false
|
|
});
|
|
return `${dayOfWeek} ${time}`;
|
|
}
|
|
if (playedAt.getFullYear() === now.getFullYear()) {
|
|
return playedAt.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
}
|
|
return playedAt.toLocaleDateString(undefined, {
|
|
month: 'short', day: 'numeric', year: 'numeric'
|
|
});
|
|
}
|
|
|
|
const timestamp = $derived(relativeTime(event.played_at));
|
|
const tooltip = $derived(new Date(event.played_at).toISOString());
|
|
</script>
|
|
|
|
<TrackListRow
|
|
title={event.track.title}
|
|
subtitle={`${event.track.artist_name} · ${event.track.album_title}`}
|
|
durationSec={event.track.duration_sec}
|
|
track={event.track}
|
|
ariaLabel={`Play ${event.track.title}`}
|
|
onActivate={handleClick}
|
|
>
|
|
{#snippet lead()}
|
|
<span class="block h-8 w-8 overflow-hidden rounded">
|
|
<Cover src={coverUrl(event.track.album_id)} />
|
|
</span>
|
|
{/snippet}
|
|
|
|
{#snippet trailing()}
|
|
<span title={tooltip} class="w-16 shrink-0 text-right text-xs text-text-secondary">
|
|
{timestamp}
|
|
</span>
|
|
{/snippet}
|
|
</TrackListRow>
|