Add sliding transfers panel
This commit is contained in:
+1
-1
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user