Make internal drag and drop robust
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
PLUGIN="u-navigator"
|
PLUGIN="u-navigator"
|
||||||
VERSION="0.2.7"
|
VERSION="0.2.8"
|
||||||
PKG_DIR="packages"
|
PKG_DIR="packages"
|
||||||
WORK_DIR=".plugin-build"
|
WORK_DIR=".plugin-build"
|
||||||
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
PKG_NAME="${PLUGIN}-${VERSION}.tgz"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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 {
|
function unav_guard_destination(string $source, string $destination): void {
|
||||||
if (file_exists($destination)) {
|
if (file_exists($destination)) {
|
||||||
unav_error(409, 'Destination already exists');
|
unav_error(409, 'Destination already exists');
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ unav_guard_destination($source['path'], $destination['path']);
|
|||||||
|
|
||||||
if ($action === 'move') {
|
if ($action === 'move') {
|
||||||
if (!rename($source['path'], $destination['path'])) {
|
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') {
|
} elseif ($action === 'copy') {
|
||||||
unav_copy_recursive($source['path'], $destination['path']);
|
unav_copy_recursive($source['path'], $destination['path']);
|
||||||
|
|||||||
+42
-2
@@ -7,6 +7,7 @@ const state = {
|
|||||||
focusedId: null,
|
focusedId: null,
|
||||||
selectedEntry: null,
|
selectedEntry: null,
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
|
draggedEntry: null,
|
||||||
jobs: new Map(),
|
jobs: new Map(),
|
||||||
nextWindowId: 1,
|
nextWindowId: 1,
|
||||||
zIndex: 20
|
zIndex: 20
|
||||||
@@ -196,11 +197,20 @@ function renderExplorer(body, win) {
|
|||||||
body.querySelector('[data-action="up"]').addEventListener('click', () => loadExplorer(win, parentPath(data.path)));
|
body.querySelector('[data-action="up"]').addEventListener('click', () => loadExplorer(win, parentPath(data.path)));
|
||||||
|
|
||||||
const dropZone = body.querySelector('.drop-zone');
|
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) => {
|
dropZone.addEventListener('dragover', (event) => {
|
||||||
|
if (hasDropPayload(event)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move';
|
event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move';
|
||||||
data.dragOver = true;
|
data.dragOver = true;
|
||||||
dropZone.classList.add('drag-over');
|
dropZone.classList.add('drag-over');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
dropZone.addEventListener('dragleave', () => {
|
dropZone.addEventListener('dragleave', () => {
|
||||||
data.dragOver = false;
|
data.dragOver = false;
|
||||||
@@ -210,7 +220,11 @@ function renderExplorer(body, win) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
data.dragOver = false;
|
data.dragOver = false;
|
||||||
dropZone.classList.remove('drag-over');
|
dropZone.classList.remove('drag-over');
|
||||||
|
try {
|
||||||
await handleDrop(event, win, data.path);
|
await handleDrop(event, win, data.path);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
dropZone.addEventListener('contextmenu', (event) => {
|
dropZone.addEventListener('contextmenu', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -227,6 +241,7 @@ function renderExplorer(body, win) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
row.addEventListener('dragstart', (event) => {
|
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('application/x-u-navigator-path', entry.path);
|
||||||
event.dataTransfer.setData('text/plain', entry.path);
|
event.dataTransfer.setData('text/plain', entry.path);
|
||||||
event.dataTransfer.effectAllowed = 'copyMove';
|
event.dataTransfer.effectAllowed = 'copyMove';
|
||||||
@@ -234,6 +249,9 @@ function renderExplorer(body, win) {
|
|||||||
});
|
});
|
||||||
row.addEventListener('dragend', () => {
|
row.addEventListener('dragend', () => {
|
||||||
row.classList.remove('dragging');
|
row.classList.remove('dragging');
|
||||||
|
setTimeout(() => {
|
||||||
|
state.draggedEntry = null;
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
row.addEventListener('dragover', (event) => {
|
row.addEventListener('dragover', (event) => {
|
||||||
if (entry.type === 'directory') {
|
if (entry.type === 'directory') {
|
||||||
@@ -246,7 +264,11 @@ function renderExplorer(body, win) {
|
|||||||
if (entry.type !== 'directory') return;
|
if (entry.type !== 'directory') return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
try {
|
||||||
await handleDrop(event, win, entry.path);
|
await handleDrop(event, win, entry.path);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
row.addEventListener('contextmenu', (event) => {
|
row.addEventListener('contextmenu', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -337,12 +359,15 @@ async function handleDrop(event, win, targetPath) {
|
|||||||
return;
|
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) {
|
if (internalPath) {
|
||||||
const name = internalPath.split('/').filter(Boolean).at(-1);
|
const name = internalPath.split('/').filter(Boolean).at(-1);
|
||||||
const destination = `${targetPath.replace(/\/$/, '')}/${name}`;
|
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 createJob(event.altKey ? 'copy' : 'move', internalPath, destination);
|
||||||
await loadExplorer(win, win.data.path);
|
await reloadExplorerWindows([win.id, state.draggedEntry?.sourceWindowId].filter(Boolean));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,6 +395,21 @@ async function createJob(type, source, destination) {
|
|||||||
render();
|
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) {
|
async function renameEntry(win, entry) {
|
||||||
const nextName = prompt('Neuer Name', entry.name);
|
const nextName = prompt('Neuer Name', entry.name);
|
||||||
if (!nextName || nextName === entry.name) {
|
if (!nextName || nextName === entry.name) {
|
||||||
|
|||||||
@@ -191,8 +191,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.u-nav .drop-zone.drag-over {
|
.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-color: var(--accent);
|
||||||
|
border-style: solid;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.u-nav .file-table {
|
.u-nav .file-table {
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "u-navigator">
|
<!ENTITY name "u-navigator">
|
||||||
<!ENTITY author "michael">
|
<!ENTITY author "michael">
|
||||||
<!ENTITY version "0.2.7">
|
<!ENTITY version "0.2.8">
|
||||||
<!ENTITY package "&name;-&version;.tgz">
|
<!ENTITY package "&name;-&version;.tgz">
|
||||||
]>
|
]>
|
||||||
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
|
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<FILE Name="/boot/config/plugins/&name;/&package;">
|
<FILE Name="/boot/config/plugins/&name;/&package;">
|
||||||
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
||||||
<MD5>c22b248a40a369a93ef58126e72fe823</MD5>
|
<MD5>5e77298b9383a80c1ca8c27020b1d8b0</MD5>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<FILE Run="/bin/bash">
|
<FILE Run="/bin/bash">
|
||||||
|
|||||||
Reference in New Issue
Block a user