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="left-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="right-breadcrumbs"', 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}`);
}
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) {
const options = { method, headers: {} };
if (body !== undefined) {
@@ -103,13 +113,15 @@ function toggleSelection(pane, item) {
}
function updateActionButtons() {
const selected = activePaneState().selectedItem;
const hasSelection = Boolean(selected);
const isFile = hasSelection && selected.kind === "file";
document.getElementById("rename-btn").disabled = !hasSelection;
const selectedItems = activePaneState().selectedItems;
const count = selectedItems.length;
const hasSelection = count > 0;
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("copy-btn").disabled = !isFile;
document.getElementById("move-btn").disabled = !isFile;
document.getElementById("copy-btn").disabled = !allFiles;
document.getElementById("move-btn").disabled = !allFiles;
}
function currentParentPath(path) {
@@ -133,7 +145,6 @@ function renderBreadcrumbs(pane, path) {
const crumbPath = aggregate;
const crumb = createButton(parts[i], () => {
setActivePane(pane);
console.debug("[breadcrumbs] click", { pane, crumbPath });
navigateTo(pane, crumbPath);
});
crumb.type = "button";
@@ -141,7 +152,6 @@ function renderBreadcrumbs(pane, path) {
ev.preventDefault();
ev.stopPropagation();
setActivePane(pane);
console.debug("[breadcrumbs] click", { pane, crumbPath });
navigateTo(pane, crumbPath);
};
nav.append(crumb);
@@ -239,11 +249,6 @@ async function loadBrowsePane(pane) {
path: model.currentPath,
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()}`);
model.currentPath = data.path;
document.getElementById(`${pane}-current-path`).textContent = data.path;
@@ -299,7 +304,6 @@ async function loadBrowsePane(pane) {
}
function navigateTo(pane, path) {
console.debug("[navigate] pane-path", { pane, path });
paneState(pane).currentPath = path;
setSelectedItem(pane, null);
loadBrowsePane(pane);
@@ -329,10 +333,11 @@ async function createFolderForActivePane() {
async function renameSelected() {
const pane = state.activePane;
const selected = paneState(pane).selectedItem;
if (!selected) {
const selectedItems = paneState(pane).selectedItems;
if (selectedItems.length !== 1) {
return;
}
const selected = selectedItems[0];
const newName = window.prompt("New name", selected.name);
if (!newName) {
return;
@@ -352,21 +357,31 @@ async function renameSelected() {
async function deleteSelected() {
const pane = state.activePane;
const selected = paneState(pane).selectedItem;
if (!selected) {
const selectedItems = [...paneState(pane).selectedItems];
if (selectedItems.length === 0) {
return;
}
if (!window.confirm(`Delete ${selected.path}?`)) {
if (!window.confirm(`Delete ${selectedItems.length} selected item(s)?`)) {
return;
}
setError("actions-error", "");
let successes = 0;
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
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);
await loadBrowsePane(pane);
} catch (err) {
setActionError("Delete", err);
}
showActionSummary("Delete", successes, failures, firstError);
}
function defaultDestination(sourcePath, targetBasePath) {
@@ -377,58 +392,84 @@ function defaultDestination(sourcePath, targetBasePath) {
async function startCopySelected() {
const sourcePane = state.activePane;
const destinationPane = otherPane(sourcePane);
const selected = paneState(sourcePane).selectedItem;
if (!selected || selected.kind !== "file") {
const selectedItems = [...paneState(sourcePane).selectedItems];
if (selectedItems.length === 0) {
return;
}
const destination = window.prompt(
"Copy destination (full path)",
defaultDestination(selected.path, paneState(destinationPane).currentPath),
const baseDestination = window.prompt(
"Copy destination base path (full path)",
paneState(destinationPane).currentPath,
);
if (!destination) {
if (!baseDestination) {
return;
}
setError("actions-error", "");
let successes = 0;
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
const destination = defaultDestination(item.path, baseDestination);
try {
if (item.kind !== "file") {
throw new Error("Only files are supported for copy");
}
const result = await apiRequest("POST", "/api/files/copy", {
source: selected.path,
source: item.path,
destination,
});
state.selectedTaskId = result.task_id;
setStatus(`Copy task queued: ${result.task_id}`);
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
successes += 1;
} 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() {
const sourcePane = state.activePane;
const destinationPane = otherPane(sourcePane);
const selected = paneState(sourcePane).selectedItem;
if (!selected || selected.kind !== "file") {
const selectedItems = [...paneState(sourcePane).selectedItems];
if (selectedItems.length === 0) {
return;
}
const destination = window.prompt(
"Move destination (full path)",
defaultDestination(selected.path, paneState(destinationPane).currentPath),
const baseDestination = window.prompt(
"Move destination base path (full path)",
paneState(destinationPane).currentPath,
);
if (!destination) {
if (!baseDestination) {
return;
}
setError("actions-error", "");
let successes = 0;
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
const destination = defaultDestination(item.path, baseDestination);
try {
if (item.kind !== "file") {
throw new Error("Only files are supported for move");
}
const result = await apiRequest("POST", "/api/files/move", {
source: selected.path,
source: item.path,
destination,
});
state.selectedTaskId = result.task_id;
setSelectedItem(sourcePane, null);
setStatus(`Move task queued: ${result.task_id}`);
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
successes += 1;
} 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() {