diff --git a/build-plugin.sh b/build-plugin.sh index 0a9953c..2b77740 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="0.2.2" +VERSION="0.2.3" PKG_DIR="packages" WORK_DIR=".plugin-build" 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" cp -R plugin-root/usr "$WORK_DIR/" -cp 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/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js" +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 rm -rf "$WORK_DIR" diff --git a/packages/u-navigator-0.2.2.tgz b/packages/u-navigator-0.2.2.tgz deleted file mode 100644 index e213fd0..0000000 Binary files a/packages/u-navigator-0.2.2.tgz and /dev/null differ diff --git a/packages/u-navigator-0.2.3.tgz b/packages/u-navigator-0.2.3.tgz new file mode 100644 index 0000000..84dd1a6 Binary files /dev/null and b/packages/u-navigator-0.2.3.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-NavigatorNav.page b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-NavigatorNav.page index 7a3256b..f181124 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-NavigatorNav.page +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-NavigatorNav.page @@ -29,17 +29,28 @@ Code="f07b" } const wrapper = toolsLink.parentElement?.tagName === 'LI' ? toolsLink.parentElement : toolsLink; - const item = wrapper.cloneNode(true); - const link = item.tagName === 'A' ? item : item.querySelector('a'); - if (!link) { - return; - } - + const isListItem = wrapper.tagName === 'LI'; + const item = isListItem ? document.createElement('li') : document.createElement('a'); + const link = isListItem ? document.createElement('a') : item; + link.className = toolsLink.className || ''; link.href = targetHref; link.textContent = targetText; link.setAttribute('data-u-navigator-nav', 'true'); - link.classList.remove('active', 'selected'); - item.classList?.remove('active', 'selected'); + link.classList.remove('active', 'selected', 'on'); + + 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); } diff --git a/public/app.js b/server/public/app.js similarity index 80% rename from public/app.js rename to server/public/app.js index f84fecb..8e9f49a 100644 --- a/public/app.js +++ b/server/public/app.js @@ -6,6 +6,7 @@ const state = { windows: [], focusedId: null, selectedEntry: null, + contextMenu: null, jobs: new Map(), nextWindowId: 1, zIndex: 20 @@ -106,6 +107,9 @@ function render() { for (const win of state.windows) { workspace.appendChild(renderWindow(win)); } + if (state.contextMenu) { + workspace.appendChild(renderContextMenu()); + } } function renderWindow(win) { @@ -181,6 +185,7 @@ function renderExplorer(body, win) { const dropZone = body.querySelector('.drop-zone'); dropZone.addEventListener('dragover', (event) => { event.preventDefault(); + event.dataTransfer.dropEffect = event.altKey ? 'copy' : 'move'; data.dragOver = true; dropZone.classList.add('drag-over'); }); @@ -194,6 +199,10 @@ function renderExplorer(body, win) { dropZone.classList.remove('drag-over'); await handleDrop(event, win, data.path); }); + dropZone.addEventListener('contextmenu', (event) => { + event.preventDefault(); + showContextMenu(event, win, null); + }); for (const row of body.querySelectorAll('.file-row')) { const entry = data.entries.find((item) => item.path === row.dataset.path); @@ -205,7 +214,12 @@ function renderExplorer(body, win) { }); row.addEventListener('dragstart', (event) => { event.dataTransfer.setData('application/x-u-navigator-path', entry.path); + event.dataTransfer.setData('text/plain', entry.path); event.dataTransfer.effectAllowed = 'copyMove'; + row.classList.add('dragging'); + }); + row.addEventListener('dragend', () => { + row.classList.remove('dragging'); }); row.addEventListener('dragover', (event) => { if (entry.type === 'directory') { @@ -218,6 +232,11 @@ function renderExplorer(body, win) { event.preventDefault(); 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(); } +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) { const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`)); state.jobs.set(id, job); @@ -429,6 +535,12 @@ function parentPath(value) { 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) { const value = Number(bytes || 0); if (value < 1024) return `${value} B`; diff --git a/public/index.html b/server/public/index.html similarity index 100% rename from public/index.html rename to server/public/index.html diff --git a/public/styles.css b/server/public/styles.css similarity index 86% rename from public/styles.css rename to server/public/styles.css index 758e203..d2c50e2 100644 --- a/public/styles.css +++ b/server/public/styles.css @@ -197,32 +197,46 @@ .u-nav .file-table { border-collapse: collapse; + background: var(--surface); + color: var(--text); width: 100%; } .u-nav .file-table th, .u-nav .file-table td { + background: var(--surface) !important; border-bottom: 1px solid var(--line); + color: var(--text) !important; padding: 8px 7px; text-align: left; vertical-align: middle; } .u-nav .file-table th { + background: var(--surface-2) !important; color: var(--muted); font-size: 12px; font-weight: 600; } .u-nav .file-row { + background: var(--surface); + color: var(--text); cursor: default; } .u-nav .file-row:hover, -.u-nav .file-row.selected { +.u-nav .file-row.selected, +.u-nav .file-row.dragging { 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 { align-items: center; display: flex; @@ -241,6 +255,30 @@ 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 { display: grid; gap: 10px; diff --git a/server/server.js b/server/server.js index 6956997..10e45ce 100644 --- a/server/server.js +++ b/server/server.js @@ -9,7 +9,7 @@ import { Sandbox, rootsFromEnv } from './sandbox.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, '..'); -const publicDir = path.join(projectRoot, 'public'); +const publicDir = path.join(projectRoot, 'server', 'public'); const MIME = { '.html': 'text/html; charset=utf-8', diff --git a/u-navigator.plg b/u-navigator.plg index e38d937..747d188 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; -0cb8fe513addbd7435a8c79107ae7973 +8d05b440bd2ad3e20b8d900238642c3f