From f0b04fd4ee708a4e9cb3c0d45f4fdf261002d2da Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 14 Mar 2026 07:10:49 +0100 Subject: [PATCH] feat: folder upload - deel 3 --- webui/backend/data/tasks.db | Bin 122880 -> 147456 bytes .../test_ui_smoke_golden.cpython-313.pyc | Bin 26477 -> 26973 bytes .../tests/golden/test_ui_smoke_golden.py | 13 +++-- webui/html/app.js | 48 +++++++++++++++--- webui/html/base.css | 44 ++++++++++++++++ webui/html/index.html | 10 +++- 6 files changed, 104 insertions(+), 11 deletions(-) diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 32c94d5282de070ab4bc582bc463ee1c9efb0f7a..3a2c117ceb69b421223f0102ee02a3e757fdd5b2 100644 GIT binary patch delta 16739 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 delta 748 zcmW+yTSydP7@cosXJ=<-XMZ&-LffPkTDwSFK2%;ZunPw2BKcsN_7FXl6=rn749%>_ zEAdqzmD=SYi`>YNsMW~8H1;L8m++o>38^exZ`=3p%b9P^`Of*z|Cg4krCy~bN^TGY z!Co7)3mRS&G~bIfvmqh`JDX?!*e^E4#@LW2Jmv$d&9q#KK1@Cg2;FRwvNCS zt`X)7-Gb>8c$ISPHPa=RI;m_G`;|j1-r{6=YNx5ucrV|Uz8F(Vi?vt3VoNryw>HBG zi`&v;?3X(%=e0rW1#=>Mu0<-UrHOqNx}_y0Xj^V>)1T?Nsk+~G%zRWXRPDkaDM#rO zBaFMc2GR0fbCu;Us}NtoxK^#6lvj%h;!V?xx=+1hIwzG`V{JA1Hb7&I^c0^AKn4~B zAr@0Az#->3rGtPaFTsJ{0LXZHkd!)7AP2pJuo;UMi5OiX@GTIe1qdGVseuc_Lj=bg zY2A&kVOrnaOmhbI&C@(GMQ(xrV$u?ct-RK| zEZ$`lHeg|ZxXan}+ds#voucR@^z)@4x#WaU_qZIQccQzJm>nS|IhQQ1F?fhtkRSO2 zew#vqH=S<0DKz$OC)Y-FFVMnTev{T4WXfEia~>qpG%-$%I|7i62e>L05jQYFrK)++ z(d!gs=Rx&HzB{md5-ws76@XRP!f#$uN$D%G;|w^lX^N8U*QnjHkl}5O10t}hn6|?) zk-y0s{<sUAmBfX@V|Dbms=rh>7?>oz9FV>fL{~Z0f+AW4pkhW+ToR@ z78dn*P3JnL`E@~!9SW6X7}bc9utE9YnXbMKT8&K@I}RhxODH9ObneDmT|z8WlTIe2 zp_qS^7t{Wf6pttPnoRk>Mp8 z|D=7y&i713#A}SA!rk>jC^TRg+Qcg5aQ!v`)y+=uSd{ZKP6+9oZv3Fe14Wv?=y#~= z)^xtq^>vMMM<7S2311vqLUpN{AWt?gncy2!j;3IP@uAD|!Imi`TSMoOx_tU@Ju-r$ z*=xAY86nSj4L3MK<&4gooC$i4_>fsuZ*io`+nhz^9j*x8DQ>YE-Q`LOME3v>`z^EV z96Nsi7j0k*dx=^0K06PC;W|Zspz03DhFu05`w;RJZRG6iqr#+&bFd5haP6^ZERHdc zthkv^m>TB0 Ipf_Ii3*MY98UO$Q delta 654 zcmcb6iSg|@M!wIyyj%=G5d33HW}eSRzJJV24+J*zvE1gK+$$n7`7eLO=SDD7{-%c9Q^Trl~XNIPTU<~UJx zrp@QX89A8#vu(a8e}!>!mooq49A*B^9~4Dc1y2sr0*?+-mI;E zg%zyurvOAkRuD{Yjy65Vwz<%zj*+o@@&{XG#vTy4`Mqrg91mD*(PSM*UDn0H zOM;h9j&oE6vX%i^9geD;i-VU3FSTUK6PWzZQJk4Cc*W!kj>?QHfuy3KK=3M4CP<(t zFr+hRuHLNXG=YiHX0n5;!sP2N0z4Y2WvO{3nhLgd3e}Sjx{7HF=NF{rx#j1iq!tBa zCTFJ>X=qwkfdxYga`F>X5P|_fLnJqUbWIUun+dd{c+KYMXniJ=kPBfkS2$vCaPai= zcJf{j(_RsFg~J3tck_eTADkSHOswYL-B?OCKS=jx6su!lbY+~uaa}_5qJ-ub1~B)7 J(`3g?833U?*4_XB diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index c6ba17a..a20e9ed 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -50,6 +50,9 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="right-focus-line"', body) self.assertIn('id="function-bar"', body) self.assertIn('id="upload-btn"', 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-input"', body) self.assertIn('id="upload-progress"', body) self.assertIn('id="upload-target"', body) @@ -178,10 +181,12 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('function effectiveThemeKey(theme, colorMode)', app_js) self.assertIn("document.documentElement.dataset.theme", app_js) self.assertIn('document.getElementById("theme-toggle").onclick = toggleTheme;', app_js) - self.assertIn('document.getElementById("upload-btn").onclick = (event) => {', app_js) - self.assertIn('if (event.altKey) {', app_js) - self.assertIn("openFolderPicker();", app_js) - self.assertIn("openUploadPicker();", app_js) + self.assertIn('document.getElementById("upload-btn").onclick = openUploadPicker;', 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.assertIn('function closeUploadMenu()', app_js) + self.assertIn('function toggleUploadMenu()', app_js) + self.assertNotIn('if (event.altKey) {', app_js) self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js) self.assertIn('async function loadSettings()', app_js) self.assertIn('await loadSettings();', app_js) diff --git a/webui/html/app.js b/webui/html/app.js index 5b52c24..a2363d8 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -325,7 +325,11 @@ function infoElements() { function uploadElements() { return { + menu: document.getElementById("upload-menu"), button: document.getElementById("upload-btn"), + menuToggle: document.getElementById("upload-menu-toggle"), + 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"), @@ -477,6 +481,25 @@ function setUploadProgressVisible(visible) { uploadElements().progress.classList.toggle("hidden", !visible); } +function closeUploadMenu() { + const elements = uploadElements(); + if (!elements.menuPopup || !elements.menuToggle) { + return; + } + elements.menuPopup.classList.add("hidden"); + elements.menuToggle.setAttribute("aria-expanded", "false"); +} + +function toggleUploadMenu() { + const elements = uploadElements(); + if (!elements.menuPopup || !elements.menuToggle) { + return; + } + const nextOpen = elements.menuPopup.classList.contains("hidden"); + elements.menuPopup.classList.toggle("hidden", !nextOpen); + elements.menuToggle.setAttribute("aria-expanded", nextOpen ? "true" : "false"); +} + function resetUploadProgress() { const elements = uploadElements(); uploadState.active = false; @@ -523,6 +546,7 @@ function openUploadPicker() { if (uploadState.active) { return; } + closeUploadMenu(); uploadState.targetPath = activePaneState().currentPath; const elements = uploadElements(); elements.input.value = ""; @@ -533,6 +557,7 @@ function openFolderPicker() { if (uploadState.active) { return; } + closeUploadMenu(); folderUploadPlanState = { targetPane: state.activePane, targetPath: activePaneState().currentPath, @@ -3040,6 +3065,11 @@ function clearSelectionForActivePane() { } function handleKeyboardShortcuts(event) { + if (event.key === "Escape" && !uploadElements().menuPopup.classList.contains("hidden")) { + event.preventDefault(); + closeUploadMenu(); + return; + } if (isInfoOpen()) { if (event.key === "Escape") { event.preventDefault(); @@ -3264,13 +3294,12 @@ function setupEvents() { setupPaneEvents("right"); document.addEventListener("keydown", handleKeyboardShortcuts); document.getElementById("theme-toggle").onclick = toggleTheme; - document.getElementById("upload-btn").onclick = (event) => { - if (event.altKey) { - openFolderPicker(); - return; - } - openUploadPicker(); + document.getElementById("upload-btn").onclick = openUploadPicker; + document.getElementById("upload-menu-toggle").onclick = (event) => { + event.stopPropagation(); + toggleUploadMenu(); }; + document.getElementById("upload-folder-btn").onclick = openFolderPicker; document.getElementById("settings-btn").onclick = () => openSettings("general"); document.getElementById("view-btn").onclick = openViewer; document.getElementById("edit-btn").onclick = openEditor; @@ -3280,6 +3309,13 @@ function setupEvents() { document.getElementById("move-btn").onclick = openF6Flow; document.getElementById("mkdir-btn").onclick = createFolderForActivePane; uploadElements().input.onchange = handleUploadSelection; + document.addEventListener("click", (event) => { + const elements = uploadElements(); + if (!elements.menu || elements.menu.contains(event.target)) { + return; + } + closeUploadMenu(); + }); const rename = renameElements(); rename.closeButton.onclick = closeRenamePopup; diff --git a/webui/html/base.css b/webui/html/base.css index 1ebdbd4..d8a4329 100644 --- a/webui/html/base.css +++ b/webui/html/base.css @@ -577,6 +577,50 @@ button:disabled { justify-content: center; } +.split-button { + position: relative; + display: inline-flex; + align-items: stretch; +} + +.split-button > button { + min-width: 0; +} + +#upload-btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +#upload-menu-toggle { + min-width: 32px; + padding-left: 7px; + padding-right: 7px; + border-left: 1px solid var(--color-border); + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.split-menu { + position: absolute; + left: 0; + bottom: calc(100% + 6px); + min-width: 148px; + display: grid; + gap: 4px; + padding: 6px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface-elevated); + box-shadow: var(--shadow-elevated); + z-index: 20; +} + +.split-menu button { + width: 100%; + justify-content: flex-start; +} + .shortcut-hint { color: var(--color-text-muted); font-size: 10px; diff --git a/webui/html/index.html b/webui/html/index.html index 68e0b4c..47b9fca 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -85,7 +85,15 @@