feat: voortgang copy/duplicate/move in headerbar

This commit is contained in:
kodi
2026-03-15 11:40:21 +01:00
parent 9d5fb5a0c9
commit 73b09d2802
24 changed files with 1104 additions and 2 deletions
+178 -2
View File
@@ -114,6 +114,16 @@ let settingsState = {
selectedColorMode: "dark",
zipDownloadLimits: null,
};
let headerTaskState = {
activeItems: [],
popoverOpen: false,
pollTimer: null,
lastRenderKey: "",
};
// The header chip reflects only user-visible file actions that currently use the shared task system.
// Delete stays out of this set because it still runs as a direct request flow, not as a backend task.
const ACTIVE_TASK_OPERATIONS = new Set(["copy", "move", "duplicate"]);
const ACTIVE_TASK_STATUSES = new Set(["queued", "running"]);
const VALID_THEME_FAMILIES = [
"default",
"macos-soft",
@@ -189,6 +199,17 @@ function setStatus(msg) {
document.getElementById("status").textContent = msg;
}
function headerTaskElements() {
return {
container: document.getElementById("header-task-chip-container"),
chipButton: document.getElementById("header-task-chip-btn"),
chipLabel: document.getElementById("header-task-chip-label"),
popover: document.getElementById("header-task-popover"),
popoverList: document.getElementById("header-task-popover-list"),
logsButton: document.getElementById("header-task-logs-btn"),
};
}
function setError(id, msg) {
if (id === "actions-error") {
document.getElementById(id).textContent = "";
@@ -1266,7 +1287,7 @@ async function uploadFileRequest(targetPath, file, overwrite = false) {
async function refreshTasksSnapshot() {
try {
const data = await apiRequest("GET", "/api/tasks");
state.lastTaskCount = Array.isArray(data.items) ? data.items.length : state.lastTaskCount;
applyTaskSnapshot(data.items);
} catch (_) {
// Task list panel is not visible in current UI; silently keep flow stable.
}
@@ -3865,6 +3886,138 @@ function formatTaskLine(task) {
};
}
function isActiveTask(task) {
return Boolean(task) && ACTIVE_TASK_OPERATIONS.has(task.operation) && ACTIVE_TASK_STATUSES.has(task.status);
}
function activeTasksFromItems(items) {
return Array.isArray(items) ? items.filter((task) => isActiveTask(task)) : [];
}
function activeTaskChipLabel(count) {
return `${count} active task${count === 1 ? "" : "s"}`;
}
function headerTaskRenderKey(items) {
return JSON.stringify(
Array.isArray(items)
? items.map((task) => ({
id: task.id || "",
operation: task.operation || "",
status: task.status || "",
source: task.source || "",
destination: task.destination || "",
done_items: task.done_items,
total_items: task.total_items,
current_item: task.current_item || "",
}))
: []
);
}
function shouldPollHeaderTasks() {
return headerTaskState.popoverOpen || headerTaskState.activeItems.length > 0;
}
function stopHeaderTaskPolling() {
if (headerTaskState.pollTimer) {
window.clearTimeout(headerTaskState.pollTimer);
headerTaskState.pollTimer = null;
}
}
function scheduleHeaderTaskPolling() {
stopHeaderTaskPolling();
if (!shouldPollHeaderTasks()) {
return;
}
headerTaskState.pollTimer = window.setTimeout(async () => {
await refreshTasksSnapshot();
scheduleHeaderTaskPolling();
}, 1500);
}
function setHeaderTaskPopoverOpen(nextOpen) {
const elements = headerTaskElements();
const open = Boolean(nextOpen) && headerTaskState.activeItems.length > 0;
headerTaskState.popoverOpen = open;
if (elements.chipButton) {
elements.chipButton.setAttribute("aria-expanded", open ? "true" : "false");
}
if (elements.popover) {
elements.popover.classList.toggle("hidden", !open);
}
scheduleHeaderTaskPolling();
}
function renderHeaderTaskPopover(items) {
const elements = headerTaskElements();
if (!elements.popoverList) {
return;
}
const renderKey = headerTaskRenderKey(items);
if (headerTaskState.lastRenderKey === renderKey) {
return;
}
const scrollTop = elements.popoverList.scrollTop;
elements.popoverList.innerHTML = "";
if (!Array.isArray(items) || items.length === 0) {
const empty = document.createElement("div");
empty.className = "header-task-item-empty";
empty.textContent = "No active tasks right now.";
elements.popoverList.append(empty);
headerTaskState.lastRenderKey = renderKey;
return;
}
for (const task of items) {
const line = formatTaskLine(task);
const row = document.createElement("div");
row.className = "header-task-item";
const title = document.createElement("div");
title.className = "header-task-item-title";
title.textContent = line.title;
const path = document.createElement("div");
path.className = "header-task-item-path";
path.textContent = line.path;
const meta = document.createElement("div");
meta.className = "header-task-item-meta";
meta.textContent = line.meta;
row.append(title, path, meta);
elements.popoverList.append(row);
}
headerTaskState.lastRenderKey = renderKey;
elements.popoverList.scrollTop = scrollTop;
}
function renderHeaderTaskChip(items) {
const elements = headerTaskElements();
if (!elements.container || !elements.chipLabel) {
return;
}
const hasActiveTasks = Array.isArray(items) && items.length > 0;
elements.container.classList.toggle("hidden", !hasActiveTasks);
elements.chipLabel.textContent = activeTaskChipLabel(items.length);
if (!hasActiveTasks) {
headerTaskState.lastRenderKey = "";
setHeaderTaskPopoverOpen(false);
return;
}
renderHeaderTaskPopover(items);
}
function updateHeaderTaskState(taskItems) {
headerTaskState.activeItems = activeTasksFromItems(taskItems);
renderHeaderTaskChip(headerTaskState.activeItems);
scheduleHeaderTaskPolling();
}
function applyTaskSnapshot(taskItems) {
const items = Array.isArray(taskItems) ? taskItems : [];
state.lastTaskCount = items.length;
updateHeaderTaskState(items);
return items;
}
function renderHistoryItems(items) {
const elements = settingsElements();
const renderKey = JSON.stringify(Array.isArray(items) ? items : []);
@@ -3957,7 +4110,7 @@ async function loadHistoryForSettings() {
async function loadTasksForSettings() {
const data = await apiRequest("GET", "/api/tasks");
renderTaskItems(data.items || []);
renderTaskItems(applyTaskSnapshot(data.items));
settingsState.tasksLoaded = true;
}
@@ -4517,6 +4670,11 @@ function handleKeyboardShortcuts(event) {
if (!shouldHandleShortcut(event.target)) {
return;
}
if (event.key === "Escape" && headerTaskState.popoverOpen) {
event.preventDefault();
setHeaderTaskPopoverOpen(false);
return;
}
const isInfoShortcut = event.key === "Enter" && !event.shiftKey && !event.altKey && (event.metaKey || event.ctrlKey);
if (isInfoShortcut) {
@@ -4627,6 +4785,19 @@ function setupEvents() {
setupPaneEvents("right");
document.addEventListener("keydown", handleKeyboardShortcuts);
document.getElementById("theme-toggle").onclick = toggleTheme;
const headerTasks = headerTaskElements();
if (headerTasks.chipButton) {
headerTasks.chipButton.onclick = (event) => {
event.stopPropagation();
setHeaderTaskPopoverOpen(!headerTaskState.popoverOpen);
};
}
if (headerTasks.logsButton) {
headerTasks.logsButton.onclick = () => {
setHeaderTaskPopoverOpen(false);
openSettings("logs");
};
}
document.getElementById("upload-btn").onclick = openUploadPicker;
document.getElementById("upload-menu-toggle").onclick = (event) => {
event.stopPropagation();
@@ -4715,6 +4886,10 @@ function setupEvents() {
} else {
closeUploadMenu();
}
const headerTaskContainer = headerTaskElements().container;
if (headerTaskContainer && !headerTaskContainer.contains(event.target)) {
setHeaderTaskPopoverOpen(false);
}
const contextMenu = contextMenuElements().menu;
if (contextMenu && !contextMenu.contains(event.target)) {
closeContextMenu();
@@ -4914,6 +5089,7 @@ async function init() {
paneState("right").currentPath = "/Volumes";
await loadBrowsePane("right");
}
await refreshTasksSnapshot();
}
init();