drag drop pt. 3
This commit is contained in:
@@ -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 "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user