drag drop pt. 3
This commit is contained in:
@@ -275,8 +275,12 @@
|
||||
|
||||
let clearQueueDialogOpen = $state(false);
|
||||
|
||||
// --- Drag & drop reorder (works for both linear and shuffle) ---
|
||||
// We reorder by track.id (annSongId). The player module adjusts traversal state.
|
||||
// --- Drag & drop reorder ---
|
||||
// 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 dragOverId = $state<number | null>(null);
|
||||
let dragOverEdge = $state<"top" | "bottom" | null>(null);
|
||||
@@ -286,7 +290,6 @@
|
||||
dragOverId = null;
|
||||
dragOverEdge = null;
|
||||
|
||||
// Best-effort: set payload (useful for some browsers)
|
||||
try {
|
||||
e.dataTransfer?.setData("text/plain", String(trackId));
|
||||
e.dataTransfer!.effectAllowed = "move";
|
||||
@@ -300,15 +303,12 @@
|
||||
if (!el) return "bottom" as const;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const y = e.clientY;
|
||||
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) {
|
||||
// Required to allow dropping
|
||||
e.preventDefault();
|
||||
|
||||
dragOverId = trackId;
|
||||
dragOverEdge = edgeFromPointer(e);
|
||||
|
||||
@@ -322,7 +322,6 @@
|
||||
function onDrop(targetTrackId: number, e: DragEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
// Prefer our internal dragId; fall back to transfer data
|
||||
let sourceId = dragId;
|
||||
if (sourceId == null) {
|
||||
const raw = e.dataTransfer?.getData("text/plain");
|
||||
@@ -337,11 +336,23 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Reorder by queue indices (insert between rows based on pointer edge)
|
||||
const fromIndex = snap.queue.findIndex((t) => t.id === sourceId);
|
||||
const targetIndex = snap.queue.findIndex((t) => t.id === targetTrackId);
|
||||
// Work in DISPLAY ORDER, then map to queue indices.
|
||||
const display = queueDisplay;
|
||||
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;
|
||||
dragOverId = null;
|
||||
dragOverEdge = null;
|
||||
@@ -350,46 +361,41 @@
|
||||
|
||||
const edge = dragOverEdge ?? edgeFromPointer(e);
|
||||
|
||||
// Compute insertion index in the *post-removal* list.
|
||||
//
|
||||
// We interpret:
|
||||
// - 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;
|
||||
// Compute insertion slot in display order (0..display.length)
|
||||
const fromPos = display.findIndex((it) => it.track.id === sourceId);
|
||||
const targetPos = display.findIndex((it) => it.track.id === targetTrackId);
|
||||
|
||||
// Clamp to [0..len]
|
||||
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) {
|
||||
if (fromPos === -1 || targetPos === -1) {
|
||||
dragId = null;
|
||||
dragOverId = null;
|
||||
dragOverEdge = null;
|
||||
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;
|
||||
dragOverId = null;
|
||||
@@ -405,12 +411,7 @@
|
||||
function draggableHint(trackId: number) {
|
||||
if (dragId == null) return "";
|
||||
if (dragId === trackId) return "opacity-70";
|
||||
if (dragOverId === trackId) {
|
||||
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";
|
||||
}
|
||||
if (dragOverId === trackId) return "ring-2 ring-primary/40 ring-offset-0";
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -522,15 +522,20 @@ export function removeTrack(id: number): void {
|
||||
/**
|
||||
* Reorder an existing queued track by id.
|
||||
*
|
||||
* Works in both linear and shuffle mode:
|
||||
* - Linear: `queue` order is updated and `currentIndex` is adjusted accordingly.
|
||||
* - Shuffle: traversal state (`order`/`history`/`cursor`) is re-indexed via
|
||||
* `reindexAfterMoveOrRemove` to preserve the same "already played" set and
|
||||
* future schedule, but mapped onto the new queue indices.
|
||||
* Semantics depend on shuffle:
|
||||
* - Linear (shuffle off): reorder the underlying `queue` and adjust `currentIndex`.
|
||||
* - Shuffle (shuffle on): reorder the UPCOMING play order (the `order` list), not the
|
||||
* underlying `queue`, because the UI is presenting a shuffled traversal that users
|
||||
* 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:
|
||||
* - This only reorders if the track exists in the queue.
|
||||
* - `toIndex` is clamped to the valid range.
|
||||
* - `toIndex` is a queue index when shuffle is OFF (destination position in `queue`).
|
||||
* - `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 {
|
||||
const from = indexOfTrack(id);
|
||||
@@ -540,6 +545,37 @@ export function reorderTrackById(id: number, toIndex: number): void {
|
||||
0,
|
||||
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;
|
||||
|
||||
moveIndex(from, clampedTo);
|
||||
|
||||
Reference in New Issue
Block a user