diff --git a/build-plugin.sh b/build-plugin.sh index 4c2c627..01e4990 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r014" +VERSION="2026.06.23.r015" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r014.tgz b/packages/u-navigator-2026.06.23.r014.tgz deleted file mode 100644 index 3bebf93..0000000 Binary files a/packages/u-navigator-2026.06.23.r014.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r015.tgz b/packages/u-navigator-2026.06.23.r015.tgz new file mode 100644 index 0000000..c7f5b04 Binary files /dev/null and b/packages/u-navigator-2026.06.23.r015.tgz differ diff --git a/server/public/app.js b/server/public/app.js index 6174e80..3df80c1 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -727,6 +727,7 @@ function startItemPointerDrag(event, win, entry) { sourceWindowId: win.id, path: entry.path, name: entry.name, + type: entry.type, startX: event.clientX, startY: event.clientY, active: false, @@ -756,6 +757,7 @@ function updateItemPointerDrag(event) { event.preventDefault(); document.body.classList.add('u-nav-dragging'); markDraggingRow(drag.path, true); + markDropCandidates(drag.path, true); updatePointerDropTarget(event); } @@ -769,6 +771,7 @@ async function finishItemPointerDrag(event, move, up) { if (!drag) return; markDraggingRow(drag.path, false); + markDropCandidates(drag.path, false); state.pointerDrag = null; if (!drag.active) { @@ -794,6 +797,9 @@ async function finishItemPointerDrag(event, move, up) { if (destination === drag.path) { throw new Error('Quelle und Ziel sind identisch.'); } + if (drag.type === 'directory' && target.path.startsWith(`${drag.path.replace(/\/$/, '')}/`)) { + throw new Error('Ein Ordner kann nicht in sich selbst verschoben werden.'); + } await createJob(event.altKey ? 'copy' : 'move', drag.path, destination, [drag.sourceWindowId, target.windowId]); } catch (error) { showError(error); @@ -825,8 +831,10 @@ function clearPointerDropTarget() { function findPointerDropTarget(event) { let element; + let elements = []; try { element = document.elementFromPoint(event.clientX, event.clientY); + elements = document.elementsFromPoint ? document.elementsFromPoint(event.clientX, event.clientY) : [element]; } catch (error) { recordDebug('elementFromPoint.error', { x: event.clientX, @@ -847,13 +855,30 @@ function findPointerDropTarget(event) { const win = state.windows.find((item) => item.id === windowEl.dataset.windowId); if (!win || win.kind !== 'explorer') return null; - let row; - try { - row = element.closest?.('.file-row'); - } catch (error) { - recordDebug('closest.row.error', { error: serializeError(error), element: element?.tagName }); + const drag = state.pointerDrag; + let row = null; + for (const candidate of elements) { + try { + const candidateRow = candidate.closest?.('.file-row'); + if (!candidateRow || candidateRow.dataset.type !== 'directory') { + continue; + } + if (candidateRow.dataset.path === drag?.path) { + continue; + } + if (drag?.type === 'directory' && candidateRow.dataset.path.startsWith(`${drag.path.replace(/\/$/, '')}/`)) { + continue; + } + if (candidateRow.closest?.('.window') !== windowEl) { + continue; + } + row = candidateRow; + break; + } catch (error) { + recordDebug('closest.row.error', { error: serializeError(error), element: candidate?.tagName }); + } } - if (row?.dataset.type === 'directory') { + if (row) { return { windowId: win.id, path: row.dataset.path, row }; } @@ -868,6 +893,15 @@ function markDraggingRow(path, dragging) { } } +function markDropCandidates(sourcePath, active) { + const source = state.pointerDrag; + for (const row of workspace.querySelectorAll('.file-row[data-type="directory"]')) { + const path = row.dataset.path || ''; + const valid = path !== sourcePath && !(source?.type === 'directory' && path.startsWith(`${sourcePath.replace(/\/$/, '')}/`)); + row.classList.toggle('drop-candidate', active && valid); + } +} + function lockExplorerScroll(win, body) { win.data.lockScroll = true; win.data.lockScrollTop = body.scrollTop; diff --git a/server/public/styles.css b/server/public/styles.css index e938e23..9f710ab 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -255,6 +255,7 @@ .u-nav .file-row:hover, .u-nav .file-row.selected, .u-nav .file-row.dragging, +.u-nav .file-row.drop-candidate, .u-nav .file-row.drop-target { background: var(--surface-2); } @@ -262,10 +263,15 @@ .u-nav .file-row:hover td, .u-nav .file-row.selected td, .u-nav .file-row.dragging td, +.u-nav .file-row.drop-candidate td, .u-nav .file-row.drop-target td { background: var(--surface-2) !important; } +.u-nav .file-row.drop-candidate td:first-child { + box-shadow: inset 3px 0 0 color-mix(in srgb, var(--accent) 72%, transparent); +} + .u-nav .file-row { -webkit-user-drag: none; user-select: none; @@ -327,16 +333,22 @@ .u-nav .icon-item:hover, .u-nav .icon-item.selected, .u-nav .icon-item.dragging, +.u-nav .icon-item.drop-candidate, .u-nav .icon-item.drop-target { background: var(--surface-2); border-color: var(--line); } .u-nav .icon-item.selected, +.u-nav .icon-item.drop-candidate, .u-nav .icon-item.drop-target { border-color: var(--accent); } +.u-nav .icon-item.drop-candidate { + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent); +} + .u-nav .file-icon { display: block; height: 52px; diff --git a/u-navigator.plg b/u-navigator.plg index c93f6c9..e9f82f2 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; -0100a8a8507d664658a9451a40881944 +5dd13445e7847e581364f61f5fd26808