feat: voortgang copy/duplicate/move in headerbar
This commit is contained in:
+178
-2
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user