release / govulncheck (push) Successful in 34s
release / web (push) Failing after 1m20s
release / go (push) Successful in 1m37s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 4m38s
release / android (push) Canceled after 4m39s
release / Build signed APK (releases and dev) (push) Canceled after 3m49s
Every track list (album, Liked, Search, Search → Tracks, playlist, History) now draws its rows with TrackListRow inside a TrackList box, in place of four row components that had drifted apart: - TrackListRow owns the look: lead column, title over "artist · album", like, radio, add to queue, menu, duration, then a trailing slot. It also owns the stripe, hover, the now-playing band and the unavailable state. The stripe is dropped on now-playing and selected rows, where Tailwind would otherwise let `odd:` win. - TrackRow, PlaylistTrackRow and HistoryRow are wrappers that keep only their behaviour: track number and multi-select; grip, remove and dead-entry states; cover and timestamp. Playlist and History rows gain radio, add and the menu, so every list offers the same actions. - The radio control is Lucide's Radio icon, not the 📻 emoji. - Enter on a control inside a row now belongs to that control. The old TrackRow played the track when Enter was pressed on "add to queue". - TrackList has no fill, so the stripe shows on playlists too. Drag reorder (#5393): neodrag kept the drop offset, so after the list re-rendered the dragged row sat between two slots. reorderDrag, shared by the playlist and queue rows: - snaps the row to whole rows while dragging; - resets it to its slot on drop; - on playlists, gives the touch gesture to the grip only, so the list still scrolls by touch. The playlist page reorders optimistically and rolls back on error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
4.0 KiB
Svelte
108 lines
4.0 KiB
Svelte
<script lang="ts">
|
|
import { GripVertical, X } from 'lucide-svelte';
|
|
import type { TrackRef } from '#lib/api/types.js';
|
|
import { playFromQueueIndex, removeFromQueue, moveQueueItem } from '#lib/player/store.svelte.js';
|
|
import { coverUrl, FALLBACK_COVER } from '#lib/media/covers.js';
|
|
import { reorderDrag } from './reorder-drag';
|
|
import LikeButton from './LikeButton.svelte';
|
|
|
|
let { track, index, isCurrent } = $props<{
|
|
track: TrackRef;
|
|
index: number;
|
|
isCurrent: boolean;
|
|
}>();
|
|
|
|
function handleBodyClick() {
|
|
if (isCurrent) return;
|
|
playFromQueueIndex(index);
|
|
}
|
|
|
|
function handleRemove() {
|
|
removeFromQueue(index);
|
|
}
|
|
|
|
function handleHandleKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
moveQueueItem(index, index - 1);
|
|
} else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
moveQueueItem(index, index + 1);
|
|
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
// Reorder activates via arrow keys, not Space/Enter. Prevent
|
|
// default so Space doesn't scroll the queue list and Enter doesn't
|
|
// fire the button's no-op click.
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
use:reorderDrag={{ onMove: (delta) => moveQueueItem(index, index + delta) }}
|
|
class="flex items-center gap-2 border-b border-border px-3 py-2 h-16
|
|
[&.neodrag-dragging]:relative [&.neodrag-dragging]:z-10 [&.neodrag-dragging]:shadow-lg
|
|
{isCurrent ? 'border-l-2 border-l-accent bg-surface-hover' : 'bg-background'}"
|
|
>
|
|
<!--
|
|
The album art is the grab surface (#2395). The grip used to occupy its own
|
|
column in every row; it now sits OVER the art, so it costs no horizontal
|
|
space at all. `use:reorderDrag` is on the row (above), so dragging already
|
|
worked from anywhere — the grip's real jobs are being the visual cue and
|
|
the keyboard target, and both survive here.
|
|
|
|
It stays VISIBLE at rest, just quiet — it is the only thing that says this
|
|
list can be reordered at all, so hiding it until hover would trade the
|
|
operator's space complaint for a discoverability one (rule #24), and would
|
|
leave nothing for touch, which has no hover. The scrim only appears on
|
|
hover/focus so the artwork stays legible the rest of the time; the drop
|
|
shadow is what keeps the glyph readable over pale covers without one.
|
|
-->
|
|
<div class="relative h-10 w-10 shrink-0">
|
|
<img
|
|
src={coverUrl(track.album_id)}
|
|
alt=""
|
|
onerror={(e) => ((e.currentTarget as HTMLImageElement).src = FALLBACK_COVER)}
|
|
class="h-10 w-10 rounded object-cover"
|
|
/>
|
|
<button
|
|
type="button"
|
|
aria-label="Reorder track (use arrow keys)"
|
|
aria-keyshortcuts="ArrowUp ArrowDown"
|
|
onkeydown={handleHandleKeydown}
|
|
class="group absolute inset-0 flex cursor-grab items-center justify-center rounded
|
|
text-white/70 transition hover:bg-black/45 hover:text-white
|
|
focus-visible:bg-black/45 focus-visible:text-white
|
|
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
|
|
style="filter: drop-shadow(0 1px 1px rgb(0 0 0 / 0.9))"
|
|
>
|
|
<GripVertical size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onclick={handleBodyClick}
|
|
class="flex-1 text-left min-w-0 {isCurrent ? 'cursor-default' : 'cursor-pointer'}"
|
|
aria-label={isCurrent ? `Now playing — ${track.title}` : `Play ${track.title}`}
|
|
>
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
{#if isCurrent}
|
|
<span class="text-xs text-accent-fg uppercase tracking-wide shrink-0">Now playing</span>
|
|
{/if}
|
|
<span class="font-medium truncate text-text-primary">{track.title}</span>
|
|
</div>
|
|
<div class="text-xs text-text-secondary truncate">{track.artist_name}</div>
|
|
</button>
|
|
|
|
<LikeButton entityType="track" entityId={track.id} />
|
|
|
|
<button
|
|
type="button"
|
|
onclick={handleRemove}
|
|
aria-label="Remove from queue"
|
|
class="text-text-secondary hover:text-text-primary shrink-0"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|