feat (ui): multiselect toegevoegd

This commit is contained in:
kodi
2026-03-11 10:19:40 +01:00
parent bad3f3cf42
commit 05816751b1
4 changed files with 102 additions and 60 deletions
-2
View File
@@ -1,2 +0,0 @@
dd
@@ -30,6 +30,9 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('id="footer-bar"', body) self.assertIn('id="footer-bar"', body)
self.assertIn('id="left-pane"', body) self.assertIn('id="left-pane"', body)
self.assertIn('id="right-pane"', body) self.assertIn('id="right-pane"', body)
self.assertIn('id="left-items"', body)
self.assertIn('id="right-items"', body)
self.assertIn('id="mkdir-btn"', body)
self.assertIn('id="left-breadcrumbs"', body) self.assertIn('id="left-breadcrumbs"', body)
self.assertIn('id="right-breadcrumbs"', body) self.assertIn('id="right-breadcrumbs"', body)
self.assertNotIn('id="bookmarks-panel"', body) self.assertNotIn('id="bookmarks-panel"', body)
+85 -44
View File
@@ -41,6 +41,16 @@ function setActionError(action, err) {
setError("actions-error", `${action}: ${err.message}`); setError("actions-error", `${action}: ${err.message}`);
} }
function showActionSummary(action, successes, failures, firstError) {
const base = `${action}: ${successes} success, ${failures} failed`;
if (firstError) {
setError("actions-error", `${base}. First error: ${firstError}`);
} else {
setError("actions-error", "");
}
setStatus(base);
}
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) {
@@ -103,13 +113,15 @@ function toggleSelection(pane, item) {
} }
function updateActionButtons() { function updateActionButtons() {
const selected = activePaneState().selectedItem; const selectedItems = activePaneState().selectedItems;
const hasSelection = Boolean(selected); const count = selectedItems.length;
const isFile = hasSelection && selected.kind === "file"; const hasSelection = count > 0;
document.getElementById("rename-btn").disabled = !hasSelection; const exactlyOne = count === 1;
const allFiles = hasSelection && selectedItems.every((item) => item.kind === "file");
document.getElementById("rename-btn").disabled = !exactlyOne;
document.getElementById("delete-btn").disabled = !hasSelection; document.getElementById("delete-btn").disabled = !hasSelection;
document.getElementById("copy-btn").disabled = !isFile; document.getElementById("copy-btn").disabled = !allFiles;
document.getElementById("move-btn").disabled = !isFile; document.getElementById("move-btn").disabled = !allFiles;
} }
function currentParentPath(path) { function currentParentPath(path) {
@@ -133,7 +145,6 @@ function renderBreadcrumbs(pane, path) {
const crumbPath = aggregate; const crumbPath = aggregate;
const crumb = createButton(parts[i], () => { const crumb = createButton(parts[i], () => {
setActivePane(pane); setActivePane(pane);
console.debug("[breadcrumbs] click", { pane, crumbPath });
navigateTo(pane, crumbPath); navigateTo(pane, crumbPath);
}); });
crumb.type = "button"; crumb.type = "button";
@@ -141,7 +152,6 @@ function renderBreadcrumbs(pane, path) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
setActivePane(pane); setActivePane(pane);
console.debug("[breadcrumbs] click", { pane, crumbPath });
navigateTo(pane, crumbPath); navigateTo(pane, crumbPath);
}; };
nav.append(crumb); nav.append(crumb);
@@ -239,11 +249,6 @@ async function loadBrowsePane(pane) {
path: model.currentPath, path: model.currentPath,
show_hidden: String(model.showHidden), show_hidden: String(model.showHidden),
}); });
console.debug("[browse] request", {
pane,
path: model.currentPath,
show_hidden: model.showHidden,
});
const data = await apiRequest("GET", `/api/browse?${query.toString()}`); const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
model.currentPath = data.path; model.currentPath = data.path;
document.getElementById(`${pane}-current-path`).textContent = data.path; document.getElementById(`${pane}-current-path`).textContent = data.path;
@@ -299,7 +304,6 @@ async function loadBrowsePane(pane) {
} }
function navigateTo(pane, path) { function navigateTo(pane, path) {
console.debug("[navigate] pane-path", { pane, path });
paneState(pane).currentPath = path; paneState(pane).currentPath = path;
setSelectedItem(pane, null); setSelectedItem(pane, null);
loadBrowsePane(pane); loadBrowsePane(pane);
@@ -329,10 +333,11 @@ async function createFolderForActivePane() {
async function renameSelected() { async function renameSelected() {
const pane = state.activePane; const pane = state.activePane;
const selected = paneState(pane).selectedItem; const selectedItems = paneState(pane).selectedItems;
if (!selected) { if (selectedItems.length !== 1) {
return; return;
} }
const selected = selectedItems[0];
const newName = window.prompt("New name", selected.name); const newName = window.prompt("New name", selected.name);
if (!newName) { if (!newName) {
return; return;
@@ -352,21 +357,31 @@ async function renameSelected() {
async function deleteSelected() { async function deleteSelected() {
const pane = state.activePane; const pane = state.activePane;
const selected = paneState(pane).selectedItem; const selectedItems = [...paneState(pane).selectedItems];
if (!selected) { if (selectedItems.length === 0) {
return; return;
} }
if (!window.confirm(`Delete ${selected.path}?`)) { if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) {
return; return;
} }
setError("actions-error", ""); setError("actions-error", "");
let successes = 0;
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
try { try {
await apiRequest("POST", "/api/files/delete", { path: selected.path }); await apiRequest("POST", "/api/files/delete", { path: item.path });
successes += 1;
} catch (err) {
failures += 1;
if (!firstError) {
firstError = `${item.path}: ${err.message}`;
}
}
}
setSelectedItem(pane, null); setSelectedItem(pane, null);
await loadBrowsePane(pane); await loadBrowsePane(pane);
} catch (err) { showActionSummary("Delete", successes, failures, firstError);
setActionError("Delete", err);
}
} }
function defaultDestination(sourcePath, targetBasePath) { function defaultDestination(sourcePath, targetBasePath) {
@@ -377,58 +392,84 @@ function defaultDestination(sourcePath, targetBasePath) {
async function startCopySelected() { async function startCopySelected() {
const sourcePane = state.activePane; const sourcePane = state.activePane;
const destinationPane = otherPane(sourcePane); const destinationPane = otherPane(sourcePane);
const selected = paneState(sourcePane).selectedItem; const selectedItems = [...paneState(sourcePane).selectedItems];
if (!selected || selected.kind !== "file") { if (selectedItems.length === 0) {
return; return;
} }
const destination = window.prompt( const baseDestination = window.prompt(
"Copy destination (full path)", "Copy destination base path (full path)",
defaultDestination(selected.path, paneState(destinationPane).currentPath), paneState(destinationPane).currentPath,
); );
if (!destination) { if (!baseDestination) {
return; return;
} }
setError("actions-error", ""); setError("actions-error", "");
let successes = 0;
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
const destination = defaultDestination(item.path, baseDestination);
try { try {
if (item.kind !== "file") {
throw new Error("Only files are supported for copy");
}
const result = await apiRequest("POST", "/api/files/copy", { const result = await apiRequest("POST", "/api/files/copy", {
source: selected.path, source: item.path,
destination, destination,
}); });
state.selectedTaskId = result.task_id; state.selectedTaskId = result.task_id;
setStatus(`Copy task queued: ${result.task_id}`); successes += 1;
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
} catch (err) { } catch (err) {
setActionError("Copy", err); failures += 1;
if (!firstError) {
firstError = `${item.path}: ${err.message}`;
} }
}
}
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
showActionSummary("Copy", successes, failures, firstError);
} }
async function startMoveSelected() { async function startMoveSelected() {
const sourcePane = state.activePane; const sourcePane = state.activePane;
const destinationPane = otherPane(sourcePane); const destinationPane = otherPane(sourcePane);
const selected = paneState(sourcePane).selectedItem; const selectedItems = [...paneState(sourcePane).selectedItems];
if (!selected || selected.kind !== "file") { if (selectedItems.length === 0) {
return; return;
} }
const destination = window.prompt( const baseDestination = window.prompt(
"Move destination (full path)", "Move destination base path (full path)",
defaultDestination(selected.path, paneState(destinationPane).currentPath), paneState(destinationPane).currentPath,
); );
if (!destination) { if (!baseDestination) {
return; return;
} }
setError("actions-error", ""); setError("actions-error", "");
let successes = 0;
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
const destination = defaultDestination(item.path, baseDestination);
try { try {
if (item.kind !== "file") {
throw new Error("Only files are supported for move");
}
const result = await apiRequest("POST", "/api/files/move", { const result = await apiRequest("POST", "/api/files/move", {
source: selected.path, source: item.path,
destination, destination,
}); });
state.selectedTaskId = result.task_id; state.selectedTaskId = result.task_id;
setSelectedItem(sourcePane, null); successes += 1;
setStatus(`Move task queued: ${result.task_id}`);
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
} catch (err) { } catch (err) {
setActionError("Move", err); failures += 1;
if (!firstError) {
firstError = `${item.path}: ${err.message}`;
} }
}
}
setSelectedItem(sourcePane, null);
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
showActionSummary("Move", successes, failures, firstError);
} }
async function addBookmark() { async function addBookmark() {