feat: folder upload - deel 1
This commit is contained in:
Binary file not shown.
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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user