drag and drop init

This commit is contained in:
2026-02-10 01:55:49 -08:00
parent 9f0234e00e
commit ed9fcbe116
2 changed files with 62 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Play, X } from "@lucide/svelte"; import { GripVertical, Play, X } from "@lucide/svelte";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { player } from "$lib/player/store.svelte"; import { player } from "$lib/player/store.svelte";
import type { Track } from "$lib/player/types"; import type { Track } from "$lib/player/types";
@@ -14,6 +14,30 @@
// If it's in the queue, we can just set currentId. // If it's in the queue, we can just set currentId.
player.playId(track.id); player.playId(track.id);
} }
let dragOverIndex = $state<number | null>(null);
function onDragStart(e: DragEvent, index: number) {
if (!e.dataTransfer) return;
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", index.toString());
}
function onDragOver(e: DragEvent, index: number) {
e.preventDefault(); // allow drop
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
dragOverIndex = index;
}
function onDrop(e: DragEvent, toIndex: number) {
e.preventDefault();
dragOverIndex = null;
const fromIndexStr = e.dataTransfer?.getData("text/plain");
if (fromIndexStr) {
const fromIndex = parseInt(fromIndexStr, 10);
player.move(fromIndex, toIndex);
}
}
</script> </script>
<div <div
@@ -40,27 +64,36 @@
Queue is empty Queue is empty
</div> </div>
{:else} {:else}
{#each player.displayQueue as track (track.id)} {#each player.displayQueue as track, i (track.id)}
<div <div
role="button" role="button"
tabindex="0" tabindex="0"
draggable="true"
ondragstart={(e) => onDragStart(e, i)}
ondragover={(e) => onDragOver(e, i)}
ondrop={(e) => onDrop(e, i)}
onclick={() => onJump(track)} onclick={() => onJump(track)}
onkeydown={(e) => e.key === "Enter" && onJump(track)} onkeydown={(e) => e.key === "Enter" && onJump(track)}
class="group flex items-center gap-2 px-3 py-2 rounded-md hover:bg-muted/50 transition-colors cursor-pointer text-sm" class="group flex items-center gap-2 px-3 py-2 rounded-md hover:bg-muted/50 transition-colors cursor-pointer text-sm"
class:active={player.currentId === track.id} class:active={player.currentId === track.id}
class:border-t-2={dragOverIndex === i}
class:border-primary={dragOverIndex === i}
> >
<div <div
class="w-8 shrink-0 text-center text-xs text-muted-foreground/60 font-mono" class="w-8 shrink-0 flex items-center justify-center cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-foreground"
aria-label="Drag to reorder"
>
<GripVertical class="h-4 w-4" />
</div>
<div
class="w-6 shrink-0 text-center text-xs text-muted-foreground/60 font-mono"
> >
{#if player.currentId === track.id} {#if player.currentId === track.id}
<div <div
class="w-2 h-2 bg-primary rounded-full mx-auto animate-pulse" class="w-2 h-2 bg-primary rounded-full mx-auto animate-pulse"
></div> ></div>
{:else} {:else}
<span class="group-hover:hidden">#</span> <span class="text-xs">{i + 1}</span>
<Play
class="hidden group-hover:block mx-auto h-3 w-3 text-muted-foreground"
/>
{/if} {/if}
</div> </div>

View File

@@ -216,6 +216,28 @@ class PlayerStore {
} }
} }
move(fromIdx: number, toIdx: number) {
if (fromIdx === toIdx) return;
if (this.isShuffled) {
const indices = [...this.shuffledIndices];
if (fromIdx < 0 || fromIdx >= indices.length) return;
if (toIdx < 0 || toIdx >= indices.length) return;
const [item] = indices.splice(fromIdx, 1);
indices.splice(toIdx, 0, item);
this.shuffledIndices = indices;
} else {
const q = [...this.queue];
if (fromIdx < 0 || fromIdx >= q.length) return;
if (toIdx < 0 || toIdx >= q.length) return;
const [item] = q.splice(fromIdx, 1);
q.splice(toIdx, 0, item);
this.queue = q;
}
}
// Playback Controls // Playback Controls
next() { next() {