feat: folder upload - deel 1

This commit is contained in:
kodi
2026-03-14 06:52:18 +01:00
parent 360815498e
commit e2e206573d
4 changed files with 139 additions and 2 deletions
Binary file not shown.
@@ -178,7 +178,10 @@ 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("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("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)
@@ -200,6 +203,13 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('"/api/settings"', app_js) self.assertIn('"/api/settings"', app_js)
self.assertIn('function uploadElements()', app_js) self.assertIn('function uploadElements()', app_js)
self.assertIn('function openUploadPicker()', app_js) self.assertIn('function openUploadPicker()', app_js)
self.assertIn('function ensureFolderUploadPicker()', app_js)
self.assertIn('function openFolderPicker()', app_js)
self.assertIn('function buildFolderUploadPlan(files, targetPath)', app_js)
self.assertIn('function handleFolderSelection(event)', app_js)
self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
self.assertIn('Folder: ${plan.rootFolderName} (plan only)', app_js)
self.assertIn('Folder upload plan ready:', app_js)
self.assertIn('async function handleUploadSelection(event)', app_js) self.assertIn('async function handleUploadSelection(event)', app_js)
self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js) self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js)
self.assertIn('"/api/files/upload"', app_js) self.assertIn('"/api/files/upload"', app_js)
+128 -1
View File
@@ -69,6 +69,14 @@ let uploadState = {
cancelled: false, cancelled: false,
conflictResolver: null, conflictResolver: null,
}; };
let folderUploadPlanState = {
targetPath: "",
rootFolderName: "",
entries: [],
fileCount: 0,
subfolderCount: 0,
};
let folderUploadPickerInput = null;
let settingsState = { let settingsState = {
activeTab: "general", activeTab: "general",
logsLoaded: false, logsLoaded: false,
@@ -325,6 +333,22 @@ function uploadElements() {
}; };
} }
function ensureFolderUploadPicker() {
if (folderUploadPickerInput) {
return folderUploadPickerInput;
}
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.hidden = true;
input.setAttribute("webkitdirectory", "");
input.setAttribute("directory", "");
input.onchange = handleFolderSelection;
document.body.append(input);
folderUploadPickerInput = input;
return folderUploadPickerInput;
}
function uploadConflictElements() { function uploadConflictElements() {
return { return {
overlay: document.getElementById("upload-conflict-modal"), overlay: document.getElementById("upload-conflict-modal"),
@@ -471,6 +495,16 @@ function resetUploadProgress() {
setUploadProgressVisible(false); setUploadProgressVisible(false);
} }
function showFolderUploadPlan(plan) {
const elements = uploadElements();
folderUploadPlanState = plan;
elements.button.disabled = false;
elements.target.textContent = `Upload to: ${plan.targetPath}`;
elements.currentFile.textContent = `Folder: ${plan.rootFolderName} (plan only)`;
elements.count.textContent = `${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"}${plan.subfolderCount > 0 ? `${plan.subfolderCount} subfolder${plan.subfolderCount === 1 ? "" : "s"}` : ""}`;
setUploadProgressVisible(true);
}
function updateUploadProgress() { function updateUploadProgress() {
const elements = uploadElements(); const elements = uploadElements();
const total = uploadState.files.length; const total = uploadState.files.length;
@@ -494,6 +528,22 @@ function openUploadPicker() {
elements.input.click(); elements.input.click();
} }
function openFolderPicker() {
if (uploadState.active) {
return;
}
folderUploadPlanState = {
targetPath: activePaneState().currentPath,
rootFolderName: "",
entries: [],
fileCount: 0,
subfolderCount: 0,
};
const input = ensureFolderUploadPicker();
input.value = "";
input.click();
}
function isUploadConflictOpen() { function isUploadConflictOpen() {
const overlay = document.getElementById("upload-conflict-modal"); const overlay = document.getElementById("upload-conflict-modal");
return Boolean(overlay) && !overlay.classList.contains("hidden"); return Boolean(overlay) && !overlay.classList.contains("hidden");
@@ -522,6 +572,77 @@ function promptUploadConflict(fileName, targetPath, message) {
}); });
} }
function countPlannedSubfolders(relativePaths) {
const folderPaths = new Set();
relativePaths.forEach((relativePath) => {
const parts = relativePath.split("/").filter(Boolean);
if (parts.length <= 1) {
return;
}
let current = "";
for (let index = 0; index < parts.length - 1; index += 1) {
current = current ? `${current}/${parts[index]}` : parts[index];
folderPaths.add(current);
}
});
return folderPaths.size;
}
function buildFolderUploadPlan(files, targetPath) {
if (!files.length) {
return null;
}
const plannedEntries = files.map((file) => {
const webkitRelativePath = String(file.webkitRelativePath || "").replace(/\\/g, "/");
const parts = webkitRelativePath.split("/").filter(Boolean);
return {
file,
webkitRelativePath,
rootFolderName: parts[0] || "",
relativePath: parts.length > 1 ? parts.slice(1).join("/") : file.name,
};
});
const rootFolderName = plannedEntries[0].rootFolderName;
if (!rootFolderName) {
throw new Error("Folder picker did not return a usable folder structure");
}
if (plannedEntries.some((entry) => entry.rootFolderName !== rootFolderName || !entry.relativePath)) {
throw new Error("Folder picker returned multiple roots or invalid relative paths");
}
return {
targetPath,
rootFolderName,
entries: plannedEntries.map((entry) => ({
name: entry.file.name,
size: entry.file.size,
relativePath: entry.relativePath,
})),
fileCount: plannedEntries.length,
subfolderCount: countPlannedSubfolders(plannedEntries.map((entry) => entry.relativePath)),
};
}
function handleFolderSelection(event) {
const files = Array.from(event.target.files || []);
event.target.value = "";
if (files.length === 0) {
return;
}
try {
const targetPath = folderUploadPlanState.targetPath || activePaneState().currentPath;
const plan = buildFolderUploadPlan(files, targetPath);
if (!plan) {
return;
}
showFolderUploadPlan(plan);
setError("actions-error", "");
setStatus(`Folder upload plan ready: ${plan.rootFolderName} (${plan.fileCount} file${plan.fileCount === 1 ? "" : "s"})`);
} catch (err) {
setUploadProgressVisible(false);
setActionError("Folder upload", err);
}
}
async function handleUploadSelection(event) { async function handleUploadSelection(event) {
const files = Array.from(event.target.files || []); const files = Array.from(event.target.files || []);
event.target.value = ""; event.target.value = "";
@@ -3006,7 +3127,13 @@ 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("upload-btn").onclick = (event) => {
if (event.altKey) {
openFolderPicker();
return;
}
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;