From 8fe9d0f43640faa6225f13b5ed69ddd0f9f4672f Mon Sep 17 00:00:00 2001 From: kodi Date: Fri, 13 Mar 2026 16:21:51 +0100 Subject: [PATCH] feat: upload - deel 02 --- container/Containerfile | 2 +- webui/backend/data/tasks.db | Bin 110592 -> 110592 bytes .../test_ui_smoke_golden.cpython-313.pyc | Bin 22740 -> 23894 bytes .../tests/golden/test_ui_smoke_golden.py | 15 +++ webui/html/app.js | 106 ++++++++++++++++++ webui/html/base.css | 24 ++++ webui/html/index.html | 7 ++ 7 files changed, 153 insertions(+), 1 deletion(-) diff --git a/container/Containerfile b/container/Containerfile index dffc74b..6dbe5b1 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -18,7 +18,7 @@ RUN mkdir -p /app/backend /app/html /app/conf /Volumes/8TB /Volumes/8TB_RAID1 # Installeer een lichtgewicht Python API framework (FastAPI) # We gebruiken --break-system-packages omdat we in een container zitten -RUN pip3 install fastapi uvicorn --break-system-packages +RUN pip3 install fastapi uvicorn python-multipart --break-system-packages # Exposeer de poort voor de webinterface EXPOSE 8030 diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 5e55de77caf4f01ff73db69444d4673cb9accb15..1443848b5824f73715e2ff6393b2b6d3cb612d81 100644 GIT binary patch delta 251 zcmZp8z}E19ZGtpo%tRSy#+Z!>OV%@cvIIn!5qps{laQSmCXycBy+QQay($*c(9q{ zz%`D^KTim7npzo}TbUY0d2arBg8MojE7v~;zU#c5Jjb{Xa{b$^DDZ~MqS3*VJ5*Ah zAw8Xe!PJx(OG^uK@)J|0U-V)$nEb)lc=CFGPI==HLzrDg21aJO2FALENMhR$u4c4n F0RX&_Pk8_U delta 96 zcmV-m0H6PW;0A!;29O&8Tag??0b8+Pq^|==1bLGOuzj~J1fVv=akb^ zejJ!nL>Qn*d~Q&@;KBX|BLJTad0Bk;SFCw1LCqh) zzkBvtL0or_71#T{aL_djITGom6J|s*9CC!vtBf-+;SZrIV~tUv^r=N##!BC}SOiPT zQmDgw{%hzA-WcGma~^qWSFl;Hs^V1Ql&%B~Uz;E+^`;&)0&P-VQ!AR(XatRQZK^J6 z6^V5AwQ1$1rb&vPn3k*3zirEEQ_+LJ(U)+V4>;p4Z8P~fZ41Q!g+MV#@eD>U z_A(+4e}4{=*en+G%L{wr0XP?D5#Nn}h-d+iCAtx<;F;tv4yZ+W zpaY>-=s;+s10j(Nlk(l-%2eeNFokO=_JZiVkMLDYnmObDH2JjZyR z@dBd(x3eL1k><8bMF;GT4%%kTh+YF>IAohEb};qzBFEwzMHh|eGAxklo5e1sUMY4n z^)1+xOE|6;t`+9tP%c8U>m)m$i@2^9ZWQKoR>ce7=Xz~Dg`2RH3!%3O2YuecJOAuY z#S*)P`5m{dXdf(&Zg85b1Iwf9Jw>)fdAU3zX*-QPE#sw;5!>8`>84WFWmOT(?)_D% zMmD1njT!xGCABUo?GB_E7aJFpvar^2T2!X0lG#*{suJ^zN@s~z%ozT!cam*yQdQ>4 zIJSITY;mXX*vs1xtmZxNLEaM?T{_z0UV6lJzw5s4Ugq=5 zhsRsov9)vjV^U7TgTsB!)B@k)hH)eRq7zNR7e@~`_t1x4fzDDtnt-jP7`M;r$o_i3 zUV>8TVMHX=$M%{iE7}BY>Ri delta 710 zcmYL_OH30{6o%){qn$pWQ##UWQz(Hpj#@!bpmv0ss0k(nbeOoPwGyS+@-ichC|XhQ zmAEjTjSCk|)WoID-RZ^^J7P?HL^ilGuC$8LRU7ZUpx(vzo%5fQJCpnV1$tXX_3L$Y zPL7kw8F|R7x6uUtp*He((I2oyWh+F6Rue@tMVMkI#V(2+6fwLU z8bRnO4mK|%oM?F>+8uu99oKgiK1cMXHT$+qzjy|b)xSp~g}`xWeQ66$M$0yc$5dEx zD9l=n{GF!a!Pp~216ECo} zBZYEriG61*9z_znrC>_%Jl=|Ith;F1ndeNI`I6~?!doWM;t}thOrRmrxkdm`-OUA5h}j0XnKq#S~Qr7ar6J{k8|2B{6)Xd zqhU}6w&HjFx`4$)Sy#NByp`2Kv4^6QqKjfL!K%ApO=ixI&YsI;XDq9mM4H#~(s3_- zJhX&t4@iAs9>* E09tIyw*UYD diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index d6b1369..adcd1d5 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -49,6 +49,12 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="left-focus-line"', body) self.assertIn('id="right-focus-line"', body) self.assertIn('id="function-bar"', body) + self.assertIn('id="upload-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="settings-btn"', body) self.assertIn('id="rename-btn"', body) self.assertIn('id="view-btn"', body) @@ -131,6 +137,7 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertNotIn('id="tasks-panel"', body) ordered_ids = [ + 'id="upload-btn"', 'id="settings-btn"', 'id="rename-btn"', 'id="view-btn"', @@ -171,6 +178,7 @@ 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 = openUploadPicker;', 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) @@ -190,6 +198,13 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);', app_js) self.assertIn('settings.interfaceTab.onclick = () => setSettingsTab("interface");', app_js) self.assertIn('"/api/settings"', app_js) + self.assertIn('function uploadElements()', app_js) + self.assertIn('function openUploadPicker()', app_js) + self.assertIn('async function handleUploadSelection(event)', app_js) + self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js) + self.assertIn('"/api/files/upload"', app_js) + self.assertIn('Upload to: ${uploadState.targetPath}', app_js) + self.assertIn('Uploading ${total} file', app_js) self.assertIn('`/api/files/thumbnail?', app_js) self.assertIn("function iconTypeForEntry(entry)", app_js) self.assertIn("function mediaIconSvg(type)", app_js) diff --git a/webui/html/app.js b/webui/html/app.js index 4543999..f89d720 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -57,6 +57,12 @@ let imageViewerState = { path: null, resizeHandler: null, }; +let uploadState = { + active: false, + targetPath: "", + files: [], + index: 0, +}; let settingsState = { activeTab: "general", logsLoaded: false, @@ -302,6 +308,17 @@ function infoElements() { }; } +function uploadElements() { + return { + button: document.getElementById("upload-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"), + }; +} + async function apiRequest(method, url, body) { const options = { method, headers: {} }; if (body !== undefined) { @@ -317,6 +334,23 @@ async function apiRequest(method, url, body) { return data; } +async function uploadFileRequest(targetPath, file) { + const formData = new FormData(); + formData.append("target_path", targetPath); + formData.append("file", file, file.name); + + const response = await fetch("/api/files/upload", { + method: "POST", + body: formData, + }); + const data = await response.json().catch(() => ({})); + if (!response.ok) { + const error = data.error || {}; + throw new Error(error.message || `HTTP ${response.status}`); + } + return data; +} + async function refreshTasksSnapshot() { try { const data = await apiRequest("GET", "/api/tasks"); @@ -333,6 +367,76 @@ function createButton(text, onClick) { return button; } +function setUploadProgressVisible(visible) { + uploadElements().progress.classList.toggle("hidden", !visible); +} + +function resetUploadProgress() { + const elements = uploadElements(); + uploadState.active = false; + uploadState.targetPath = ""; + uploadState.files = []; + uploadState.index = 0; + elements.button.disabled = false; + elements.target.textContent = ""; + elements.currentFile.textContent = ""; + elements.count.textContent = ""; + setUploadProgressVisible(false); +} + +function updateUploadProgress() { + const elements = uploadElements(); + const total = uploadState.files.length; + const currentFile = uploadState.files[uploadState.index] || null; + elements.target.textContent = `Upload to: ${uploadState.targetPath}`; + elements.currentFile.textContent = currentFile + ? `Uploading ${total} file${total === 1 ? "" : "s"} - Current file: ${currentFile.name}` + : `Uploading ${total} file${total === 1 ? "" : "s"}`; + elements.count.textContent = total > 0 ? `${Math.min(uploadState.index + 1, total)}/${total} files` : ""; + elements.button.disabled = uploadState.active; + setUploadProgressVisible(uploadState.active); +} + +function openUploadPicker() { + if (uploadState.active) { + return; + } + uploadState.targetPath = activePaneState().currentPath; + const elements = uploadElements(); + elements.input.value = ""; + elements.input.click(); +} + +async function handleUploadSelection(event) { + const files = Array.from(event.target.files || []); + event.target.value = ""; + if (files.length === 0) { + return; + } + + const targetPath = uploadState.targetPath || activePaneState().currentPath; + uploadState.active = true; + uploadState.targetPath = targetPath; + uploadState.files = files; + uploadState.index = 0; + setError("actions-error", ""); + updateUploadProgress(); + + try { + for (let index = 0; index < files.length; index += 1) { + uploadState.index = index; + updateUploadProgress(); + await uploadFileRequest(targetPath, files[index]); + } + await loadBrowsePane(state.activePane); + setStatus(`Upload: ${files.length} file${files.length === 1 ? "" : "s"} uploaded`); + } catch (err) { + setActionError("Upload", err); + } finally { + resetUploadProgress(); + } +} + function setActivePane(pane) { state.activePane = pane; document.getElementById("left-pane").classList.toggle("active-pane", pane === "left"); @@ -2723,6 +2827,7 @@ function setupEvents() { setupPaneEvents("right"); document.addEventListener("keydown", handleKeyboardShortcuts); document.getElementById("theme-toggle").onclick = toggleTheme; + document.getElementById("upload-btn").onclick = openUploadPicker; document.getElementById("settings-btn").onclick = () => openSettings("general"); document.getElementById("view-btn").onclick = openViewer; document.getElementById("edit-btn").onclick = openEditor; @@ -2731,6 +2836,7 @@ function setupEvents() { document.getElementById("copy-btn").onclick = startCopySelected; document.getElementById("move-btn").onclick = openF6Flow; document.getElementById("mkdir-btn").onclick = createFolderForActivePane; + uploadElements().input.onchange = handleUploadSelection; const rename = renameElements(); rename.closeButton.onclick = closeRenamePopup; diff --git a/webui/html/base.css b/webui/html/base.css index b2d1a2a..1ebdbd4 100644 --- a/webui/html/base.css +++ b/webui/html/base.css @@ -225,6 +225,30 @@ button:disabled { background: var(--color-button-secondary-bg); } +.upload-progress { + display: grid; + gap: 1px; + margin-top: 4px; + padding: 4px 8px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface-elevated); + color: var(--color-text-muted); + font-size: 12px; + line-height: 1.25; +} + +.upload-progress-target, +.upload-progress-file { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.upload-progress-count { + color: var(--color-text-primary); +} + #theme-toggle-icon { font-size: 14px; line-height: 1; diff --git a/webui/html/index.html b/webui/html/index.html index 1e6cfc4..68e0b4c 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -85,6 +85,7 @@