Files
minstrel/web/src/lib/components/HistoryRow.svelte
T
bvandeusenandClaude Opus 5.5 918db27fec
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
feat(web): shared collection header, Play and Shuffle, and the centred page column (M494)
- 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>
2026-10-08 14:18:44 -04:00

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>