feat: upload - deel 02
This commit is contained in:
@@ -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)
|
# Installeer een lichtgewicht Python API framework (FastAPI)
|
||||||
# We gebruiken --break-system-packages omdat we in een container zitten
|
# 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
|
# Exposeer de poort voor de webinterface
|
||||||
EXPOSE 8030
|
EXPOSE 8030
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -49,6 +49,12 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('id="left-focus-line"', body)
|
self.assertIn('id="left-focus-line"', body)
|
||||||
self.assertIn('id="right-focus-line"', body)
|
self.assertIn('id="right-focus-line"', body)
|
||||||
self.assertIn('id="function-bar"', 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="settings-btn"', body)
|
||||||
self.assertIn('id="rename-btn"', body)
|
self.assertIn('id="rename-btn"', body)
|
||||||
self.assertIn('id="view-btn"', body)
|
self.assertIn('id="view-btn"', body)
|
||||||
@@ -131,6 +137,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertNotIn('id="tasks-panel"', body)
|
self.assertNotIn('id="tasks-panel"', body)
|
||||||
|
|
||||||
ordered_ids = [
|
ordered_ids = [
|
||||||
|
'id="upload-btn"',
|
||||||
'id="settings-btn"',
|
'id="settings-btn"',
|
||||||
'id="rename-btn"',
|
'id="rename-btn"',
|
||||||
'id="view-btn"',
|
'id="view-btn"',
|
||||||
@@ -171,6 +178,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('function effectiveThemeKey(theme, colorMode)', app_js)
|
self.assertIn('function effectiveThemeKey(theme, colorMode)', app_js)
|
||||||
self.assertIn("document.documentElement.dataset.theme", app_js)
|
self.assertIn("document.documentElement.dataset.theme", app_js)
|
||||||
self.assertIn('document.getElementById("theme-toggle").onclick = toggleTheme;', 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('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js)
|
||||||
self.assertIn('async function loadSettings()', app_js)
|
self.assertIn('async function loadSettings()', app_js)
|
||||||
self.assertIn('await 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('applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);', app_js)
|
||||||
self.assertIn('settings.interfaceTab.onclick = () => setSettingsTab("interface");', app_js)
|
self.assertIn('settings.interfaceTab.onclick = () => setSettingsTab("interface");', app_js)
|
||||||
self.assertIn('"/api/settings"', 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('`/api/files/thumbnail?', app_js)
|
||||||
self.assertIn("function iconTypeForEntry(entry)", app_js)
|
self.assertIn("function iconTypeForEntry(entry)", app_js)
|
||||||
self.assertIn("function mediaIconSvg(type)", app_js)
|
self.assertIn("function mediaIconSvg(type)", app_js)
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ let imageViewerState = {
|
|||||||
path: null,
|
path: null,
|
||||||
resizeHandler: null,
|
resizeHandler: null,
|
||||||
};
|
};
|
||||||
|
let uploadState = {
|
||||||
|
active: false,
|
||||||
|
targetPath: "",
|
||||||
|
files: [],
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
let settingsState = {
|
let settingsState = {
|
||||||
activeTab: "general",
|
activeTab: "general",
|
||||||
logsLoaded: false,
|
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) {
|
async function apiRequest(method, url, body) {
|
||||||
const options = { method, headers: {} };
|
const options = { method, headers: {} };
|
||||||
if (body !== undefined) {
|
if (body !== undefined) {
|
||||||
@@ -317,6 +334,23 @@ async function apiRequest(method, url, body) {
|
|||||||
return data;
|
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() {
|
async function refreshTasksSnapshot() {
|
||||||
try {
|
try {
|
||||||
const data = await apiRequest("GET", "/api/tasks");
|
const data = await apiRequest("GET", "/api/tasks");
|
||||||
@@ -333,6 +367,76 @@ function createButton(text, onClick) {
|
|||||||
return button;
|
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) {
|
function setActivePane(pane) {
|
||||||
state.activePane = pane;
|
state.activePane = pane;
|
||||||
document.getElementById("left-pane").classList.toggle("active-pane", pane === "left");
|
document.getElementById("left-pane").classList.toggle("active-pane", pane === "left");
|
||||||
@@ -2723,6 +2827,7 @@ function setupEvents() {
|
|||||||
setupPaneEvents("right");
|
setupPaneEvents("right");
|
||||||
document.addEventListener("keydown", handleKeyboardShortcuts);
|
document.addEventListener("keydown", handleKeyboardShortcuts);
|
||||||
document.getElementById("theme-toggle").onclick = toggleTheme;
|
document.getElementById("theme-toggle").onclick = toggleTheme;
|
||||||
|
document.getElementById("upload-btn").onclick = openUploadPicker;
|
||||||
document.getElementById("settings-btn").onclick = () => openSettings("general");
|
document.getElementById("settings-btn").onclick = () => openSettings("general");
|
||||||
document.getElementById("view-btn").onclick = openViewer;
|
document.getElementById("view-btn").onclick = openViewer;
|
||||||
document.getElementById("edit-btn").onclick = openEditor;
|
document.getElementById("edit-btn").onclick = openEditor;
|
||||||
@@ -2731,6 +2836,7 @@ function setupEvents() {
|
|||||||
document.getElementById("copy-btn").onclick = startCopySelected;
|
document.getElementById("copy-btn").onclick = startCopySelected;
|
||||||
document.getElementById("move-btn").onclick = openF6Flow;
|
document.getElementById("move-btn").onclick = openF6Flow;
|
||||||
document.getElementById("mkdir-btn").onclick = createFolderForActivePane;
|
document.getElementById("mkdir-btn").onclick = createFolderForActivePane;
|
||||||
|
uploadElements().input.onchange = handleUploadSelection;
|
||||||
|
|
||||||
const rename = renameElements();
|
const rename = renameElements();
|
||||||
rename.closeButton.onclick = closeRenamePopup;
|
rename.closeButton.onclick = closeRenamePopup;
|
||||||
|
|||||||
@@ -225,6 +225,30 @@ button:disabled {
|
|||||||
background: var(--color-button-secondary-bg);
|
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 {
|
#theme-toggle-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
|
|
||||||
<section id="footer-bar">
|
<section id="footer-bar">
|
||||||
<div id="function-bar" class="toolbar compact-toolbar">
|
<div id="function-bar" class="toolbar compact-toolbar">
|
||||||
|
<button id="upload-btn" type="button"><span>Upload</span></button>
|
||||||
<button id="settings-btn" type="button"><span class="shortcut-hint">F1</span><span>Settings</span></button>
|
<button id="settings-btn" type="button"><span class="shortcut-hint">F1</span><span>Settings</span></button>
|
||||||
<button id="rename-btn" type="button" disabled><span class="shortcut-hint">F2</span><span>Rename</span></button>
|
<button id="rename-btn" type="button" disabled><span class="shortcut-hint">F2</span><span>Rename</span></button>
|
||||||
<button id="view-btn" type="button" disabled><span class="shortcut-hint">F3</span><span>View</span></button>
|
<button id="view-btn" type="button" disabled><span class="shortcut-hint">F3</span><span>View</span></button>
|
||||||
@@ -94,6 +95,12 @@
|
|||||||
<button id="mkdir-btn" type="button"><span class="shortcut-hint">F7</span><span>MKdir</span></button>
|
<button id="mkdir-btn" type="button"><span class="shortcut-hint">F7</span><span>MKdir</span></button>
|
||||||
<button id="delete-btn" type="button" disabled><span class="shortcut-hint">F8</span><span>Delete</span></button>
|
<button id="delete-btn" type="button" disabled><span class="shortcut-hint">F8</span><span>Delete</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
<input id="upload-input" type="file" multiple hidden>
|
||||||
|
<div id="upload-progress" class="upload-progress hidden" aria-live="polite">
|
||||||
|
<div id="upload-target" class="upload-progress-target"></div>
|
||||||
|
<div id="upload-current-file" class="upload-progress-file"></div>
|
||||||
|
<div id="upload-count" class="upload-progress-count"></div>
|
||||||
|
</div>
|
||||||
<div id="actions-error" class="error"></div>
|
<div id="actions-error" class="error"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user