From 8af4b1a6b0b2e23dbab98ebb4f75df1c9f6596b7 Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 14 Mar 2026 14:58:07 +0100 Subject: [PATCH] feat: B4 - progressbar bij single file --- webui/backend/data/tasks.db | Bin 221184 -> 221184 bytes .../test_ui_smoke_golden.cpython-313.pyc | Bin 41518 -> 42844 bytes .../tests/golden/test_ui_smoke_golden.py | 12 +++ webui/html/app.js | 69 ++++++++++++++++-- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 59d91599b16250b4aff1720baa353830f53d88b6..c4ca545fdf84f3765d36c1ccec3b0eee4f69f884 100644 GIT binary patch delta 290 zcmZoTz}s+ucY-wIu8A_vjJp~Wwk9wxxz89po%I1@G!xUU=?M=QeW$N~z}U;UaI>Mn zTt+=j=1?Xh6N^N1V`F1oqog!rU6VxPR9#DB!$e)nL`!3n6qD4nq~sJvS&+)FSP5N{oJ!4F$ZWuXAJ);xIL{GB%B3l-&;Gh3%8$ zWvpP}Tf4h8l!)>ABdnAb6tFjfFv62@rKSkWZQ6DljN9Np;8z+h@h vjH{-9U}4stp1{Q{GX20Ord%0gD~QEL21aJO2FALECLx9}vF#;H%ssIH1zl5y delta 139 zcmV;60CfL=zzu-F4UiiFxRD$~0l0x+wO|3H-vLsW1>gZ&mtx=nO##}MuHXTV0n4*7 z5XS)oDFamkm++7RB)2T%0oYswjsYwHw+cW49RUH9mnK01BLQfWF%V>zE>Z#yx4l6E tN4^{k0(1ZltPOAsoeS^^+X*NLAO~gz$^^Uvt^#zkF%VP&w{!vnj9qh}Eu;Vd 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 8ffe3018d5e1e1cade5d6ebd198ca0a06b4b29cb..528d7d638cb5943ca6c1256964d2398b626d87ae 100644 GIT binary patch delta 1587 zcmah}?@t?L9KYvU;rP*ABY51Hu%ka%JD?E4+=S_j8g&HXR^@xxCqLN?bYI&$#O2icf25xy;&fhaf$LLn6S9SXky}XPp`*BGn+i$&wai> zKHu;2eUjc;r+;}s@B7W=YNYV*^6b6L7gz7@V}@v>Pv?hzeHL{bYejEzZh|F@6X7!X zeiU9u+u>H!#8+ee82=hCn?C*4=>b5JBJI_nTt&Svat@HH8&NmO{4T1vP|q~)aEcBj zOnV*ASXgIaJwZRHMjI?|qlHZtHe2YbBD!_d>kNY!t9M?w4uBh#r~PQ@VgOa9eW1l; zpq1F?h~WLEN2a?V=wXG_6Ar>&Wn3Vm(2R$Po03 z4Fr8+BSF8|WFqKBJ8${HA;it}Hw3U^5TC>7%*-LedI-B}v0lRN%p3%L=)sI1gs|;u zabs`)5!!q+iV~VHT@n(hAAa9k-kqNzzaS5O7TzL)Tlpk?y4 z(S#FEvL|j|>o$q!EChw&r|+tk9U|8n(E@5hNkgQjeWeldsh)%Hsl!6t+_fNC%dSlY zrD4JMeoe!?Unqx{bS18}71V@UEL4j<)_73zXukgn;FqzrL!ppI^CxBWZQAGSO3KBY zHG=0VM;Ea6=Wfz|rP68oIB7GLHJG`FO!I>4 znroAbZlnxGL~MoEw<}YP%ocZi6?;a}_A*~Oy2=^MD^~6ncVy*VgL&DeuddHFN+Al~a*?nkR7#mcQz|KFsz&2MPeLVn z6Y(H#O4Q7TIBK=Hi23j0Cf0^?ke}?T?JWkm zXnPX`2TLg%^@h}-F<#;XH~lhhinryPrr4it5)y)qxh&~HYRE8BM@EnaGK!2LO{9fn z0acrapy|2LnmZw1b1NCOj~{4)YgixR;Xp|e+-g?s=MiXL$Pxp413ZeF;xU{Vgx%^X zbErtB56hcAOB)vatY$Dd(oNX^#O)?FwCDSKELk0e$&{;cChc%w6Wo?IbD9Rft|+du zpo|_9d-HcySiAI*jKFryfp@ivc(Bl<@bc;y*!2N8Ump=G^$wxgVYt>96!}IuLx1#& zdyno@Nckms(~$ES>;i)w|C3hOINs4cc)#XAq{XmxrnMY+5nY-x;P2zOHo7inCZMq7 zz?Ua!*lIIaUuEH`o#~m5UV*spz~vPNTdPcjo_$rxb@Baqi6k$E$vLvDe|B;ooZKG; L%e_e`ycqfiOGLnH diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index 61533ed..9fff341 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -229,14 +229,18 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('function closeFeedbackModal()', app_js) self.assertIn('function downloadModalElements()', app_js) self.assertIn('function isZipDownloadSelection(items)', app_js) + self.assertIn('function singleFileDownloadRequestKey(path)', app_js) self.assertIn('function archiveTaskStatusLabel(status)', app_js) self.assertIn('function archiveTaskCountText(task)', app_js) self.assertIn('function archiveTaskCurrentItemText(task)', app_js) self.assertIn('function archiveTaskProgressPercent(task)', app_js) self.assertIn('function openZipDownloadModal(selectedItems)', app_js) + self.assertIn('function openSingleFileDownloadModal(selectedItem)', app_js) self.assertIn('function markZipDownloadReady(fileName)', app_js) self.assertIn('function markZipDownloadFailed(err)', app_js) self.assertIn('function markZipDownloadCancelled()', app_js) + self.assertIn('function markSingleFileDownloadRequested(fileName, path)', app_js) + self.assertIn('function markSingleFileDownloadFailed(err, selectedItem)', app_js) self.assertIn('function closeDownloadModal()', app_js) self.assertIn('function zipDownloadRequestKey(paths)', app_js) self.assertIn('async function createArchiveDownloadTask(paths)', app_js) @@ -252,9 +256,14 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('async function downloadFileRequest(paths)', app_js) self.assertIn('const zipDownload = isZipDownloadSelection(selectedItems);', app_js) self.assertIn('openZipDownloadModal(selectedItems);', app_js) + self.assertIn('openSingleFileDownloadModal(selected);', app_js) + self.assertIn('const requestKey = zipDownload ? zipDownloadRequestKey(selectedPaths) : singleFileDownloadRequestKey(selected.path);', app_js) self.assertIn('targetText: "Archive download requested"', app_js) + self.assertIn('targetText: `File download requested: ${selectedItem.name}`', app_js) self.assertIn('statusText: "Requested"', app_js) + self.assertIn('statusText: "Requesting download..."', app_js) self.assertIn('countText: "Waiting for archive task"', app_js) + self.assertIn('countText: "Direct file download"', app_js) self.assertIn('targetText: "Archive download task"', app_js) self.assertIn('statusText: "Ready"', app_js) self.assertIn('countText: "Browser download requested"', app_js) @@ -266,6 +275,9 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('return `Current: ${task.current_item}`;', app_js) self.assertIn('downloadProgressState.requestKey === requestKey', app_js) self.assertIn('setStatus("Preparing download...");', app_js) + self.assertIn('setStatus("Requesting download...");', app_js) + self.assertIn('setStatus(zipDownload ? "Preparing download..." : "Requesting download...");', app_js) + self.assertIn('setStatus(`Download requested: ${anchor.download}`);', app_js) self.assertIn('"/api/files/download/archive-prepare"', app_js) self.assertIn('`/api/tasks/${encodeURIComponent(taskId)}`', app_js) self.assertIn('`/api/files/download/archive/${encodeURIComponent(taskId)}`', app_js) diff --git a/webui/html/app.js b/webui/html/app.js index 587cfc8..056feff 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -86,6 +86,7 @@ let downloadProgressState = { archiveLabel: "", totalItems: 0, requestKey: null, + mode: null, taskId: null, cancelRequested: false, }; @@ -377,6 +378,10 @@ function zipDownloadRequestKey(paths) { return paths.join("\n"); } +function singleFileDownloadRequestKey(path) { + return path; +} + function selectedItemCountLabel(totalItems) { return `${totalItems} selected item${totalItems === 1 ? "" : "s"}`; } @@ -465,6 +470,7 @@ function updateDownloadModalDisplay(info) { function openZipDownloadModal(selectedItems) { const requestPaths = selectedItems.map((item) => item.path); downloadProgressState.active = true; + downloadProgressState.mode = "archive"; downloadProgressState.archiveLabel = "ZIP archive"; downloadProgressState.totalItems = selectedItems.length; downloadProgressState.requestKey = zipDownloadRequestKey(requestPaths); @@ -483,6 +489,26 @@ function openZipDownloadModal(selectedItems) { }); } +function openSingleFileDownloadModal(selectedItem) { + downloadProgressState.active = true; + downloadProgressState.mode = "single_file"; + downloadProgressState.archiveLabel = selectedItem.name; + downloadProgressState.totalItems = 1; + downloadProgressState.requestKey = singleFileDownloadRequestKey(selectedItem.path); + downloadProgressState.taskId = null; + downloadProgressState.cancelRequested = false; + setDownloadModalVisible(true); + updateDownloadModalDisplay({ + active: true, + targetText: `File download requested: ${selectedItem.name}`, + currentFileText: `File: ${selectedItem.path}`, + countText: "Direct file download", + statusText: "Requesting download...", + percent: 0, + cancelVisible: false, + }); +} + function markZipDownloadReady(fileName) { downloadProgressState.active = false; downloadProgressState.cancelRequested = false; @@ -527,6 +553,33 @@ function markZipDownloadCancelled() { }); } +function markSingleFileDownloadRequested(fileName, path) { + downloadProgressState.active = false; + updateDownloadModalDisplay({ + active: false, + targetText: `File download requested: ${fileName}`, + currentFileText: `File: ${path}`, + countText: "Browser download requested", + statusText: "Download requested", + percent: 0, + cancelVisible: false, + }); + window.setTimeout(closeDownloadModal, 480); +} + +function markSingleFileDownloadFailed(err, selectedItem) { + downloadProgressState.active = false; + updateDownloadModalDisplay({ + active: false, + targetText: "File download failed", + currentFileText: `File: ${selectedItem.path}`, + countText: "Direct file download", + statusText: `Failed: ${err.message || "Download failed"}`, + percent: 0, + cancelVisible: false, + }); +} + function updateZipDownloadTaskProgress(task) { if (!downloadProgressState.active) { return; @@ -572,6 +625,7 @@ function closeDownloadModal() { if (downloadProgressState.active) { return; } + downloadProgressState.mode = null; downloadProgressState.archiveLabel = ""; downloadProgressState.totalItems = 0; downloadProgressState.requestKey = null; @@ -743,17 +797,20 @@ async function startDownloadSelected() { } const zipDownload = isZipDownloadSelection(selectedItems); const selectedPaths = selectedItems.map((item) => item.path); - const requestKey = zipDownloadRequestKey(selectedPaths); - if (zipDownload && downloadProgressState.active && downloadProgressState.requestKey === requestKey) { - setStatus("Preparing download..."); + const selected = selectedItems[0]; + const requestKey = zipDownload ? zipDownloadRequestKey(selectedPaths) : singleFileDownloadRequestKey(selected.path); + if (downloadProgressState.active && downloadProgressState.requestKey === requestKey) { + setStatus(zipDownload ? "Preparing download..." : "Requesting download..."); return; } if (zipDownload) { openZipDownloadModal(selectedItems); setStatus("Preparing download..."); + } else { + openSingleFileDownloadModal(selected); + setStatus("Requesting download..."); } try { - const selected = selectedItems[0]; if (zipDownload) { const created = await createArchiveDownloadTask(selectedPaths); downloadProgressState.taskId = created.task_id; @@ -778,7 +835,8 @@ async function startDownloadSelected() { anchor.click(); anchor.remove(); URL.revokeObjectURL(url); - setStatus(`Download started: ${anchor.download}`); + markSingleFileDownloadRequested(anchor.download, selected.path); + setStatus(`Download requested: ${anchor.download}`); } catch (err) { if (zipDownload) { if (err.code === "download_cancelled") { @@ -789,6 +847,7 @@ async function startDownloadSelected() { setStatus("Download failed"); } } else { + markSingleFileDownloadFailed(err, selected); setActionError("Download", err); } }