From b37fabd9367f9c0f33ac31868be2dfcb268c43f4 Mon Sep 17 00:00:00 2001 From: Senko-san Date: Sat, 13 Jun 2026 17:40:58 +0300 Subject: [PATCH] feat(queue): make queue tracks draggable via dnd-kit Wire up @dnd-kit sortable context in QueuePanel so tracks can be reordered by dragging the grip handle, dispatching moveInQueue on drop. --- package-lock.json | 56 ++++++++++++++++ package.json | 3 + src/components/player/QueuePanel.tsx | 97 +++++++++++++++++++++++----- src/styles/shell.css | 6 ++ 4 files changed, 145 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index fea4599..7730d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "mcma-webui", "version": "1.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@olly/modern-sk": "^0.1.5", "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.12.0", @@ -552,6 +555,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", diff --git a/package.json b/package.json index 6575ca7..b22bc1d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "test:watch": "rstest --watch" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@olly/modern-sk": "^0.1.5", "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.12.0", diff --git a/src/components/player/QueuePanel.tsx b/src/components/player/QueuePanel.tsx index 344efdc..98606ff 100644 --- a/src/components/player/QueuePanel.tsx +++ b/src/components/player/QueuePanel.tsx @@ -8,6 +8,22 @@ import { IconButton, } from '@olly/modern-sk'; import { useTranslation } from 'react-i18next'; +import { + DndContext, + closestCenter, + PointerSensor, + KeyboardSensor, + useSensor, + useSensors, + type DragEndEvent, +} from '@dnd-kit/core'; +import { + SortableContext, + useSortable, + verticalListSortingStrategy, + sortableKeyboardCoordinates, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import { Icon } from '../common/Icon'; import { ArtTile } from '../common/ArtTile'; import { PlayingIndicator } from '../common/PlayingIndicator'; @@ -35,6 +51,19 @@ export function QueuePanel() { const isRadio = queue.source === 'radio'; const sourceLabel = queue.sourceName ?? queue.source; + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + dispatch(moveInQueue({ from: Number(active.id), to: Number(over.id) })); + }; + return (