From f092007998150be41d0f5de1cc39d58973807191 Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 14 Mar 2026 07:28:31 +0100 Subject: [PATCH] feat: upload progressbar --- webui/backend/data/tasks.db | Bin 147456 -> 159744 bytes .../test_ui_smoke_golden.cpython-313.pyc | Bin 26973 -> 27592 bytes .../tests/golden/test_ui_smoke_golden.py | 19 ++- webui/html/app.js | 144 ++++++++++++++---- webui/html/base.css | 27 ++++ webui/html/index.html | 20 ++- 6 files changed, 170 insertions(+), 40 deletions(-) diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 3a2c117ceb69b421223f0102ee02a3e757fdd5b2..f8c1e17ce976c13128ffdcdda861de739c152cc7 100644 GIT binary patch delta 4675 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 delta 1348 zcmX|BZA_JA7=EAU{W$OU1Biu4Ch;N%2osXo21l?#U-AQJHaO?Np^a>{BC#~1EjT0C z(h!cvymhwOk{_+{5wSd0KNM?oGbAv-(v>J21P&j%#Ma8}e$T1?JiFfezV7S3?)!Q6 zwu~z+ElSr)@jSeI!L?!x-Q!_9wf`ZnHUdligsyu*>Wm8y<{b-OmPp z?9YvpRpcBXB^<0Pru&_8pZt-0S{|4GP!g1Er9`=+3@M)}oytz7Mqi|`~{DOxXlYJ4Xi&rCOW1*W9R-2pM{Y z-c1^mWb?Z4j$9#iXnFFm@r*d6G{{ZdZ?r?buD>f4@-pjxj?Ad_YNA7tkFv|$8UAlG zWUQhEj{D|vDWvt06wRw$G7gCi(lVi(myJegT-~a=NU1nW>-D?l*X9kK(<|)<0%n+G z3Qm3tfx^qgBK2^1g5*HQEXjl?XYlxPkz~QX?IcaCj%Df5(=CSyNDa;iuAUZpT$Apc3UWEcp;E!NxVmop-!#CJS5bki90e?CS zO(W?qJ9~AJqzmFKwBpw0$(-L6b+Bxxic zuHrrfc`4?wXcEnJ+XK0t#hR1TSd*HMHIIQWjH#uYsI9uV^ngD#=2FSDU49sy;eHhF ziDE6=o@Y0#jbN)vTk#1O>a2kQ`&KlS%Lw?>;7a(vX%{o71+}w80{Ry6QekWgc{&#x zEO{Dv_9lWaPFdt3sGCJIA9NydC3Nn^r6lWjV~Slfc&WMe%?|A#P8glS2^Mw`OI#i; z-*nFs6OG20YG4v+dv{`ay!mo0^za1Qy%Zr?!J_kN&=MhHF#Cc!Y*&)kOIED=vC*Sd)N>r&Pw|~R4OcRVv?g(>R^{xA8TV?_Bc}= z7sMI!x_Mk!FgwkyQh_7abeV}p#5iRfGPW5{8>{pseON!LH|Z6+Q@iiz(JpGeQUcW7 pBI`p3ZV{Ou=&B>3g*)Vd0+lz~IVPD;8SR6)al4FAyNfo_e*hREp<)04 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 44496a713d89d628fb0c8bb4028a0cc4daf4d6d7..ca384f275f9235753ae5b9081b568298414c093f 100644 GIT binary patch delta 901 zcmZuvUr19?7~k0+cQrS+y3P^i+?_hN9QvRwtEDRq%fg4T9yFEf?sZ&PcVqXCZ1e{S z$`#hbM^sc=S(=q*oFJixf?j%DG13h+|Z~ywqv8VUI!^6f_;TnJ@o2G;7sRWlqB(Q(})`GdQGzU zE{ETDbU<%W73|)t=v_HHPb&OYj=z;qDgw(2A`vc-4sbRrYG<=STdxY7fiL70G=%#~ zFR3q@IL1dMVv#7#cuuo?f=ZgVDp7`xP)X~*l1HTDVOFG)6?sl55s$OH=o#c9?Em~& zJQ|L(g5VjT<5aS%EWbcB!Aq6%&lE&jObAr6Jbw$#huDZ`K;-cr>m5D1iPJW-8oKQz zkg6b-lvO*kR;fZ4iQ*fElp$?toMSS?ei(LGaUHd&-Sy46ld)+- zDEooAp_5RfQlYgDYI+f<__jcA)E+IdBbO z@4+Die|#i-XsLmREfl&3t3HS3eozOqZo6hoK{O5xtq#qDpdQ9r9f**Hh#tE?qeYO@)9yKGpevCF1Oh`8nR&YR_!gujM6k>$7!wWHg_g4OlO>RMOHbhQus GT=`!bEHJ15 delta 653 zcmX?co$>A^M!wIyyj%=Gko9Xz<`3VEeE*o4d;}+Hs%$o5;bz^ul2d|x@>)@m$yWtx zCdaahY)%#IV47^nr84=g@Px^nRsvv2T`2uh1j6SPh0rstcvBr}414F#+~nwf!t@iW8b{~Bt1lmF_8OkOJ}FxgjEWOA%Ig#HTUOA0~w zwh(&rTk|Bg&C~7b7@2!RStdK^Dl+#$=*^-I6^vk`7CNdivVx`NghJ@Kp=^@{oRk^a z!J_j+Il%OSP);ztFqCWZ0-!86n725T2ShV2nH=P-%egdoS@3d8raZpMan6dYOM_Pg zFQ2^5SrsV05-ct-$wi!*FL>4D1)>Tpq5Q$CK@6c#f#5Z!Oc38HFr+hRuHEe8(#XEq z$VY(D#@a1ECndE=0TL?;CHYngD%Ax!iFtaEs0c_b$*9$b3Kiw&mq66}CFZ8qZg%ud z5@4GHG`aZP=9RIZm?xh|jGEkf@ SFG^^BVE}VK_)K1tEdv1MK+CWI 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 @@ +