diff --git a/build-plugin.sh b/build-plugin.sh index cd8fe7c..819bef3 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r022" +VERSION="2026.06.23.r023" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r022.tgz b/packages/u-navigator-2026.06.23.r022.tgz deleted file mode 100644 index c0e65ed..0000000 Binary files a/packages/u-navigator-2026.06.23.r022.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r023.tgz b/packages/u-navigator-2026.06.23.r023.tgz new file mode 100644 index 0000000..68285e4 Binary files /dev/null and b/packages/u-navigator-2026.06.23.r023.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page index 5040f87..a71a65b 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page @@ -16,7 +16,7 @@ $version = "__VERSION__";
diff --git a/server/public/app.js b/server/public/app.js index 3df80c1..985a1a4 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -1,5 +1,7 @@ const workspace = document.querySelector('#workspace'); +const appShell = document.querySelector('.u-nav'); const statusText = document.querySelector('#statusText'); +const transferButton = document.querySelector('#newQueueButton'); const API_BASE = window.UNAV_API_BASE || '/api'; const APP_VERSION = window.UNAV_VERSION || 'dev'; const state = { @@ -17,6 +19,9 @@ const state = { jobRefresh: new Map(), sizeJobs: new Map(), sizeResults: new Map(), + transferPanelVisible: false, + transferPanelClosing: false, + transferAutoHideTimer: null, nextWindowId: 1, zIndex: 20 }; @@ -30,7 +35,7 @@ const icons = { document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer()); document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties()); -document.querySelector('#newQueueButton').addEventListener('click', () => openQueue()); +transferButton.addEventListener('click', () => toggleTransferPanel()); window.addEventListener('error', (event) => { recordDebug('window.error', { message: event.message, @@ -54,7 +59,6 @@ async function init() { } statusText.textContent = `${state.config.roots.length} Root, ${state.config.readOnly ? 'Read-only' : 'Schreibzugriff aktiv'} · ${APP_VERSION}`; openExplorer(state.config.roots[0].path); - openQueue(); } catch (error) { statusText.textContent = error.message; } @@ -105,13 +109,8 @@ function openProperties(entry = state.selectedEntry) { } function openQueue() { - return createWindow('Transfers', 'queue', { - width: 440, - height: 340, - x: 250, - y: 130, - data: {} - }); + showTransferPanel(); + return null; } function createWindow(title, kind, options) { @@ -142,6 +141,7 @@ function render() { if (state.contextMenu) { workspace.appendChild(renderContextMenu()); } + renderTransferPanel(); } function renderWindow(win) { @@ -182,7 +182,6 @@ function renderWindow(win) { body.className = 'window-body'; if (win.kind === 'explorer') renderExplorer(body, win); if (win.kind === 'properties') renderProperties(body); - if (win.kind === 'queue') renderQueue(body); body.addEventListener('scroll', () => { if (win.data.lockScroll) { body.scrollTop = win.data.lockScrollTop || 0; @@ -361,6 +360,11 @@ function renderProperties(body) { } function renderQueue(body) { + body.innerHTML = queueMarkup(); + bindQueueActions(body); +} + +function queueMarkup() { const jobs = [...state.jobs.values()].reverse(); const jobMarkup = jobs.length ? jobs.map((job) => `
@@ -377,7 +381,7 @@ function renderQueue(body) {
`).join('') : '

Noch keine Debug-Einträge.

'; - body.innerHTML = ` + return `
${jobMarkup}
@@ -387,12 +391,105 @@ function renderQueue(body) { ${debugMarkup}
`; - body.querySelector('[data-action="clear-debug"]')?.addEventListener('click', () => { +} + +function bindQueueActions(root) { + root.querySelector('[data-action="clear-debug"]')?.addEventListener('click', () => { state.debug = []; - render(); + updateTransferPanel(); }); } +function toggleTransferPanel() { + if (state.transferPanelVisible) { + hideTransferPanel(); + } else { + showTransferPanel(); + } +} + +function showTransferPanel() { + if (state.transferAutoHideTimer) { + clearTimeout(state.transferAutoHideTimer); + state.transferAutoHideTimer = null; + } + state.transferPanelVisible = true; + state.transferPanelClosing = false; + renderTransferPanel(); +} + +function hideTransferPanel() { + if (!state.transferPanelVisible && !state.transferPanelClosing) { + return; + } + if (state.transferAutoHideTimer) { + clearTimeout(state.transferAutoHideTimer); + state.transferAutoHideTimer = null; + } + state.transferPanelVisible = false; + state.transferPanelClosing = true; + renderTransferPanel(); + setTimeout(() => { + if (!state.transferPanelVisible) { + state.transferPanelClosing = false; + renderTransferPanel(); + } + }, 220); +} + +function scheduleTransferAutoHide() { + if (!state.transferPanelVisible || activeTransferCount() > 0 || state.transferAutoHideTimer) { + return; + } + state.transferAutoHideTimer = setTimeout(() => { + state.transferAutoHideTimer = null; + if (activeTransferCount() === 0) { + hideTransferPanel(); + } + }, 2200); +} + +function activeTransferCount() { + return [...state.jobs.values()].filter((job) => job.type !== 'size' && ['queued', 'running', 'cancel_requested'].includes(job.status)).length; +} + +function renderTransferPanel() { + const existing = appShell.querySelector('.transfer-panel'); + if (!state.transferPanelVisible && !state.transferPanelClosing) { + existing?.remove(); + transferButton.classList.remove('active'); + transferButton.setAttribute('aria-expanded', 'false'); + return; + } + + const panel = existing || document.createElement('aside'); + panel.className = `transfer-panel ${state.transferPanelClosing ? 'closing' : 'open'}`; + panel.setAttribute('aria-label', 'Transfers'); + panel.innerHTML = ` +
+ Transfers + +
+
${queueMarkup()}
+ `; + if (!existing) { + appShell.appendChild(panel); + } + + const buttonRect = transferButton.getBoundingClientRect(); + const shellRect = appShell.getBoundingClientRect(); + const panelWidth = Math.min(520, Math.max(360, shellRect.width - 32)); + const right = Math.max(16, shellRect.right - buttonRect.right); + panel.style.width = `${panelWidth}px`; + panel.style.right = `${right}px`; + panel.style.top = `${buttonRect.bottom - shellRect.top + 8}px`; + + panel.querySelector('[data-action="close-transfers"]').addEventListener('click', () => hideTransferPanel()); + bindQueueActions(panel); + transferButton.classList.add('active'); + transferButton.setAttribute('aria-expanded', 'true'); +} + async function loadExplorer(win, targetPath) { win.data.loading = true; win.data.error = null; @@ -453,8 +550,9 @@ async function createJob(type, source, destination, refreshWindowIds = []) { windowIds: [...new Set(refreshWindowIds)].filter(Boolean), done: false }); + showTransferPanel(); pollJob(job.id); - render(); + updateTransferPanel(); return job; } @@ -622,13 +720,14 @@ async function pollJob(id) { try { const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`)); state.jobs.set(id, job); - render(); + updateTransferPanel(); if (['queued', 'running', 'cancel_requested'].includes(job.status)) { setTimeout(() => pollJob(id), 700); } else if (job.type === 'size') { finishSizeJob(job); } else { await refreshAfterJob(job); + scheduleTransferAutoHide(); } } catch (error) { recordDebug('job.poll.error', { id, error: serializeError(error) }); @@ -1109,17 +1208,12 @@ function recordDebug(label, data = {}) { if (state.debug.length > 80) { state.debug.splice(0, state.debug.length - 80); } - updateQueueWindows(); + updateTransferPanel(); return item; } -function updateQueueWindows() { - for (const win of state.windows.filter((item) => item.kind === 'queue')) { - const body = workspace.querySelector(`.window[data-window-id="${win.id}"] .window-body`); - if (body) { - renderQueue(body); - } - } +function updateTransferPanel() { + renderTransferPanel(); } function serializeError(error) { diff --git a/server/public/styles.css b/server/public/styles.css index 9f710ab..fd3f8b6 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -14,6 +14,8 @@ font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; grid-template-rows: auto 1fr; min-height: calc(100vh - 170px); + overflow: hidden; + position: relative; } @media (prefers-color-scheme: dark) { @@ -88,6 +90,11 @@ gap: 8px; } +.u-nav .toolbar button.active { + background: var(--surface-2); + border-color: var(--accent); +} + .u-nav .workspace { min-height: 0; overflow: hidden; @@ -472,6 +479,71 @@ border-color: var(--line); } +.u-nav .transfer-panel { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: var(--shadow); + display: grid; + grid-template-rows: auto minmax(0, 1fr); + max-height: min(520px, calc(100% - 78px)); + min-height: 220px; + min-width: 320px; + overflow: hidden; + position: absolute; + transform-origin: top right; + z-index: 10000; +} + +.u-nav .transfer-panel.open { + animation: transfer-slide-down 180ms ease-out both; +} + +.u-nav .transfer-panel.closing { + animation: transfer-slide-up 180ms ease-in both; + pointer-events: none; +} + +.u-nav .transfer-panel-header { + align-items: center; + background: var(--surface-2); + border-bottom: 1px solid var(--line); + display: flex; + justify-content: space-between; + min-height: 40px; + padding: 5px 8px 5px 12px; +} + +.u-nav .transfer-panel-body { + min-height: 0; + overflow: auto; + padding: 10px; +} + +@keyframes transfer-slide-down { + from { + opacity: 0; + transform: translateY(-18px) scaleY(0.96); + } + + to { + opacity: 1; + transform: translateY(0) scaleY(1); + } +} + +@keyframes transfer-slide-up { + from { + opacity: 1; + transform: translateY(0) scaleY(1); + } + + to { + opacity: 0; + transform: translateY(-18px) scaleY(0.96); + } +} + .u-nav .queue-list { display: grid; gap: 10px; @@ -580,4 +652,12 @@ .u-nav .resize-handle { display: none; } + + .u-nav .transfer-panel { + left: 10px !important; + max-height: min(70vh, 520px); + min-width: 0; + right: 10px !important; + width: auto !important; + } } diff --git a/u-navigator.plg b/u-navigator.plg index 3017996..b5a5892 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + @@ -24,7 +24,7 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -84c66337fdd03108c2c04690f88ee5b8 +c57cb70f4d040d65869b6ffde469b18a