From 7bb59a2b6570062ce802ee5d3ec2178f57324d6d Mon Sep 17 00:00:00 2001 From: kodi Date: Sat, 14 Mar 2026 09:31:01 +0100 Subject: [PATCH] feat: contextmenu deel 2 --- .../test_ui_smoke_golden.cpython-313.pyc | Bin 29750 -> 30787 bytes .../tests/golden/test_ui_smoke_golden.py | 13 +++ webui/html/app.js | 97 +++++++++++++++++- webui/html/base.css | 39 +++++++ webui/html/index.html | 7 ++ 5 files changed, 155 insertions(+), 1 deletion(-) 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 85b4696d051a3fe927720ee702090313677bcadc..0584c959f97f21532620fd2ad3089d5523ecdf06 100644 GIT binary patch delta 1163 zcmZXSTWAwO6o#|eq)nS{E*jHlrEU_fS#6T46{*q|FRk&4vaDL9N=VjbYuvQit(Wx* zDk35Z=RsRgTU2~g%X-(=)@xA_gn~W^zKFhvT18(h&d#jjWgh06^Z)0}3^V82JL>*N z$~D3;#TGQ4mR*&gsn<11TfdKm_UV)LnYJSgG~0dfyvBq1)w>S*ygt}47svX+F##I7 zYKhrzvdEu+htA0`t&Zp`O?LW8+-LF@(v8ht6H@!&qBjf`^+a7-&)DJ?em{KnAEFNE zTImT|(!5f-Hk8WAs<>T^ZV}~;$=ELCBfL>J*wwJ%HpU^VLmZFTeGlw0K zwpgfN%1h%ZOn9!K!dj-3~W3^oyGb&te;1EkEvhK z=Pf)!L2)CwM^|G7WrM-SFq(t$Abm+7ylk=;arNR~u5PMV5I@Zb(pM4RAlHs-uM31X zgaRDj6bdnK2@cHLLJ{U2p%}AIaANigB?hUxKqsO_1NnC;j)*t32y$8u^1zG_{X!Y@A=ZX8PIHk!QdTANXsNOJ+_qRwHm_zhJzL90$YE@49lKMT zhJzSs?-)K+Q6r(Ov_73wlTlP3mE^Rzy*b2bPH7Vx2_lI@g=2B9>#r-$)2o($2ozpI z&!nUoeTul*3gcxryqDbYHsglERySNA^eq@@9Cj>w0UGJs042|08+-I scei+*j0nJ!vrFK!A@ZETG58d3HAbD{Uu_8V0s3W-UFo{i1D%LGX&R9 zZW3>2Y}m{tF`aocw~P)4BkyJll}}8Q|EcnBE>;&~WqoQcm8L(rPe+`oLv}JBi^}HH zI+<*fD@;T-dl(xqPIfR5ncQP?1SBvy#Z&++(P7F1kyv3S08+5|jM*mM&4*m;7#ZhJ zc63)}TmU8)g2_c-axs`(0w$M&$z@=2If$I>;9fJyQ)Kc5cQr=V$@2{qnb(DaX~y-F z8$FeoH-xf-MK^|Wfay)4oFJNU^JGCUW#%oR++fkIp*&#GZIf%gbXm6t?+D&GdA*k^ zkhKfQy6L6LxjlGy@J>snJb}sn-r~%B!FwhvdMh*T1(J$_0>S%CnLyFRz@Wg8&Y-z} zbGf$(Go#Do|NaV_Z~C8+U|$WCD!#|Fc}+$()8w)ofz4~P1-UtbnOM!gXR&x}K33+< zXq3Xr=*l>Q3}EgDKW+vVo{p-^%#t@)c-noNd|UlLFf*`76^Q{&1OPg= BvK9aU diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index 3380c41..2340646 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -68,6 +68,10 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="feedback-modal"', body) self.assertIn('id="feedback-message"', body) self.assertIn('id="feedback-close-btn"', body) + self.assertIn('id="context-menu"', body) + self.assertIn('id="context-menu-scope"', body) + self.assertIn('id="context-menu-target"', body) + self.assertIn('id="context-menu-open-placeholder"', body) self.assertIn('id="settings-btn"', body) self.assertIn('id="rename-btn"', body) self.assertIn('id="view-btn"', body) @@ -200,6 +204,15 @@ 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 contextMenuElements()', app_js) + self.assertIn('function openContextMenu(pane, entry, event)', app_js) + self.assertIn('function closeContextMenu()', app_js) + self.assertIn('selectedPathsSet.has(entry.path)', app_js) + self.assertIn('entry.isParent', app_js) + self.assertIn('row.oncontextmenu = (event) => {', app_js) + self.assertIn('event.target.closest("li[data-row-index]")', app_js) + self.assertIn('if (!row) {', app_js) + self.assertIn('closeContextMenu();', 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('throw createApiError(response, data);', app_js) diff --git a/webui/html/app.js b/webui/html/app.js index 17d701d..a8f5fd4 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -52,6 +52,12 @@ let deleteConfirmState = { items: [], recursivePaths: [], }; +let contextMenuState = { + open: false, + pane: "left", + items: [], + anchorPath: null, +}; let batchMoveState = { destinationBase: "", count: 0, @@ -315,6 +321,62 @@ function feedbackElements() { }; } +function contextMenuElements() { + return { + menu: document.getElementById("context-menu"), + scope: document.getElementById("context-menu-scope"), + target: document.getElementById("context-menu-target"), + placeholder: document.getElementById("context-menu-open-placeholder"), + }; +} + +function isContextMenuOpen() { + return contextMenuState.open && !contextMenuElements().menu.classList.contains("hidden"); +} + +function closeContextMenu() { + const elements = contextMenuElements(); + contextMenuState.open = false; + contextMenuState.pane = "left"; + contextMenuState.items = []; + contextMenuState.anchorPath = null; + if (!elements.menu) { + return; + } + elements.menu.classList.add("hidden"); + elements.scope.textContent = ""; + elements.target.textContent = ""; +} + +function openContextMenu(pane, entry, event) { + if (!entry || entry.isParent) { + return; + } + const elements = contextMenuElements(); + const selectedItems = paneState(pane).selectedItems || []; + const selectedPathsSet = new Set(selectedItems.map((item) => item.path)); + const items = selectedPathsSet.has(entry.path) + ? selectedItems.map((item) => ({ ...item })) + : [selectedEntryFromItem(entry)]; + + contextMenuState.open = true; + contextMenuState.pane = pane; + contextMenuState.items = items; + contextMenuState.anchorPath = entry.path; + + const isMulti = items.length > 1; + elements.scope.textContent = isMulti ? "Multi-selection" : "Single item"; + elements.target.textContent = isMulti ? `${items.length} selected items` : entry.name; + + const menuWidth = 220; + const menuHeight = 120; + const x = Math.min(event.clientX, window.innerWidth - menuWidth - 12); + const y = Math.min(event.clientY, window.innerHeight - menuHeight - 12); + elements.menu.style.left = `${Math.max(8, x)}px`; + elements.menu.style.top = `${Math.max(8, y)}px`; + elements.menu.classList.remove("hidden"); +} + function settingsElements() { return { overlay: document.getElementById("settings-modal"), @@ -1689,6 +1751,7 @@ function updatePaneFocusLine(pane) { } function renderPaneItems(pane) { + closeContextMenu(); const model = paneState(pane); const items = document.getElementById(`${pane}-items`); items.innerHTML = ""; @@ -1718,6 +1781,9 @@ function renderPaneItems(pane) { clearSelectionAnchor(pane); renderPaneItems(pane); }; + up.oncontextmenu = (event) => { + event.preventDefault(); + }; const upNameCell = document.createElement("span"); upNameCell.className = "entry-name entry-dir"; upNameCell.append(createSelectionSlot(pane, { ...entry, isParent: true }, index)); @@ -1789,6 +1855,12 @@ function renderPaneItems(pane) { } renderPaneItems(pane); }; + row.oncontextmenu = (event) => { + event.preventDefault(); + event.stopPropagation(); + setActivePane(pane); + openContextMenu(pane, entry, event); + }; if (entry.kind === "file" && isImageSelection({ path: entry.path, name: entry.name, kind: entry.kind })) { row.ondblclick = (ev) => { ev.stopPropagation(); @@ -1873,6 +1945,7 @@ async function loadBrowsePane(pane) { } function navigateTo(pane, path) { + closeContextMenu(); const model = paneState(pane); model.currentPath = path; model.currentRowIndex = 0; @@ -3335,6 +3408,13 @@ function clearSelectionForActivePane() { } function handleKeyboardShortcuts(event) { + if (isContextMenuOpen()) { + if (event.key === "Escape") { + event.preventDefault(); + closeContextMenu(); + } + return; + } if (event.key === "Escape" && !uploadElements().menuPopup.classList.contains("hidden")) { event.preventDefault(); closeUploadMenu(); @@ -3617,9 +3697,24 @@ function setupEvents() { document.addEventListener("click", (event) => { const elements = uploadElements(); if (!elements.menu || elements.menu.contains(event.target)) { + } else { + closeUploadMenu(); + } + const contextMenu = contextMenuElements().menu; + if (contextMenu && !contextMenu.contains(event.target)) { + closeContextMenu(); + } + }); + document.addEventListener("contextmenu", (event) => { + const contextMenu = contextMenuElements().menu; + if (contextMenu && contextMenu.contains(event.target)) { + event.preventDefault(); return; } - closeUploadMenu(); + const row = event.target instanceof Element ? event.target.closest("li[data-row-index]") : null; + if (!row) { + closeContextMenu(); + } }); const rename = renameElements(); diff --git a/webui/html/base.css b/webui/html/base.css index 60dce9a..c26925a 100644 --- a/webui/html/base.css +++ b/webui/html/base.css @@ -727,6 +727,45 @@ button:disabled { width: min(440px, calc(100vw - 24px)); } +.context-menu { + position: fixed; + min-width: 220px; + padding: 8px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface-elevated); + box-shadow: var(--shadow-elevated); + z-index: 1100; +} + +.context-menu-scope { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--color-text-muted); +} + +.context-menu-target { + margin-top: 4px; + font-size: 12px; + color: var(--color-text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.context-menu-separator { + height: 1px; + margin: 8px 0; + background: var(--color-border); +} + +.context-menu button { + width: 100%; + justify-content: flex-start; +} + #upload-modal .popup-card { max-width: 320px; padding: 12px 14px; diff --git a/webui/html/index.html b/webui/html/index.html index ee0cd17..9d36b75 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -118,6 +118,13 @@ + +