drag drop pt. 3

This commit is contained in:
2026-02-06 05:33:29 -08:00
parent c18f4e80b9
commit de711b8c40
2 changed files with 95 additions and 58 deletions

View File

@@ -275,8 +275,12 @@
let clearQueueDialogOpen = $state(false); let clearQueueDialogOpen = $state(false);
// --- Drag & drop reorder (works for both linear and shuffle) --- // --- Drag & drop reorder ---
// We reorder by track.id (annSongId). The player module adjusts traversal state. // Important: in shuffle mode, the UI is showing `queueDisplay` (shuffled traversal),
// so we must compute targets against that order (not `snap.queue`).
//
// We still reorder by track.id (annSongId). The player module applies shuffle-aware
// semantics (linear: reorder queue; shuffle: reorder upcoming play order).
let dragId = $state<number | null>(null); let dragId = $state<number | null>(null);
let dragOverId = $state<number | null>(null); let dragOverId = $state<number | null>(null);
let dragOverEdge = $state<"top" | "bottom" | null>(null); let dragOverEdge = $state<"top" | "bottom" | null>(null);
@@ -286,7 +290,6 @@
dragOverId = null; dragOverId = null;
dragOverEdge = null; dragOverEdge = null;
// Best-effort: set payload (useful for some browsers)
try { try {
e.dataTransfer?.setData("text/plain", String(trackId)); e.dataTransfer?.setData("text/plain", String(trackId));
e.dataTransfer!.effectAllowed = "move"; e.dataTransfer!.effectAllowed = "move";
@@ -300,15 +303,12 @@
if (!el) return "bottom" as const; if (!el) return "bottom" as const;
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const y = e.clientY;
const mid = rect.top + rect.height / 2; const mid = rect.top + rect.height / 2;
return y < mid ? ("top" as const) : ("bottom" as const); return e.clientY < mid ? ("top" as const) : ("bottom" as const);
} }
function onDragOver(trackId: number, e: DragEvent) { function onDragOver(trackId: number, e: DragEvent) {
// Required to allow dropping
e.preventDefault(); e.preventDefault();
dragOverId = trackId; dragOverId = trackId;
dragOverEdge = edgeFromPointer(e); dragOverEdge = edgeFromPointer(e);
@@ -322,7 +322,6 @@
function onDrop(targetTrackId: number, e: DragEvent) { function onDrop(targetTrackId: number, e: DragEvent) {
e.preventDefault(); e.preventDefault();
// Prefer our internal dragId; fall back to transfer data
let sourceId = dragId; let sourceId = dragId;
if (sourceId == null) { if (sourceId == null) {
const raw = e.dataTransfer?.getData("text/plain"); const raw = e.dataTransfer?.getData("text/plain");
@@ -337,11 +336,23 @@
return; return;
} }
// Reorder by queue indices (insert between rows based on pointer edge) // Work in DISPLAY ORDER, then map to queue indices.
const fromIndex = snap.queue.findIndex((t) => t.id === sourceId); const display = queueDisplay;
const targetIndex = snap.queue.findIndex((t) => t.id === targetTrackId); const fromItem = display.find((it) => it.track.id === sourceId);
const targetItem = display.find((it) => it.track.id === targetTrackId);
if (fromIndex === -1 || targetIndex === -1) { if (!fromItem || !targetItem) {
dragId = null;
dragOverId = null;
dragOverEdge = null;
return;
}
const fromQueueIndex = fromItem.queueIndex;
const targetQueueIndex = targetItem.queueIndex;
// If dropping onto itself, no-op
if (sourceId === targetTrackId) {
dragId = null; dragId = null;
dragOverId = null; dragOverId = null;
dragOverEdge = null; dragOverEdge = null;
@@ -350,46 +361,41 @@
const edge = dragOverEdge ?? edgeFromPointer(e); const edge = dragOverEdge ?? edgeFromPointer(e);
// Compute insertion index in the *post-removal* list. // Compute insertion slot in display order (0..display.length)
// const fromPos = display.findIndex((it) => it.track.id === sourceId);
// We interpret: const targetPos = display.findIndex((it) => it.track.id === targetTrackId);
// - edge === "top" -> insert before target row
// - edge === "bottom" -> insert after target row
//
// Because `reorderTrackById` uses "toIndex in current queue", we must
// translate the desired insertion slot into that API carefully.
//
// Approach:
// - Convert "insert after" into a "before the next item" index.
// - Adjust for the fact that removing `fromIndex` shifts indices.
let insertIndex = edge === "top" ? targetIndex : targetIndex + 1;
// Clamp to [0..len] if (fromPos === -1 || targetPos === -1) {
insertIndex = Math.max(0, Math.min(snap.queue.length, insertIndex));
// If moving downward and inserting after/before beyond itself, removal shifts indices.
// `reorderTrackById` expects a final *index of the moved item*, so we convert the
// insertion slot (0..len) into a destination index (0..len-1).
let toIndex: number;
if (insertIndex <= fromIndex) {
// You're inserting somewhere before (or at) the source position:
// final index is exactly the insert slot.
toIndex = insertIndex;
} else {
// You're inserting after the source position; after removal, everything shifts left by 1.
// The moved item ends up at insertIndex - 1.
toIndex = insertIndex - 1;
}
// If dropping "between" where it already is, no-op
if (toIndex === fromIndex) {
dragId = null; dragId = null;
dragOverId = null; dragOverId = null;
dragOverEdge = null; dragOverEdge = null;
return; return;
} }
reorderTrackById(sourceId, toIndex); let insertPos = edge === "top" ? targetPos : targetPos + 1;
insertPos = Math.max(0, Math.min(display.length, insertPos));
// Convert insert slot (0..len) into a destination position (0..len-1)
// accounting for removal shift.
let toPos: number;
if (insertPos <= fromPos) toPos = insertPos;
else toPos = insertPos - 1;
if (toPos === fromPos) {
dragId = null;
dragOverId = null;
dragOverEdge = null;
return;
}
// Map destination display position back to a queue index:
// - If inserting at the end, use the last item's queueIndex (the module will interpret appropriately)
// - Otherwise, use the queueIndex of the item currently at `toPos`
const anchor =
toPos >= display.length ? display[display.length - 1] : display[toPos];
const anchorQueueIndex = anchor?.queueIndex ?? targetQueueIndex;
reorderTrackById(sourceId, anchorQueueIndex);
dragId = null; dragId = null;
dragOverId = null; dragOverId = null;
@@ -405,12 +411,7 @@
function draggableHint(trackId: number) { function draggableHint(trackId: number) {
if (dragId == null) return ""; if (dragId == null) return "";
if (dragId === trackId) return "opacity-70"; if (dragId === trackId) return "opacity-70";
if (dragOverId === trackId) { if (dragOverId === trackId) return "ring-2 ring-primary/40 ring-offset-0";
if (dragOverEdge === "top") return "ring-2 ring-primary/40 ring-offset-0";
if (dragOverEdge === "bottom")
return "ring-2 ring-primary/40 ring-offset-0";
return "ring-2 ring-primary/40 ring-offset-0";
}
return ""; return "";
} }

View File

@@ -522,15 +522,20 @@ export function removeTrack(id: number): void {
/** /**
* Reorder an existing queued track by id. * Reorder an existing queued track by id.
* *
* Works in both linear and shuffle mode: * Semantics depend on shuffle:
* - Linear: `queue` order is updated and `currentIndex` is adjusted accordingly. * - Linear (shuffle off): reorder the underlying `queue` and adjust `currentIndex`.
* - Shuffle: traversal state (`order`/`history`/`cursor`) is re-indexed via * - Shuffle (shuffle on): reorder the UPCOMING play order (the `order` list), not the
* `reindexAfterMoveOrRemove` to preserve the same "already played" set and * underlying `queue`, because the UI is presenting a shuffled traversal that users
* future schedule, but mapped onto the new queue indices. * expect to be able to rearrange.
*
* In shuffle mode this only affects tracks that are still in the future schedule
* (`order`). Already-played history is left intact.
* *
* Notes: * Notes:
* - This only reorders if the track exists in the queue. * - `toIndex` is a queue index when shuffle is OFF (destination position in `queue`).
* - `toIndex` is clamped to the valid range. * - `toIndex` is a queue index when shuffle is ON too, but it is interpreted as
* "place this track before the track currently at queue index `toIndex` in the
* UPCOMING order", i.e. it changes `order`, not `queue`.
*/ */
export function reorderTrackById(id: number, toIndex: number): void { export function reorderTrackById(id: number, toIndex: number): void {
const from = indexOfTrack(id); const from = indexOfTrack(id);
@@ -540,6 +545,37 @@ export function reorderTrackById(id: number, toIndex: number): void {
0, 0,
Math.min(queue.length - 1, Math.floor(toIndex)), Math.min(queue.length - 1, Math.floor(toIndex)),
); );
// Shuffle: reorder upcoming traversal schedule, not underlying queue.
if (shuffleEnabled) {
// Ensure traversal state exists
ensureTraversalStateForCurrent();
// Only reorder within the future schedule (`order`).
// If the dragged track isn't in `order`, it's either current, already played, or unscheduled.
const fromPos = order.indexOf(from);
if (fromPos === -1) {
queueNotify();
return;
}
// Determine insertion position relative to the target queue index within `order`.
// If target isn't currently in `order`, we clamp to the end of the future schedule.
let toPos = order.indexOf(clampedTo);
if (toPos === -1) toPos = order.length;
// Moving an item forward past itself needs an index adjustment after removal.
const without = order.filter((i) => i !== from);
if (toPos > fromPos) toPos = Math.max(0, toPos - 1);
const nextPos = Math.max(0, Math.min(without.length, toPos));
order = [...without.slice(0, nextPos), from, ...without.slice(nextPos)];
queueNotify();
return;
}
// Linear: reorder the underlying queue
if (from === clampedTo) return; if (from === clampedTo) return;
moveIndex(from, clampedTo); moveIndex(from, clampedTo);