Add sliding transfers panel

This commit is contained in:
Mikei386
2026-06-23 11:54:35 +02:00
parent 6b88fdb633
commit 25fa0ce644
7 changed files with 201 additions and 27 deletions
+1 -1
View File
@@ -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"
Binary file not shown.
Binary file not shown.
@@ -16,7 +16,7 @@ $version = "__VERSION__";
<nav class="toolbar" aria-label="Workspace actions">
<button id="newExplorerButton" title="Explorer oeffnen">Explorer</button>
<button id="newPropertiesButton" title="Eigenschaften oeffnen">Eigenschaften</button>
<button id="newQueueButton" title="Transfer-Queue oeffnen">Transfers</button>
<button id="newQueueButton" title="Transfers einblenden" aria-expanded="false">Transfers</button>
</nav>
</header>
<main id="workspace" class="workspace" aria-label="U-Navigator Workspace"></main>
+117 -23
View File
@@ -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) => `
<article class="job">
@@ -377,7 +381,7 @@ function renderQueue(body) {
</article>
`).join('') : '<p class="muted">Noch keine Debug-Einträge.</p>';
body.innerHTML = `
return `
<div class="queue-list">${jobMarkup}</div>
<section class="debug-log" aria-label="Debug-Protokoll">
<header>
@@ -387,12 +391,105 @@ function renderQueue(body) {
${debugMarkup}
</section>
`;
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 = `
<header class="transfer-panel-header">
<strong>Transfers</strong>
<button type="button" class="icon" data-action="close-transfers" title="Schließen">×</button>
</header>
<div class="transfer-panel-body">${queueMarkup()}</div>
`;
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) {
+80
View File
@@ -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;
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
<!DOCTYPE PLUGIN [
<!ENTITY name "u-navigator">
<!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.23.r022">
<!ENTITY version "2026.06.23.r023">
<!ENTITY package "&name;-&version;.tgz">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
<!ENTITY support "https://git.casaderoll.de/michael/Unraid-Navigator">
@@ -24,7 +24,7 @@
<FILE Name="/boot/config/plugins/&name;/&package;">
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
<MD5>84c66337fdd03108c2c04690f88ee5b8</MD5>
<MD5>c57cb70f4d040d65869b6ffde469b18a</MD5>
</FILE>
<FILE Run="/bin/bash">