diff --git a/build-plugin.sh b/build-plugin.sh index 5281a74..73f2dee 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="0.2.7" +VERSION="0.2.8" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-0.2.7.tgz b/packages/u-navigator-0.2.7.tgz deleted file mode 100644 index 1ade6c7..0000000 Binary files a/packages/u-navigator-0.2.7.tgz and /dev/null differ diff --git a/packages/u-navigator-0.2.8.tgz b/packages/u-navigator-0.2.8.tgz new file mode 100644 index 0000000..48e31ef Binary files /dev/null and b/packages/u-navigator-0.2.8.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php index 1a78d5e..53b86b3 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php @@ -122,6 +122,28 @@ function unav_copy_recursive(string $source, string $destination): void { } } +function unav_delete_recursive(string $path): void { + if (is_link($path) || is_file($path)) { + if (!unlink($path)) { + unav_error(500, 'Could not remove source after move'); + } + return; + } + if (is_dir($path)) { + foreach (scandir($path) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + unav_delete_recursive($path . '/' . $entry); + } + if (!rmdir($path)) { + unav_error(500, 'Could not remove source directory after move'); + } + return; + } + unav_error(400, 'Only files and directories can be moved'); +} + function unav_guard_destination(string $source, string $destination): void { if (file_exists($destination)) { unav_error(409, 'Destination already exists'); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php index 5915c84..a95c413 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php @@ -24,7 +24,8 @@ unav_guard_destination($source['path'], $destination['path']); if ($action === 'move') { if (!rename($source['path'], $destination['path'])) { - unav_error(500, 'Move failed'); + unav_copy_recursive($source['path'], $destination['path']); + unav_delete_recursive($source['path']); } } elseif ($action === 'copy') { unav_copy_recursive($source['path'], $destination['path']); diff --git a/server/public/app.js b/server/public/app.js index c867baf..9a624b6 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -7,6 +7,7 @@ const state = { focusedId: null, selectedEntry: null, contextMenu: null, + draggedEntry: null, jobs: new Map(), nextWindowId: 1, zIndex: 20 @@ -196,11 +197,20 @@ function renderExplorer(body, win) { body.querySelector('[data-action="up"]').addEventListener('click', () => loadExplorer(win, parentPath(data.path))); const dropZone = body.querySelector('.drop-zone'); + dropZone.addEventListener('dragenter', (event) => { + if (hasDropPayload(event)) { + event.preventDefault(); + data.dragOver = true; + dropZone.classList.add('drag-over'); + } + }); dropZone.addEventListener('dragover', (event) => { - event.preventDefault(); - event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move'; - data.dragOver = true; - dropZone.classList.add('drag-over'); + if (hasDropPayload(event)) { + event.preventDefault(); + event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move'; + data.dragOver = true; + dropZone.classList.add('drag-over'); + } }); dropZone.addEventListener('dragleave', () => { data.dragOver = false; @@ -210,7 +220,11 @@ function renderExplorer(body, win) { event.preventDefault(); data.dragOver = false; dropZone.classList.remove('drag-over'); - await handleDrop(event, win, data.path); + try { + await handleDrop(event, win, data.path); + } catch (error) { + showError(error); + } }); dropZone.addEventListener('contextmenu', (event) => { event.preventDefault(); @@ -227,6 +241,7 @@ function renderExplorer(body, win) { } }); row.addEventListener('dragstart', (event) => { + state.draggedEntry = { path: entry.path, name: entry.name, sourceWindowId: win.id }; event.dataTransfer.setData('application/x-u-navigator-path', entry.path); event.dataTransfer.setData('text/plain', entry.path); event.dataTransfer.effectAllowed = 'copyMove'; @@ -234,6 +249,9 @@ function renderExplorer(body, win) { }); row.addEventListener('dragend', () => { row.classList.remove('dragging'); + setTimeout(() => { + state.draggedEntry = null; + }, 0); }); row.addEventListener('dragover', (event) => { if (entry.type === 'directory') { @@ -246,7 +264,11 @@ function renderExplorer(body, win) { if (entry.type !== 'directory') return; event.preventDefault(); event.stopPropagation(); - await handleDrop(event, win, entry.path); + try { + await handleDrop(event, win, entry.path); + } catch (error) { + showError(error); + } }); row.addEventListener('contextmenu', (event) => { event.preventDefault(); @@ -337,12 +359,15 @@ async function handleDrop(event, win, targetPath) { return; } - const internalPath = event.dataTransfer.getData('application/x-u-navigator-path') || event.dataTransfer.getData('text/plain'); + const internalPath = event.dataTransfer.getData('application/x-u-navigator-path') || event.dataTransfer.getData('text/plain') || state.draggedEntry?.path; if (internalPath) { const name = internalPath.split('/').filter(Boolean).at(-1); const destination = `${targetPath.replace(/\/$/, '')}/${name}`; + if (internalPath === destination) { + throw new Error('Quelle und Ziel sind identisch.'); + } await createJob(event.altKey ? 'copy' : 'move', internalPath, destination); - await loadExplorer(win, win.data.path); + await reloadExplorerWindows([win.id, state.draggedEntry?.sourceWindowId].filter(Boolean)); return; } @@ -370,6 +395,21 @@ async function createJob(type, source, destination) { render(); } +async function reloadExplorerWindows(ids) { + const uniqueIds = new Set(ids); + const targets = state.windows.filter((win) => win.kind === 'explorer' && (uniqueIds.has(win.id) || !uniqueIds.size)); + await Promise.all(targets.map((win) => loadExplorer(win, win.data.path))); +} + +function hasDropPayload(event) { + return Boolean(state.draggedEntry || event.dataTransfer?.types?.includes('Files') || event.dataTransfer?.types?.includes('application/x-u-navigator-path') || event.dataTransfer?.types?.includes('text/plain')); +} + +function showError(error) { + const message = error?.message || String(error); + alert(message); +} + async function renameEntry(win, entry) { const nextName = prompt('Neuer Name', entry.name); if (!nextName || nextName === entry.name) { diff --git a/server/public/styles.css b/server/public/styles.css index d2c50e2..6031804 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -191,8 +191,10 @@ } .u-nav .drop-zone.drag-over { - background: color-mix(in srgb, var(--accent) 10%, transparent); + background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: var(--accent); + border-style: solid; + box-shadow: inset 0 0 0 2px var(--accent); } .u-nav .file-table { diff --git a/u-navigator.plg b/u-navigator.plg index 16b2408..bccb9e8 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; -c22b248a40a369a93ef58126e72fe823 +5e77298b9383a80c1ca8c27020b1d8b0