diff --git a/build-plugin.sh b/build-plugin.sh index c96a11b..bda7840 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r001" +VERSION="2026.06.23.r002" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r001.tgz b/packages/u-navigator-2026.06.23.r001.tgz deleted file mode 100644 index 8bb9ee6..0000000 Binary files a/packages/u-navigator-2026.06.23.r001.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r002.tgz b/packages/u-navigator-2026.06.23.r002.tgz new file mode 100644 index 0000000..50fcc6b Binary files /dev/null and b/packages/u-navigator-2026.06.23.r002.tgz differ diff --git a/server/public/app.js b/server/public/app.js index 725cc0d..71bf1c1 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -8,6 +8,8 @@ const state = { selectedEntry: null, contextMenu: null, draggedEntry: null, + pointerDrag: null, + suppressClickPath: null, jobs: new Map(), nextWindowId: 1, zIndex: 20 @@ -233,41 +235,16 @@ function renderExplorer(body, win) { for (const row of body.querySelectorAll('.file-row')) { const entry = data.entries.find((item) => item.path === row.dataset.path); - row.addEventListener('pointerdown', () => lockExplorerScroll(win, body)); + row.addEventListener('pointerdown', (event) => { + lockExplorerScroll(win, body); + startItemPointerDrag(event, win, entry); + }); row.addEventListener('click', () => selectEntry(win, entry, body)); row.addEventListener('dblclick', () => { if (entry.type === 'directory') { loadExplorer(win, entry.path); } }); - row.addEventListener('dragstart', (event) => { - state.draggedEntry = { path: entry.path, name: entry.name, sourceWindowId: win.id }; - setEffectAllowed(event); - row.classList.add('dragging'); - }); - row.addEventListener('dragend', () => { - row.classList.remove('dragging'); - setTimeout(() => { - state.draggedEntry = null; - }, 0); - }); - row.addEventListener('dragover', (event) => { - if (entry.type === 'directory') { - event.preventDefault(); - event.stopPropagation(); - setDropEffect(event, event.altKey ? 'copy' : 'move'); - } - }); - row.addEventListener('drop', async (event) => { - if (entry.type !== 'directory') return; - event.preventDefault(); - event.stopPropagation(); - try { - await handleDrop(event, win, entry.path); - } catch (error) { - showError(error); - } - }); row.addEventListener('contextmenu', (event) => { event.preventDefault(); event.stopPropagation(); @@ -283,7 +260,7 @@ function renderFileTable(entries) { } const rows = entries.map((entry) => ` - + ${icons[entry.type] || 'Item'}${escapeHtml(entry.name)} ${entry.type} ${entry.type === 'file' ? formatBytes(entry.size) : ''} @@ -528,6 +505,11 @@ async function pollJob(id) { } function selectEntry(win, entry, scrollBody = null) { + if (state.suppressClickPath === entry.path) { + state.suppressClickPath = null; + restoreExplorerScroll(win, scrollBody); + return; + } win.data.selectedPath = entry.path; state.selectedEntry = entry; focusWindow(win); @@ -563,6 +545,121 @@ function updatePropertiesWindows() { } } +function startItemPointerDrag(event, win, entry) { + if (event.button !== 0) return; + + state.pointerDrag = { + sourceWindowId: win.id, + path: entry.path, + name: entry.name, + startX: event.clientX, + startY: event.clientY, + active: false, + targetWindowId: null, + targetPath: null + }; + + const move = (moveEvent) => updateItemPointerDrag(moveEvent); + const up = (upEvent) => finishItemPointerDrag(upEvent, move, up); + document.addEventListener('pointermove', move); + document.addEventListener('pointerup', up, { once: true }); +} + +function updateItemPointerDrag(event) { + const drag = state.pointerDrag; + if (!drag) return; + + const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY); + if (!drag.active && distance < 6) { + return; + } + + drag.active = true; + event.preventDefault(); + document.body.classList.add('u-nav-dragging'); + markDraggingRow(drag.path, true); + updatePointerDropTarget(event); +} + +async function finishItemPointerDrag(event, move, up) { + document.removeEventListener('pointermove', move); + document.removeEventListener('pointerup', up); + + const drag = state.pointerDrag; + clearPointerDropTarget(); + document.body.classList.remove('u-nav-dragging'); + if (!drag) return; + + markDraggingRow(drag.path, false); + state.pointerDrag = null; + + if (!drag.active) { + return; + } + + state.suppressClickPath = drag.path; + const target = findPointerDropTarget(event); + if (!target) { + return; + } + + try { + const destination = `${target.path.replace(/\/$/, '')}/${drag.name}`; + if (destination === drag.path) { + throw new Error('Quelle und Ziel sind identisch.'); + } + await createJob(event.altKey ? 'copy' : 'move', drag.path, destination); + await reloadExplorerWindows([drag.sourceWindowId, target.windowId]); + } catch (error) { + showError(error); + } +} + +function updatePointerDropTarget(event) { + clearPointerDropTarget(); + const target = findPointerDropTarget(event); + if (!target) return; + + state.pointerDrag.targetWindowId = target.windowId; + state.pointerDrag.targetPath = target.path; + const windowEl = workspace.querySelector(`.window[data-window-id="${target.windowId}"]`); + windowEl?.querySelector('.drop-zone')?.classList.add('drag-over'); + if (target.row) { + target.row.classList.add('drop-target'); + } +} + +function clearPointerDropTarget() { + for (const item of workspace.querySelectorAll('.drop-zone.drag-over')) { + item.classList.remove('drag-over'); + } + for (const item of workspace.querySelectorAll('.file-row.drop-target')) { + item.classList.remove('drop-target'); + } +} + +function findPointerDropTarget(event) { + const element = document.elementFromPoint(event.clientX, event.clientY); + const windowEl = element?.closest?.('.window'); + if (!windowEl) return null; + + const win = state.windows.find((item) => item.id === windowEl.dataset.windowId); + if (!win || win.kind !== 'explorer') return null; + + const row = element.closest?.('.file-row'); + if (row?.dataset.type === 'directory') { + return { windowId: win.id, path: row.dataset.path, row }; + } + + return { windowId: win.id, path: win.data.path, row: null }; +} + +function markDraggingRow(path, dragging) { + for (const row of workspace.querySelectorAll(`.file-row[data-path="${cssEscape(path)}"]`)) { + row.classList.toggle('dragging', dragging); + } +} + function lockExplorerScroll(win, body) { win.data.lockScroll = true; win.data.lockScrollTop = body.scrollTop; @@ -695,6 +792,13 @@ function escapeAttr(value) { return escapeHtml(value); } +function cssEscape(value) { + if (window.CSS?.escape) { + return CSS.escape(String(value)); + } + return String(value).replace(/["\\]/g, '\\$&'); +} + function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } diff --git a/server/public/styles.css b/server/public/styles.css index 6031804..635ed91 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -229,16 +229,30 @@ .u-nav .file-row:hover, .u-nav .file-row.selected, -.u-nav .file-row.dragging { +.u-nav .file-row.dragging, +.u-nav .file-row.drop-target { background: var(--surface-2); } .u-nav .file-row:hover td, .u-nav .file-row.selected td, -.u-nav .file-row.dragging td { +.u-nav .file-row.dragging td, +.u-nav .file-row.drop-target td { background: var(--surface-2) !important; } +.u-nav .file-row { + user-select: none; +} + +.u-nav .file-row.dragging { + opacity: 0.55; +} + +.u-nav-dragging { + cursor: grabbing; +} + .u-nav .name-cell { align-items: center; display: flex; diff --git a/u-navigator.plg b/u-navigator.plg index 2a1ba12..f4d4fb4 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + ]> @@ -14,7 +14,7 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -54c2deddb4643c26d51b86aca896b52d +0a9e56a6a4ddf7648388b25a3dedb264