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