92 lines
2.9 KiB
Svelte
92 lines
2.9 KiB
Svelte
<script lang="ts">
|
|
import { GripVertical, X } from 'lucide-svelte';
|
|
import { draggable, type DragEventData } from '@neodrag/svelte';
|
|
import type { TrackRef } from '$lib/api/types';
|
|
import { playFromQueueIndex, removeFromQueue, moveQueueItem } from '$lib/player/store.svelte';
|
|
import { offsetToDelta } from './queue-row-math';
|
|
|
|
let { track, index, isCurrent } = $props<{
|
|
track: TrackRef;
|
|
index: number;
|
|
isCurrent: boolean;
|
|
}>();
|
|
|
|
let measuredRowHeight = 64; // sensible default; replaced on dragStart
|
|
|
|
function handleDragStart(data: DragEventData) {
|
|
// The action's `data.rootNode` is the element use:draggable was applied to.
|
|
const rect = data.rootNode.getBoundingClientRect();
|
|
if (rect.height > 0) measuredRowHeight = rect.height;
|
|
}
|
|
|
|
function handleBodyClick() {
|
|
if (isCurrent) return;
|
|
playFromQueueIndex(index);
|
|
}
|
|
|
|
function handleRemove() {
|
|
removeFromQueue(index);
|
|
}
|
|
|
|
function handleDragEnd(data: DragEventData) {
|
|
const delta = offsetToDelta(data.offsetY, measuredRowHeight);
|
|
if (delta === 0) return;
|
|
moveQueueItem(index, index + delta);
|
|
}
|
|
|
|
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:draggable={{ axis: 'y', bounds: 'parent', onDragStart: handleDragStart, onDragEnd: handleDragEnd }}
|
|
class="flex items-center gap-2 border-b border-border px-3 py-2 h-16
|
|
{isCurrent ? 'border-l-2 border-l-accent bg-surface-hover' : ''}"
|
|
>
|
|
<button
|
|
type="button"
|
|
aria-label="Reorder track (use arrow keys)"
|
|
aria-keyshortcuts="ArrowUp ArrowDown"
|
|
onkeydown={handleHandleKeydown}
|
|
class="cursor-grab text-text-secondary hover:text-text-primary flex-shrink-0"
|
|
>
|
|
<GripVertical size={16} />
|
|
</button>
|
|
|
|
<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 uppercase tracking-wide flex-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>
|
|
|
|
<button
|
|
type="button"
|
|
onclick={handleRemove}
|
|
aria-label="Remove from queue"
|
|
class="text-text-secondary hover:text-text-primary flex-shrink-0"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|