From e85e51d64af41e4cffda414e30db26e18606f7f5 Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 14 Mar 2026 15:29:50 +0100 Subject: [PATCH] polish --- webui/backend/data/tasks.db | Bin 225280 -> 229376 bytes .../test_ui_smoke_golden.cpython-313.pyc | Bin 43270 -> 44294 bytes .../tests/golden/test_ui_smoke_golden.py | 11 + webui/html/app.js | 280 ++++++++++++------ webui/html/index.html | 2 +- 5 files changed, 197 insertions(+), 96 deletions(-) diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 1697f4dfee8795cac94d41f397d5404142578793..96f9121f614fe8797d373553a2132e53072c8d7a 100644 GIT binary patch delta 4332 zcmbVPYitzP6`p%%pX_7I!#qqH+t}*|_L#Xd_s+vbjj;(-xKcNO8?qGe%+72{;>d2G zO4LfRXY)`JPy`{#O{G>!2~tx+BftsMKhTGiQmF`m6iOAT!9i_B`GW~UZQ4ro&g?FM zup6&x$v^gr7)Kq%k^6+OI$Laf99sr*m8$I;vBosbm3!E-o7CxMw z-|{OV+_97=fDRl6i^9XqlA#UjVPhEK9Cz~gIUp@$YmxMom~cfnE<7b5A(g%|jT$~m z)3a%i`C3z$R~mOYV@`gzWADh1kBvXM4gRwL5`@ugFahja~9gAZJg%{G_i2s<+0SdpKxEPt? zkHAabs}ta61auxi4Q-pd`4K1+%Q8)3iFrfSM64HxXc8ierfE2uW~&M|b?&8Lr)Da; zBqPzngos#1j;Nb*L6lU(A*ziCMke5(->6SR%-h*%{T)MT{I()RB`HWiyYgf~M$Lx4Avu z_29rAo&lW@%CvU|^vB=}F)#KCJW3y9zmo8~{1Lv$Z{hp+5Ir#io`>A$4C&2D zpP}SPgxIzuYATXM>|jOIF=B+-x-4s0k!%BG{GIi10_{Pr>$nxr+8u1I<61zA_wi-0 zGUcTs5aY9jesdP=^iIrzZ7lyc7^`amD`Fs1CXyo}WU{)jt-NR{1zXfi zm0gA{E<|o_cUsl;C-qKR?9{|LOIZT?*SS2OE?F5s#uhM%z&*7dYE^89c+k4 zL;vE&!CH$*p`UJoi|M*SxYJv^5xx&YvG=%T``0vj^GD!M`4A(qkCE6L zkPtj!2u|}SMq#LB(FU-9hbev=uki8kLyVsC+1STrP^9Yn;mb=%Yp#{j`&G_hvOrXq_9QILr9nkJfxQVPQHqtOZu5@pS1TQy8XT`5?scQ~x?VPe=e0iAUD^PtOX*$e;Lmi{&X zTxu8N^g-%RsejVgQTV#|^P})&6f9t@(qX#v6l|C*7ki&3DUzz@xVP_@_4r)pS2 za*o?ma|0%-s^_?uYi>ZQib;-p>5dIecfPnjqA7}^<+vC99rXM#-{_%J@SM9O^Z*wF z87}o`GMZ4~2l3y=`amZ9I^&T7tDjA}iA5ug%`S4gA>=q0G5H)C8mgzE)8D)cSJUld zppC8?WBHps#42(BR`$~!VAo{bnA)(oR5ThxcZ#^(@x#PeD(EBmM6c*bQXb zyb}zR5_|OuTrg4%Te%pknh;{ot2%5Iu~8$(aX?|PSFyqn+<5_P#q9}TD<#!n*JpHetJ(#qfJ@=2k9XgCeAyS3Px99hP4=l+1>2y3EqcWrt;$?u>80^d&mEbz%J?k~yU-FYjm zxgEuxvj2qdzaGxMf1O>0s-8UW> z+~q$pKT_6@6iD-zA_Z%1XR*7?Bjxj`L$m0)ByTY1x3+-hO_i!=Bgx(F+33X9TvF^R zyJsT?VPZMA&{fj3Z!3FbXU9N%&LC>BvmB(y4^odrR)VZq5BfIGL82Hbwx!Ct!rmgg z>^i;>Y75F;l-PKAPu|#C5f)p5kW3(gkaH_TO<^!Ipf!+#)=ERD8Z!ANmpOnMS`}9* zX3FVgu?ZXI=loEql*DKTtO!E+)6VE2FNhhKmB%GK4#BlA9= z{eSW2_7*$JH!vb97;yj_|# z__@1_twA&<7D2RnzRT|ri4vjvDqlMHTowyUw~g~}$L2Bb!))62_GaNhzsH86-g{f% H#_0b5Tdyre delta 824 zcmXw%T}YH^6vy9l&igSl_B_utTdrwsQt2)wHq&g%e$~wGHjyqdqqU0bkfKA{8Z)_p zK@d~qE{iGI2WJ=ZLW_u~FkO5?sK}dcO!Q(4B64z<@M14ULfE_Z-K4uehjV_M^Z%c0 zR%UbJe2Fz>7)JGA>28?1+j8xp57dHNFv``!`0C({8jO3Pj9=Abp}Dik=o_1b;0N-J zVapjuEvhlqqe4o^nemW+Qu3mNGSeL{9*h=lD6uH~;ZLcqRNL`X`=PdUSGxPq(R6#N ztMhQjalKW+^A?D*m4QmG&cFu#J_F_J)z5O5_jhUn#xDw<**Us}^Jd)lmG}qy^@R*9 z+i>z?$lt?nd*Bq*xpKdrJ^?j=u+iP21a$Z$%$c;uTw*5+ow_><2hEESe+9?;(A3Lk zLHz*rY~|o%uFS#brbl}QIOD1S-J&zpNR?#bDE@*;4D&<|dJ%?Q@(`>5UM@hnfCFsh zVOx>>L-xs5SuF)U6@S4IF)7Z&C2^Sd=b^)<9pr_Gp2$PeUGJC+Il2Pjt~71YH{U?R zcMimiL8vfzQv@GdQ}zvefEOe9?HV%hHBRFg{)VTp9cwTMF|LSW@q2%HAcg_%j^Y!} z*Q3KP64=252@LVE1Qv5c0@vwyGmaAb_u^VB^l6iE|DDuJG4y*KH>Q59G`KyEXVCmt zdonZvysH@lI#P%C&@7^zz>%A<25R-{gE&O`&Izh>CR)%e4a6WoC@4u`@>&Ud+8*Je zES7U5i@~63#(A|vdF*BElk;*y_B%teO@1c*;~nIzCMZ%9MJB-xMh~oH^_6f zv{r{6;OuGPo<;>#hUi@76jZq_pA_h-TADEnw|iO*^-SGXXH}C5Do4KjKS5p}r;qfP fahh}7M`>KiIn_v49Is0Pl{8e+rPM1 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 1583f279dd756119357e69d7da7e53a7ea06977f..011e571e409780a8990de095a411616d1a39e14c 100644 GIT binary patch delta 1330 zcmZWoZ%i9y9Pah+F=?;3Qm(~h?+!`})OO`ROeFgw3q>{!>w-vN*Y;LUu6MP)!l-PR zn2hMMsQjYfV3FvI7!!kDG!YXs{j%u3fHv9Uhkf#s#$?M(_Hn-Va$s~#?s?R^G6c{KjHf4W=5MoG-t6!{c;cj?%3HV_OF3=wREFD3$1 z6RDd4oefZ79W75M?0`n=ERlqRa5O;^K9r~+982sY+>&^LaBHGcXQ~Ze;G@oVym<$H zI=juX3&!|5qI4Mwv!`?$iW_nsuRYG9N!z zlvApx$VkZv@-am|C+4$5N}88KuOU@bB_WKmLZQHCaFOHH$uQz`IcfH^a9%~dh%-)b z=t5&D@t;|GVnEIn)&ET+U*PT(4u`@>ltoqKr8B%#6!f877%B2_F_{%rbU|AZ>YhwB zr4+fKB8>WiLK?^T86NwOuU#4R=chRB+nKvml$jCbv@#PhULjO+8A2|vWOHh$%d`xyFR}K>hP_O8 zF0OfDc4aAgn`XCZcFWha-e0C$@6eUILgRWznQqw=mJh5ql<63}y2L`q8l$zXy6nkq zZ)^jndT{D(=bO&Q+GlUzt4{2>EgSfb!+Y(>*67(X&F}R`Zs1vQqkT8CJYA+6ApUVs vyYwK8B_1m#ng6*iSIKnFBbz4?9bYo`K^8<-xFpg6Y3|P{K=ObGEMmPyncJy&qWR=F5IJ zcsHxZH83&?Pj*O9WfTFEqF_=COp1d^2{0)MCZ)inG?+@>6 zGY0Dbv92Xk9^YiSWM#039#n)ME}{<=5tzI{N{lU(56Cr`{2^J9l^@751hQ25jZB#! zL8`!z&Y)quStZ4sdGo(?MWM}as_rsw{#g?$$$ku|qWFRE=Fn-`Op~n^@^5}WgNJRh z`~2yf1r`{wO;%khI9YuuKbzeFwksU&lXaI!Om3MiFxhG*FZ+b_8D&>E)WBk9OZg{f z&f(pBW7$d;&O9bo^Y00)H7uLER(mmu7qc_EGS1+*E}?l*Lh}m)nEN4zfAZ@!(f}(= Bz<2-v diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index 6460eff..aa9b09d 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -117,6 +117,7 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="search-results"', body) self.assertIn('id="info-modal"', body) self.assertIn('id="rename-popup"', body) + self.assertIn('id="rename-label"', body) self.assertIn('id="rename-input"', body) self.assertIn('id="rename-apply-btn"', body) self.assertIn('id="settings-general-tab"', body) @@ -227,6 +228,8 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('function feedbackElements()', app_js) self.assertIn('function openFeedbackModal(message)', app_js) self.assertIn('function closeFeedbackModal()', app_js) + self.assertIn('function openConfirmModal({ title, message, path, applyText = "Confirm" })', app_js) + self.assertIn('function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit })', app_js) self.assertIn('function downloadModalElements()', app_js) self.assertIn('function isZipDownloadSelection(items)', app_js) self.assertIn('function singleFileDownloadRequestKey(path)', app_js) @@ -328,6 +331,11 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('startCopySelected();', app_js) self.assertIn('openF6Flow();', app_js) self.assertIn('deleteSelected();', app_js) + self.assertIn('const confirmed = await openConfirmModal({', app_js) + self.assertIn('title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?"', app_js) + self.assertIn('title: "Discard unsaved changes?"', app_js) + self.assertIn('title: "Create Folder"', app_js) + self.assertIn('title: "Add Bookmark"', app_js) self.assertIn('openInfo();', app_js) self.assertIn('elements.title.textContent = "Properties";', app_js) self.assertIn('if (selectedItems.length > 1) {', app_js) @@ -338,6 +346,9 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertNotIn('Only files are supported for copy', app_js) self.assertIn('document.getElementById("upload-menu-toggle").onclick = (event) => {', app_js) self.assertIn('document.getElementById("upload-folder-btn").onclick = openFolderPicker;', app_js) + self.assertNotIn('window.confirm(', app_js) + self.assertNotIn('window.prompt(', app_js) + self.assertNotIn('window.alert(', app_js) self.assertIn('throw createApiError(response, data);', app_js) self.assertIn('function closeUploadMenu()', app_js) self.assertIn('function toggleUploadMenu()', app_js) diff --git a/webui/html/app.js b/webui/html/app.js index 22a788e..554a774 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -46,11 +46,10 @@ let moveState = { let renameState = { source: null, name: "", + submitAction: null, }; let deleteConfirmState = { - pane: "left", - items: [], - recursivePaths: [], + resolver: null, }; let contextMenuState = { open: false, @@ -292,6 +291,8 @@ function moveElements() { function renameElements() { return { overlay: document.getElementById("rename-popup"), + title: document.getElementById("rename-title"), + label: document.getElementById("rename-label"), input: document.getElementById("rename-input"), error: document.getElementById("rename-error"), applyButton: document.getElementById("rename-apply-btn"), @@ -2583,7 +2584,28 @@ function navigateTo(pane, path) { async function createFolderForPane(pane) { setActivePane(pane); - const name = window.prompt("Folder name"); + const name = await new Promise((resolve) => { + openTextInputModal({ + title: "Create Folder", + label: "Folder name", + applyText: "Create", + initialValue: "", + onSubmit: async (rawValue, elements, cancelled) => { + if (cancelled) { + resolve(""); + return true; + } + const value = rawValue.trim(); + elements.error.textContent = ""; + if (!value) { + elements.error.textContent = "Folder name is required"; + return false; + } + resolve(value); + return true; + }, + }); + }); if (!name) { return; } @@ -2609,49 +2631,36 @@ async function renameSelected() { if (selectedItems.length !== 1) { return; } - const selected = selectedItems[0]; - const newName = window.prompt("New name", selected.name); - if (!newName) { - return; - } - setError("actions-error", ""); - try { - await apiRequest("POST", "/api/files/rename", { - path: selected.path, - new_name: newName, - }); - setSelectedItem(pane, null); - await loadBrowsePane(pane); - } catch (err) { - setActionError("Rename", err); - } + openRenamePopup(); } function closeDeleteConfirmModal() { const elements = deleteConfirmElements(); - deleteConfirmState.pane = "left"; - deleteConfirmState.items = []; - deleteConfirmState.recursivePaths = []; + const resolver = deleteConfirmState.resolver; + deleteConfirmState.resolver = null; elements.error.textContent = ""; + elements.title.textContent = "Delete folder and contents?"; + elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it."; + elements.path.textContent = ""; + elements.applyButton.textContent = "Delete"; elements.overlay.classList.add("hidden"); + if (typeof resolver === "function") { + resolver(false); + } } -function openDeleteConfirmModal(pane, items, recursivePaths) { +function openConfirmModal({ title, message, path, applyText = "Confirm" }) { const elements = deleteConfirmElements(); - deleteConfirmState.pane = pane; - deleteConfirmState.items = items.map((item) => ({ ...item })); - deleteConfirmState.recursivePaths = Array.from(recursivePaths); elements.error.textContent = ""; - if (items.length === 1) { - elements.title.textContent = "Delete folder and contents?"; - elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it."; - elements.path.textContent = items[0].path; - } else { - elements.title.textContent = "Delete selected items and folder contents?"; - elements.message.textContent = `This will permanently delete ${items.length} selected items, including all files and subfolders inside the selected folders.`; - elements.path.textContent = `${items.length} selected items`; - } + elements.title.textContent = title; + elements.message.textContent = message; + elements.path.textContent = path || ""; + elements.applyButton.textContent = applyText; elements.overlay.classList.remove("hidden"); + elements.applyButton.focus(); + return new Promise((resolve) => { + deleteConfirmState.resolver = resolve; + }); } async function executeDeleteItems(pane, items, recursivePaths) { @@ -2690,20 +2699,19 @@ async function executeDeleteItems(pane, items, recursivePaths) { } async function submitDeleteConfirmModal() { - const elements = deleteConfirmElements(); - if (!deleteConfirmState.items.length) { + const resolver = deleteConfirmState.resolver; + if (typeof resolver !== "function") { return; } + deleteConfirmState.resolver = null; + const elements = deleteConfirmElements(); elements.error.textContent = ""; - try { - const pane = deleteConfirmState.pane; - const items = [...deleteConfirmState.items]; - const recursivePaths = new Set(deleteConfirmState.recursivePaths); - closeDeleteConfirmModal(); - await executeDeleteItems(pane, items, recursivePaths); - } catch (err) { - elements.error.textContent = err.message; - } + elements.title.textContent = "Delete folder and contents?"; + elements.message.textContent = "This will permanently delete the folder and all files and subfolders inside it."; + elements.path.textContent = ""; + elements.applyButton.textContent = "Delete"; + elements.overlay.classList.add("hidden"); + resolver(true); } async function collectDeleteRecursivePaths(selectedItems) { @@ -2734,10 +2742,29 @@ async function deleteSelected() { try { const recursivePaths = await collectDeleteRecursivePaths(selectedItems); if (recursivePaths.size > 0) { - openDeleteConfirmModal(pane, selectedItems, recursivePaths); + const confirmed = await openConfirmModal({ + title: selectedItems.length === 1 ? "Delete folder and contents?" : "Delete selected items and folder contents?", + message: selectedItems.length === 1 + ? "This will permanently delete the folder and all files and subfolders inside it." + : `This will permanently delete ${selectedItems.length} selected items, including all files and subfolders inside the selected folders.`, + path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`, + applyText: "Delete", + }); + if (!confirmed) { + return; + } + await executeDeleteItems(pane, selectedItems, recursivePaths); return; } - if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) { + const confirmed = await openConfirmModal({ + title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?", + message: selectedItems.length === 1 + ? "This will permanently delete the selected item." + : `This will permanently delete ${selectedItems.length} selected items.`, + path: selectedItems.length === 1 ? selectedItems[0].path : `${selectedItems.length} selected items`, + applyText: "Delete", + }); + if (!confirmed) { return; } await executeDeleteItems(pane, selectedItems, new Set()); @@ -2841,7 +2868,28 @@ async function executeMoveSelection(baseDestination) { async function addBookmark() { const pane = state.activePane; const path = paneState(pane).currentPath; - const label = window.prompt("Bookmark label", path); + const label = await new Promise((resolve) => { + openTextInputModal({ + title: "Add Bookmark", + label: "Bookmark label", + applyText: "Add", + initialValue: path, + onSubmit: async (rawValue, elements, cancelled) => { + if (cancelled) { + resolve(""); + return true; + } + const value = rawValue.trim(); + elements.error.textContent = ""; + if (!value) { + elements.error.textContent = "Bookmark label is required"; + return false; + } + resolve(value); + return true; + }, + }); + }); if (!label) { return; } @@ -3145,15 +3193,43 @@ function resetRenameState() { renameState = { source: null, name: "", + submitAction: null, }; } -function closeRenamePopup() { +function settleRenamePopup(value = null, cancelled = false, notify = true) { const elements = renameElements(); + const submitAction = renameState.submitAction; elements.overlay.classList.add("hidden"); elements.error.textContent = ""; elements.input.value = ""; + elements.title.textContent = "Rename"; + elements.label.textContent = "Name"; + elements.applyButton.textContent = "Rename"; resetRenameState(); + if (notify && typeof submitAction === "function") { + return submitAction(value, null, cancelled); + } + return undefined; +} + +function closeRenamePopup() { + settleRenamePopup(null, true); +} + +function openTextInputModal({ title, label, applyText, initialValue = "", onSubmit }) { + const elements = renameElements(); + renameState.source = null; + renameState.name = initialValue; + renameState.submitAction = onSubmit; + elements.title.textContent = title; + elements.label.textContent = label; + elements.applyButton.textContent = applyText; + elements.input.value = initialValue; + elements.error.textContent = ""; + elements.overlay.classList.remove("hidden"); + elements.input.focus(); + elements.input.select(); } function openRenamePopup() { @@ -3162,15 +3238,49 @@ function openRenamePopup() { return false; } const source = selectedItems[0]; - const elements = renameElements(); - renameState.source = source; - renameState.name = source.name; - elements.input.value = source.name; - elements.error.textContent = ""; - elements.overlay.classList.remove("hidden"); - elements.input.focus(); - elements.input.select(); - return true; + return openTextInputModal({ + title: "Rename", + label: "Name", + applyText: "Rename", + initialValue: source.name, + onSubmit: async (rawValue, elements, cancelled) => { + if (cancelled) { + return true; + } + const newName = rawValue.trim(); + elements.error.textContent = ""; + if (!newName) { + elements.error.textContent = "Name is required"; + return false; + } + if (newName === source.name) { + elements.error.textContent = "Name must differ from current name"; + return false; + } + if (newName.includes("/")) { + elements.error.textContent = "Name cannot contain /"; + return false; + } + if (newName === "." || newName === "..") { + elements.error.textContent = "Invalid name"; + return false; + } + + try { + await apiRequest("POST", "/api/files/rename", { + path: source.path, + new_name: newName, + }); + setSelectedItem(state.activePane, null); + await loadBrowsePane(state.activePane); + setStatus(`Renamed ${source.path}`); + return true; + } catch (err) { + elements.error.textContent = err.message; + return false; + } + }, + }); } function resetBatchMoveState() { @@ -3253,40 +3363,12 @@ function openF6Flow() { async function submitRenamePopup() { const elements = renameElements(); - const source = renameState.source; - if (!source) { + if (typeof renameState.submitAction !== "function") { return; } - const newName = elements.input.value.trim(); - elements.error.textContent = ""; - if (!newName) { - elements.error.textContent = "Name is required"; - return; - } - if (newName === source.name) { - elements.error.textContent = "Name must differ from current name"; - return; - } - if (newName.includes("/")) { - elements.error.textContent = "Name cannot contain /"; - return; - } - if (newName === "." || newName === "..") { - elements.error.textContent = "Invalid name"; - return; - } - - try { - await apiRequest("POST", "/api/files/rename", { - path: source.path, - new_name: newName, - }); - closeRenamePopup(); - setSelectedItem(state.activePane, null); - await loadBrowsePane(state.activePane); - setStatus(`Renamed ${source.path}`); - } catch (err) { - elements.error.textContent = err.message; + const shouldClose = await renameState.submitAction(elements.input.value, elements, false); + if (shouldClose !== false) { + settleRenamePopup("", false, false); } } @@ -3764,9 +3846,17 @@ function resetEditorState() { }; } -function attemptCloseEditor() { - if (editorIsDirty() && !window.confirm("Discard unsaved changes?")) { - return; +async function attemptCloseEditor() { + if (editorIsDirty()) { + const discard = await openConfirmModal({ + title: "Discard unsaved changes?", + message: "Your unsaved editor changes will be lost.", + path: editorState.path || "", + applyText: "Discard", + }); + if (!discard) { + return; + } } closeEditor(); } diff --git a/webui/html/index.html b/webui/html/index.html index 2823fc4..d4e4627 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -302,7 +302,7 @@