|
|
@@ -4,6 +4,7 @@ const Browser = (() => {
|
|
|
let dirContents = {}; // 'root' | dirId -> { dirs, tracks }
|
|
|
let dirMeta = {}; // dirId -> { id, name, parentId }
|
|
|
let dragTrackIds = []; // ids being dragged (may be multiple)
|
|
|
+ let dragDirId = null; // dir id being dragged (single)
|
|
|
let allDirs = []; // flat list for move dialog
|
|
|
|
|
|
// ===== Multi-select state =====
|
|
|
@@ -120,7 +121,7 @@ const Browser = (() => {
|
|
|
const isSelected = selectedDirId === dir.id;
|
|
|
html += `<div class="tree-item dir-item${isSelected ? ' selected' : ''}"
|
|
|
data-id="${dir.id}" data-name="${escAttr(dir.name)}"
|
|
|
- style="padding-left:${12 + baseIndent}px" data-drop-target="true">
|
|
|
+ style="padding-left:${12 + baseIndent}px" data-drop-target="true" draggable="true">
|
|
|
<span class="expand-btn${isExpanded ? ' expanded' : ''}" data-expand="${dir.id}"></span>
|
|
|
<span class="item-icon">📁</span>
|
|
|
<span class="item-name">${escHtml(dir.name)}</span>
|
|
|
@@ -260,7 +261,6 @@ const Browser = (() => {
|
|
|
container.querySelectorAll('.track-item[draggable="true"]').forEach(el => {
|
|
|
el.addEventListener('dragstart', (e) => {
|
|
|
const id = parseInt(el.dataset.id);
|
|
|
- // If dragging a selected item, drag all selected; otherwise just this one
|
|
|
if (selectedTrackIds.has(id)) {
|
|
|
dragTrackIds = [...selectedTrackIds];
|
|
|
} else {
|
|
|
@@ -281,10 +281,27 @@ const Browser = (() => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+ // Drag folders
|
|
|
+ container.querySelectorAll('.dir-item[draggable="true"]').forEach(el => {
|
|
|
+ el.addEventListener('dragstart', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ dragDirId = parseInt(el.dataset.id);
|
|
|
+ e.dataTransfer.effectAllowed = 'move';
|
|
|
+ e.dataTransfer.setData('text/plain', String(dragDirId));
|
|
|
+ el.classList.add('dragging');
|
|
|
+ });
|
|
|
+ el.addEventListener('dragend', () => {
|
|
|
+ el.classList.remove('dragging');
|
|
|
+ container.querySelectorAll('.drag-target').forEach(d => d.classList.remove('drag-target'));
|
|
|
+ dragDirId = null;
|
|
|
+ stopAutoScroll();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
// Auto-scroll when dragging near list edges
|
|
|
const list = document.getElementById('browser-list');
|
|
|
list.addEventListener('dragover', (e) => {
|
|
|
- if (dragTrackIds.length === 0) return;
|
|
|
+ if (dragTrackIds.length === 0 && dragDirId === null) return;
|
|
|
const rect = list.getBoundingClientRect();
|
|
|
const ZONE = 48;
|
|
|
if (e.clientY < rect.top + ZONE) {
|
|
|
@@ -302,14 +319,36 @@ const Browser = (() => {
|
|
|
// Drop targets (folders)
|
|
|
container.querySelectorAll('.dir-item[data-drop-target]').forEach(el => {
|
|
|
el._dragCount = 0;
|
|
|
+
|
|
|
+ function isDragging() { return dragTrackIds.length > 0 || dragDirId !== null; }
|
|
|
+
|
|
|
+ // For dir drags: reject drop onto self or own descendant
|
|
|
+ function isValidDirDrop(targetId) {
|
|
|
+ if (dragDirId === null) return false;
|
|
|
+ if (dragDirId === targetId) return false;
|
|
|
+ // Walk up from targetId through dirMeta; if we hit dragDirId it's a descendant
|
|
|
+ let cur = targetId;
|
|
|
+ const seen = new Set();
|
|
|
+ while (cur !== null && cur !== undefined) {
|
|
|
+ if (seen.has(cur)) break;
|
|
|
+ seen.add(cur);
|
|
|
+ const meta = dirMeta[cur];
|
|
|
+ if (!meta) break;
|
|
|
+ cur = meta.parentId;
|
|
|
+ if (cur === dragDirId) return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
el.addEventListener('dragenter', (e) => {
|
|
|
- if (dragTrackIds.length === 0) return;
|
|
|
+ if (!isDragging()) return;
|
|
|
+ if (dragDirId !== null && !isValidDirDrop(parseInt(el.dataset.id))) return;
|
|
|
e.preventDefault();
|
|
|
el._dragCount = (el._dragCount || 0) + 1;
|
|
|
el.classList.add('drag-target');
|
|
|
});
|
|
|
el.addEventListener('dragleave', () => {
|
|
|
- if (dragTrackIds.length === 0) return;
|
|
|
+ if (!isDragging()) return;
|
|
|
el._dragCount = (el._dragCount || 1) - 1;
|
|
|
if (el._dragCount <= 0) {
|
|
|
el._dragCount = 0;
|
|
|
@@ -317,7 +356,8 @@ const Browser = (() => {
|
|
|
}
|
|
|
});
|
|
|
el.addEventListener('dragover', (e) => {
|
|
|
- if (dragTrackIds.length === 0) return;
|
|
|
+ if (!isDragging()) return;
|
|
|
+ if (dragDirId !== null && !isValidDirDrop(parseInt(el.dataset.id))) return;
|
|
|
e.preventDefault();
|
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
});
|
|
|
@@ -326,13 +366,20 @@ const Browser = (() => {
|
|
|
e.stopPropagation();
|
|
|
el._dragCount = 0;
|
|
|
el.classList.remove('drag-target');
|
|
|
- if (dragTrackIds.length === 0) return;
|
|
|
const targetDirId = parseInt(el.dataset.id);
|
|
|
const targetDirName = el.dataset.name;
|
|
|
- const ids = dragTrackIds.slice();
|
|
|
- dragTrackIds = [];
|
|
|
- stopAutoScroll();
|
|
|
- confirmMoveTracks(ids, targetDirId, targetDirName);
|
|
|
+ if (dragTrackIds.length > 0) {
|
|
|
+ const ids = dragTrackIds.slice();
|
|
|
+ dragTrackIds = [];
|
|
|
+ stopAutoScroll();
|
|
|
+ confirmMoveTracks(ids, targetDirId, targetDirName);
|
|
|
+ } else if (dragDirId !== null && isValidDirDrop(targetDirId)) {
|
|
|
+ const srcId = dragDirId;
|
|
|
+ const srcName = dirMeta[srcId]?.name || 'folder';
|
|
|
+ dragDirId = null;
|
|
|
+ stopAutoScroll();
|
|
|
+ confirmMoveDir(srcId, srcName, targetDirId, targetDirName);
|
|
|
+ }
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
@@ -588,6 +635,24 @@ const Browser = (() => {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ function confirmMoveDir(srcId, srcName, targetDirId, targetDirName) {
|
|
|
+ document.getElementById('confirm-title').textContent = 'Move Folder';
|
|
|
+ document.getElementById('confirm-message').textContent =
|
|
|
+ `Move folder "${srcName}" into "${targetDirName}"?`;
|
|
|
+ const modal = document.getElementById('confirm-modal');
|
|
|
+ modal.style.display = 'flex';
|
|
|
+ document.getElementById('confirm-ok-btn').onclick = async () => {
|
|
|
+ modal.style.display = 'none';
|
|
|
+ try {
|
|
|
+ await API.moveDir(srcId, targetDirId);
|
|
|
+ await reload();
|
|
|
+ showToast('Folder moved', 'success');
|
|
|
+ } catch (e) {
|
|
|
+ showToast('Error: ' + e.message, 'error');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
async function showMoveDialog(trackId, trackName) {
|
|
|
try {
|
|
|
allDirs = await loadAllDirs();
|