diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 3a2c117..f8c1e17 100644 Binary files a/webui/backend/data/tasks.db and b/webui/backend/data/tasks.db differ diff --git a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc index 44496a7..ca384f2 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index a20e9ed..68480d0 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -54,10 +54,17 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="upload-menu-popup"', body) self.assertIn('id="upload-folder-btn"', body) self.assertIn('id="upload-input"', body) - self.assertIn('id="upload-progress"', body) - self.assertIn('id="upload-target"', body) - self.assertIn('id="upload-current-file"', body) - self.assertIn('id="upload-count"', body) + self.assertIn('id="upload-menu"', body) + self.assertIn('id="upload-menu-toggle"', body) + self.assertIn('id="upload-menu-popup"', body) + self.assertIn('id="upload-folder-btn"', body) + self.assertIn('id="upload-modal"', body) + self.assertIn('id="upload-modal-target"', body) + self.assertIn('id="upload-modal-current-file"', body) + self.assertIn('id="upload-modal-progress-bar"', body) + self.assertIn('id="upload-modal-count"', body) + self.assertIn('id="upload-modal-status"', body) + self.assertIn('id="upload-modal-cancel-btn"', body) self.assertIn('id="settings-btn"', body) self.assertIn('id="rename-btn"', body) self.assertIn('id="view-btn"', body) @@ -210,13 +217,15 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('function openUploadPicker()', app_js) self.assertIn('function ensureFolderUploadPicker()', app_js) self.assertIn('function openFolderPicker()', app_js) + self.assertIn('function uploadModalElements()', app_js) + self.assertIn('function setUploadModalVisible(', app_js) + self.assertIn('function updateUploadModalDisplay(', app_js) self.assertIn('function buildFolderUploadPlan(files, targetPath)', app_js) self.assertIn('function folderDirectoryPaths(plan)', app_js) self.assertIn('async function ensureFolderDirectoryExists(path)', app_js) self.assertIn('async function executeFolderUploadPlan(plan)', app_js) self.assertIn('async function handleFolderSelection(event)', app_js) self.assertIn('input.setAttribute("webkitdirectory", "")', app_js) - self.assertIn('Folder upload to: ${plan.targetPath}/${plan.rootFolderName}', app_js) self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js) self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js) self.assertIn('Folder upload: preparing', app_js) diff --git a/webui/html/app.js b/webui/html/app.js index a2363d8..1ff42c8 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -68,6 +68,7 @@ let uploadState = { skippedCount: 0, cancelled: false, conflictResolver: null, + cancelRequested: false, }; let folderUploadPlanState = { targetPane: "left", @@ -331,13 +332,69 @@ function uploadElements() { menuPopup: document.getElementById("upload-menu-popup"), folderButton: document.getElementById("upload-folder-btn"), input: document.getElementById("upload-input"), - progress: document.getElementById("upload-progress"), - target: document.getElementById("upload-target"), - currentFile: document.getElementById("upload-current-file"), - count: document.getElementById("upload-count"), }; } +function uploadModalElements() { + return { + overlay: document.getElementById("upload-modal"), + target: document.getElementById("upload-modal-target"), + currentFile: document.getElementById("upload-modal-current-file"), + count: document.getElementById("upload-modal-count"), + progressBar: document.getElementById("upload-modal-progress-bar"), + status: document.getElementById("upload-modal-status"), + cancelButton: document.getElementById("upload-modal-cancel-btn"), + }; +} + +function setUploadModalVisible(visible) { + const elements = uploadModalElements(); + if (!elements.overlay) { + return; + } + elements.overlay.classList.toggle("hidden", !visible); +} + +function updateUploadModalDisplay(info) { + const elements = uploadModalElements(); + if (!elements.overlay) { + return; + } + const total = info.total || 0; + elements.target.textContent = `Uploading to: ${info.targetPath}`; + elements.currentFile.textContent = info.currentFileName + ? `Current file: ${info.currentFileName}` + : info.rootFolderName + ? `Folder: ${info.rootFolderName}` + : "Preparing files"; + elements.count.textContent = total ? `${Math.min(info.index, total)}/${total} files` : ""; + const percent = total ? Math.min(100, Math.round((info.index / total) * 100)) : 0; + elements.progressBar.style.width = `${percent}%`; + if (info.statusText) { + elements.status.textContent = info.statusText; + } else if (!elements.status.textContent) { + elements.status.textContent = ""; + } + elements.cancelButton.disabled = !uploadState.active; +} + +function setUploadModalStatus(msg) { + const elements = uploadModalElements(); + if (!elements.overlay) { + return; + } + elements.status.textContent = msg || ""; +} + +function requestUploadCancel() { + uploadState.cancelRequested = true; + const elements = uploadModalElements(); + if (elements.cancelButton) { + elements.cancelButton.disabled = true; + } + setUploadModalStatus("Cancel requested; finishing current file..."); +} + function ensureFolderUploadPicker() { if (folderUploadPickerInput) { return folderUploadPickerInput; @@ -477,10 +534,6 @@ function createButton(text, onClick) { return button; } -function setUploadProgressVisible(visible) { - uploadElements().progress.classList.toggle("hidden", !visible); -} - function closeUploadMenu() { const elements = uploadElements(); if (!elements.menuPopup || !elements.menuToggle) { @@ -512,21 +565,22 @@ function resetUploadProgress() { uploadState.skippedCount = 0; uploadState.cancelled = false; uploadState.conflictResolver = null; + uploadState.cancelRequested = false; elements.button.disabled = false; - elements.target.textContent = ""; - elements.currentFile.textContent = ""; - elements.count.textContent = ""; - setUploadProgressVisible(false); + setUploadModalVisible(false); + setUploadModalStatus(""); } function showFolderUploadPlan(plan) { - const elements = uploadElements(); folderUploadPlanState = plan; - elements.button.disabled = false; - elements.target.textContent = `Upload to: ${plan.targetPath}`; - elements.currentFile.textContent = `Folder: ${plan.rootFolderName}`; - elements.count.textContent = `${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"}${plan.subfolderCount > 0 ? ` • ${plan.subfolderCount} subfolder${plan.subfolderCount === 1 ? "" : "s"}` : ""}`; - setUploadProgressVisible(true); + updateUploadModalDisplay({ + targetPath: `${plan.targetPath}/${plan.rootFolderName}`, + rootFolderName: plan.rootFolderName, + total: plan.fileCount, + index: 0, + }); + setUploadModalVisible(true); + setUploadModalStatus("Preparing folder upload..."); } function updateUploadProgress() { @@ -685,15 +739,6 @@ async function ensureFolderDirectoryExists(path) { await apiRequest("GET", `/api/browse?${new URLSearchParams({ path }).toString()}`); } -function updateFolderUploadProgress(plan, currentName, index) { - const elements = uploadElements(); - elements.target.textContent = `Folder upload to: ${plan.targetPath}/${plan.rootFolderName}`; - elements.currentFile.textContent = currentName ? `Current file: ${currentName}` : `Folder: ${plan.rootFolderName}`; - elements.count.textContent = `${Math.min(index + 1, plan.fileCount)}/${plan.fileCount} files`; - elements.button.disabled = true; - setUploadProgressVisible(true); -} - async function executeFolderUploadPlan(plan) { uploadState.active = true; uploadState.targetPath = `${plan.targetPath}/${plan.rootFolderName}`; @@ -710,11 +755,20 @@ async function executeFolderUploadPlan(plan) { try { const directories = folderDirectoryPaths(plan); for (const directoryPath of directories) { + if (uploadState.cancelRequested) { + uploadState.cancelled = true; + break; + } await ensureFolderDirectoryExists(directoryPath); } + setUploadModalStatus(""); outer: for (let index = 0; index < plan.entries.length; index += 1) { + if (uploadState.cancelRequested) { + uploadState.cancelled = true; + break; + } const entry = plan.entries[index]; const relativeParts = entry.relativePath.split("/").filter(Boolean); const fileName = relativeParts[relativeParts.length - 1]; @@ -724,7 +778,13 @@ async function executeFolderUploadPlan(plan) { : `${plan.targetPath}/${plan.rootFolderName}`; uploadState.index = index; - updateFolderUploadProgress(plan, entry.relativePath, index); + updateUploadModalDisplay({ + targetPath: `${plan.targetPath}/${plan.rootFolderName}`, + rootFolderName: plan.rootFolderName, + total: plan.fileCount, + index: index + 1, + currentFileName: entry.relativePath, + }); let overwrite = uploadState.overwriteAll; while (true) { try { @@ -758,6 +818,7 @@ async function executeFolderUploadPlan(plan) { uploadState.skippedCount += 1; break; } + uploadState.cancelRequested = true; uploadState.cancelled = true; break outer; } @@ -822,14 +883,31 @@ async function handleUploadSelection(event) { uploadState.successfulCount = 0; uploadState.skippedCount = 0; uploadState.cancelled = false; + uploadState.cancelRequested = false; setError("actions-error", ""); - updateUploadProgress(); + setUploadModalVisible(true); + updateUploadModalDisplay({ + targetPath, + rootFolderName: "", + total: files.length, + index: 0, + }); try { outer: for (let index = 0; index < files.length; index += 1) { + if (uploadState.cancelRequested) { + uploadState.cancelled = true; + break; + } uploadState.index = index; - updateUploadProgress(); + updateUploadModalDisplay({ + targetPath, + rootFolderName: "", + total: files.length, + index: index + 1, + currentFileName: files[index].name, + }); let overwrite = uploadState.overwriteAll; while (true) { try { @@ -863,6 +941,7 @@ async function handleUploadSelection(event) { uploadState.skippedCount += 1; break; } + uploadState.cancelRequested = true; uploadState.cancelled = true; break outer; } @@ -883,6 +962,7 @@ async function handleUploadSelection(event) { await loadBrowsePane(state.activePane); } setActionError("Upload", err); + setUploadModalStatus(err.message); } finally { resetUploadProgress(); } @@ -3309,6 +3389,10 @@ function setupEvents() { document.getElementById("move-btn").onclick = openF6Flow; document.getElementById("mkdir-btn").onclick = createFolderForActivePane; uploadElements().input.onchange = handleUploadSelection; + const modalCancel = uploadModalElements().cancelButton; + if (modalCancel) { + modalCancel.onclick = requestUploadCancel; + } document.addEventListener("click", (event) => { const elements = uploadElements(); if (!elements.menu || elements.menu.contains(event.target)) { diff --git a/webui/html/base.css b/webui/html/base.css index d8a4329..2448b44 100644 --- a/webui/html/base.css +++ b/webui/html/base.css @@ -656,6 +656,33 @@ button:disabled { box-shadow: var(--shadow-elevated); } +#upload-modal .popup-card { + max-width: 320px; + padding: 12px 14px; + text-align: left; +} + +.upload-modal-progress { + width: 100%; + height: 4px; + border-radius: 999px; + background: var(--color-border); + margin: 6px 0; + overflow: hidden; +} + +.upload-modal-progress-bar { + height: 100%; + width: 0; + background: var(--color-accent); + transition: width 150ms ease; +} + +.upload-modal-count { + font-size: 12px; + color: var(--color-text-muted); +} + .popup-meta { color: var(--color-text-muted); font-size: 12px; diff --git a/webui/html/index.html b/webui/html/index.html index 47b9fca..ae00d78 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -104,11 +104,6 @@ -
@@ -170,6 +165,21 @@ +