Improve navigator UI and context actions

This commit is contained in:
Mikei386
2026-06-22 22:40:50 +02:00
parent 20205cddc0
commit f29ecb0be5
9 changed files with 176 additions and 15 deletions
+3 -3
View File
@@ -2,7 +2,7 @@
set -eu set -eu
PLUGIN="u-navigator" PLUGIN="u-navigator"
VERSION="0.2.2" VERSION="0.2.3"
PKG_DIR="packages" PKG_DIR="packages"
WORK_DIR=".plugin-build" WORK_DIR=".plugin-build"
PKG_NAME="${PLUGIN}-${VERSION}.tgz" PKG_NAME="${PLUGIN}-${VERSION}.tgz"
@@ -11,8 +11,8 @@ rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets" "$PKG_DIR" mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets" "$PKG_DIR"
cp -R plugin-root/usr "$WORK_DIR/" cp -R plugin-root/usr "$WORK_DIR/"
cp public/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js" cp server/public/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js"
cp public/styles.css "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/styles.css" cp server/public/styles.css "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/styles.css"
tar -czf "$PKG_DIR/$PKG_NAME" -C "$WORK_DIR" usr tar -czf "$PKG_DIR/$PKG_NAME" -C "$WORK_DIR" usr
rm -rf "$WORK_DIR" rm -rf "$WORK_DIR"
Binary file not shown.
Binary file not shown.
@@ -29,17 +29,28 @@ Code="f07b"
} }
const wrapper = toolsLink.parentElement?.tagName === 'LI' ? toolsLink.parentElement : toolsLink; const wrapper = toolsLink.parentElement?.tagName === 'LI' ? toolsLink.parentElement : toolsLink;
const item = wrapper.cloneNode(true); const isListItem = wrapper.tagName === 'LI';
const link = item.tagName === 'A' ? item : item.querySelector('a'); const item = isListItem ? document.createElement('li') : document.createElement('a');
if (!link) { const link = isListItem ? document.createElement('a') : item;
return; link.className = toolsLink.className || '';
}
link.href = targetHref; link.href = targetHref;
link.textContent = targetText; link.textContent = targetText;
link.setAttribute('data-u-navigator-nav', 'true'); link.setAttribute('data-u-navigator-nav', 'true');
link.classList.remove('active', 'selected'); link.classList.remove('active', 'selected', 'on');
item.classList?.remove('active', 'selected');
if (isListItem) {
item.className = wrapper.className || '';
item.classList.remove('active', 'selected', 'on');
item.appendChild(link);
}
if (location.pathname === targetHref) {
toolsLink.classList.remove('active', 'selected', 'on');
wrapper.classList?.remove('active', 'selected', 'on');
link.classList.add('active');
item.classList?.add('active');
}
wrapper.insertAdjacentElement('afterend', item); wrapper.insertAdjacentElement('afterend', item);
} }
+112
View File
@@ -6,6 +6,7 @@ const state = {
windows: [], windows: [],
focusedId: null, focusedId: null,
selectedEntry: null, selectedEntry: null,
contextMenu: null,
jobs: new Map(), jobs: new Map(),
nextWindowId: 1, nextWindowId: 1,
zIndex: 20 zIndex: 20
@@ -106,6 +107,9 @@ function render() {
for (const win of state.windows) { for (const win of state.windows) {
workspace.appendChild(renderWindow(win)); workspace.appendChild(renderWindow(win));
} }
if (state.contextMenu) {
workspace.appendChild(renderContextMenu());
}
} }
function renderWindow(win) { function renderWindow(win) {
@@ -181,6 +185,7 @@ function renderExplorer(body, win) {
const dropZone = body.querySelector('.drop-zone'); const dropZone = body.querySelector('.drop-zone');
dropZone.addEventListener('dragover', (event) => { dropZone.addEventListener('dragover', (event) => {
event.preventDefault(); event.preventDefault();
event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move';
data.dragOver = true; data.dragOver = true;
dropZone.classList.add('drag-over'); dropZone.classList.add('drag-over');
}); });
@@ -194,6 +199,10 @@ function renderExplorer(body, win) {
dropZone.classList.remove('drag-over'); dropZone.classList.remove('drag-over');
await handleDrop(event, win, data.path); await handleDrop(event, win, data.path);
}); });
dropZone.addEventListener('contextmenu', (event) => {
event.preventDefault();
showContextMenu(event, win, null);
});
for (const row of body.querySelectorAll('.file-row')) { for (const row of body.querySelectorAll('.file-row')) {
const entry = data.entries.find((item) => item.path === row.dataset.path); const entry = data.entries.find((item) => item.path === row.dataset.path);
@@ -205,7 +214,12 @@ function renderExplorer(body, win) {
}); });
row.addEventListener('dragstart', (event) => { row.addEventListener('dragstart', (event) => {
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.effectAllowed = 'copyMove'; event.dataTransfer.effectAllowed = 'copyMove';
row.classList.add('dragging');
});
row.addEventListener('dragend', () => {
row.classList.remove('dragging');
}); });
row.addEventListener('dragover', (event) => { row.addEventListener('dragover', (event) => {
if (entry.type === 'directory') { if (entry.type === 'directory') {
@@ -218,6 +232,11 @@ function renderExplorer(body, win) {
event.preventDefault(); event.preventDefault();
await handleDrop(event, win, entry.path); await handleDrop(event, win, entry.path);
}); });
row.addEventListener('contextmenu', (event) => {
event.preventDefault();
selectEntry(win, entry);
showContextMenu(event, win, entry);
});
} }
} }
@@ -334,6 +353,93 @@ async function createJob(type, source, destination) {
render(); render();
} }
async function renameEntry(win, entry) {
const nextName = prompt('Neuer Name', entry.name);
if (!nextName || nextName === entry.name) {
return;
}
if (nextName.includes('/')) {
alert('Der Name darf keinen Slash enthalten.');
return;
}
await createJob('move', entry.path, `${dirname(entry.path)}/${nextName}`);
await loadExplorer(win, win.data.path);
}
async function promptTransfer(win, entry, type) {
const targetDir = prompt(type === 'copy' ? 'Kopieren nach Ordner' : 'Verschieben nach Ordner', win.data.path);
if (!targetDir) {
return;
}
await createJob(type, entry.path, `${targetDir.replace(/\/$/, '')}/${entry.name}`);
await loadExplorer(win, win.data.path);
}
function showContextMenu(event, win, entry) {
const rect = workspace.getBoundingClientRect();
state.contextMenu = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
winId: win.id,
entryPath: entry?.path || null
};
render();
}
function renderContextMenu() {
const menu = document.createElement('div');
menu.className = 'context-menu';
menu.style.left = `${state.contextMenu.x}px`;
menu.style.top = `${state.contextMenu.y}px`;
const win = state.windows.find((item) => item.id === state.contextMenu.winId);
const entry = win?.data.entries?.find((item) => item.path === state.contextMenu.entryPath) || null;
const actions = [];
if (entry) {
if (entry.type === 'directory') {
actions.push(['Öffnen', () => loadExplorer(win, entry.path)]);
}
actions.push(['Umbenennen', () => renameEntry(win, entry)]);
actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]);
actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]);
} else {
actions.push(['Neu laden', () => loadExplorer(win, win.data.path)]);
}
for (const [label, action] of actions) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.addEventListener('click', async () => {
state.contextMenu = null;
render();
try {
await action();
} catch (error) {
alert(error.message);
}
});
menu.appendChild(button);
}
setTimeout(() => {
const close = () => {
state.contextMenu = null;
render();
document.removeEventListener('click', close);
document.removeEventListener('keydown', keyClose);
};
const keyClose = (event) => {
if (event.key === 'Escape') close();
};
document.addEventListener('click', close);
document.addEventListener('keydown', keyClose);
}, 0);
return menu;
}
async function pollJob(id) { async function pollJob(id) {
const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`)); const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`));
state.jobs.set(id, job); state.jobs.set(id, job);
@@ -429,6 +535,12 @@ function parentPath(value) {
return `/${parts.slice(0, -1).join('/')}`; return `/${parts.slice(0, -1).join('/')}`;
} }
function dirname(value) {
const parts = String(value || '').split('/').filter(Boolean);
if (parts.length <= 1) return '/';
return `/${parts.slice(0, -1).join('/')}`;
}
function formatBytes(bytes) { function formatBytes(bytes) {
const value = Number(bytes || 0); const value = Number(bytes || 0);
if (value < 1024) return `${value} B`; if (value < 1024) return `${value} B`;
+39 -1
View File
@@ -197,32 +197,46 @@
.u-nav .file-table { .u-nav .file-table {
border-collapse: collapse; border-collapse: collapse;
background: var(--surface);
color: var(--text);
width: 100%; width: 100%;
} }
.u-nav .file-table th, .u-nav .file-table th,
.u-nav .file-table td { .u-nav .file-table td {
background: var(--surface) !important;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
color: var(--text) !important;
padding: 8px 7px; padding: 8px 7px;
text-align: left; text-align: left;
vertical-align: middle; vertical-align: middle;
} }
.u-nav .file-table th { .u-nav .file-table th {
background: var(--surface-2) !important;
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
} }
.u-nav .file-row { .u-nav .file-row {
background: var(--surface);
color: var(--text);
cursor: default; cursor: default;
} }
.u-nav .file-row:hover, .u-nav .file-row:hover,
.u-nav .file-row.selected { .u-nav .file-row.selected,
.u-nav .file-row.dragging {
background: var(--surface-2); background: var(--surface-2);
} }
.u-nav .file-row:hover td,
.u-nav .file-row.selected td,
.u-nav .file-row.dragging td {
background: var(--surface-2) !important;
}
.u-nav .name-cell { .u-nav .name-cell {
align-items: center; align-items: center;
display: flex; display: flex;
@@ -241,6 +255,30 @@
padding: 2px 8px; padding: 2px 8px;
} }
.u-nav .context-menu {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
display: grid;
min-width: 190px;
padding: 6px;
position: absolute;
z-index: 100000;
}
.u-nav .context-menu button {
border-color: transparent;
justify-content: flex-start;
text-align: left;
width: 100%;
}
.u-nav .context-menu button:hover {
background: var(--surface-2);
border-color: var(--line);
}
.u-nav .queue-list { .u-nav .queue-list {
display: grid; display: grid;
gap: 10px; gap: 10px;
+1 -1
View File
@@ -9,7 +9,7 @@ import { Sandbox, rootsFromEnv } from './sandbox.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..'); const projectRoot = path.resolve(__dirname, '..');
const publicDir = path.join(projectRoot, 'public'); const publicDir = path.join(projectRoot, 'server', 'public');
const MIME = { const MIME = {
'.html': 'text/html; charset=utf-8', '.html': 'text/html; charset=utf-8',
+2 -2
View File
@@ -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.2"> <!ENTITY version "0.2.3">
<!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>0cb8fe513addbd7435a8c79107ae7973</MD5> <MD5>8d05b440bd2ad3e20b8d900238642c3f</MD5>
</FILE> </FILE>
<FILE Run="/bin/bash"> <FILE Run="/bin/bash">