Add Phase 2 remote browse scaffolding for /Clients

This commit is contained in:
kodi
2026-03-27 11:39:26 +01:00
parent 841318c9e2
commit 4062cbf6c8
15 changed files with 635 additions and 31 deletions
+86 -19
View File
@@ -141,6 +141,10 @@ const VALID_THEME_FAMILIES = [
"fluent-neon",
];
const VALID_COLOR_MODES = ["dark", "light"];
const VIRTUAL_SOURCES = [
{ path: "/Volumes", label: "Volumes" },
{ path: "/Clients", label: "Clients" },
];
let searchState = {
pane: "left",
path: "/Volumes",
@@ -200,6 +204,56 @@ function activePaneState() {
return paneState(state.activePane);
}
function sourceRootForPath(path) {
const normalized = (path || "").trim();
if (normalized === "/Clients" || normalized.startsWith("/Clients/")) {
return "/Clients";
}
return "/Volumes";
}
function isRemoteBrowsePath(path) {
return sourceRootForPath(path) === "/Clients";
}
function syncSourceSwitchers() {
["left", "right"].forEach((pane) => {
const container = document.getElementById(`${pane}-source-switcher`);
if (!container) {
return;
}
const activeSource = sourceRootForPath(paneState(pane).currentPath);
[...container.querySelectorAll("button[data-source-path]")].forEach((button) => {
const isActive = button.dataset.sourcePath === activeSource;
button.disabled = isActive;
button.setAttribute("aria-pressed", isActive ? "true" : "false");
});
});
}
function ensureSourceSwitchers() {
["left", "right"].forEach((pane) => {
const toolbar = document.querySelector(`#${pane}-pane .pane-topbar`);
if (!toolbar || document.getElementById(`${pane}-source-switcher`)) {
return;
}
const container = document.createElement("div");
container.id = `${pane}-source-switcher`;
container.className = "pane-source-switcher";
VIRTUAL_SOURCES.forEach((source) => {
const button = createButton(source.label, () => {
setActivePane(pane);
navigateTo(pane, source.path);
});
button.type = "button";
button.dataset.sourcePath = source.path;
container.append(button);
});
toolbar.prepend(container);
});
syncSourceSwitchers();
}
function setStatus(msg) {
document.getElementById("status").textContent = msg;
}
@@ -716,6 +770,7 @@ function openContextMenu(pane, entry, event) {
const items = selectedPathsSet.has(entry.path)
? selectedItems.map((item) => ({ ...item }))
: [selectedEntryFromItem(entry)];
const remoteSelection = items.some((item) => isRemoteBrowsePath(item.path));
contextMenuState.open = true;
contextMenuState.pane = pane;
@@ -723,26 +778,28 @@ function openContextMenu(pane, entry, event) {
contextMenuState.anchorPath = entry.path;
const isMulti = items.length > 1;
const openableSingle = items.length === 1 && isOpenableSelection(items[0]);
const editableSingle = items.length === 1 && isEditableSelection(items[0]);
const downloadableSelection = items.length > 0;
const openableSingle = items.length === 1 && (!remoteSelection || items[0].kind === "directory") && isOpenableSelection(items[0]);
const editableSingle = items.length === 1 && !remoteSelection && isEditableSelection(items[0]);
const downloadableSelection = items.length > 0 && !remoteSelection;
elements.scope.textContent = isMulti ? "Multi-selection" : "Single item";
elements.target.textContent = isMulti ? `${items.length} selected items` : entry.name;
elements.openButton.classList.toggle("hidden", isMulti);
elements.openButton.disabled = !openableSingle;
elements.editButton.classList.toggle("hidden", isMulti || items.length !== 1 || items[0].kind !== "file");
elements.editButton.classList.toggle("hidden", isMulti || items.length !== 1 || items[0].kind !== "file" || remoteSelection);
elements.editButton.disabled = !editableSingle;
elements.downloadButton.classList.remove("hidden");
elements.downloadButton.classList.toggle("hidden", remoteSelection);
elements.downloadButton.disabled = !downloadableSelection;
elements.renameButton.classList.toggle("hidden", isMulti);
elements.renameButton.classList.toggle("hidden", isMulti || remoteSelection);
elements.duplicateButton.classList.remove("hidden");
elements.duplicateButton.disabled = items.length === 0;
elements.duplicateButton.disabled = remoteSelection || items.length === 0;
elements.copyButton.classList.remove("hidden");
elements.copyButton.disabled = items.length === 0;
elements.copyButton.disabled = remoteSelection || items.length === 0;
elements.moveButton.classList.remove("hidden");
elements.moveButton.disabled = remoteSelection || items.length === 0;
elements.deleteButton.classList.remove("hidden");
elements.propertiesButton.classList.remove("hidden");
elements.propertiesButton.disabled = items.length === 0;
elements.deleteButton.disabled = remoteSelection || items.length === 0;
elements.propertiesButton.classList.toggle("hidden", remoteSelection);
elements.propertiesButton.disabled = remoteSelection || items.length === 0;
const menuWidth = 220;
const menuHeight = 120;
@@ -2050,12 +2107,17 @@ function updateActionButtons() {
const hasSelection = count > 0;
const exactlyOne = count === 1;
const allFiles = hasSelection && selectedItems.every((item) => item.kind === "file");
document.getElementById("view-btn").disabled = !exactlyOne || !allFiles;
document.getElementById("edit-btn").disabled = !exactlyOne || !allFiles || !isEditableSelection(selectedItems[0] || null);
document.getElementById("rename-btn").disabled = !exactlyOne;
document.getElementById("delete-btn").disabled = !hasSelection;
document.getElementById("copy-btn").disabled = !hasSelection;
document.getElementById("move-btn").disabled = !hasSelection;
const remoteBrowse = isRemoteBrowsePath(activePaneState().currentPath);
document.getElementById("view-btn").disabled = remoteBrowse || !exactlyOne || !allFiles;
document.getElementById("edit-btn").disabled = remoteBrowse || !exactlyOne || !allFiles || !isEditableSelection(selectedItems[0] || null);
document.getElementById("rename-btn").disabled = remoteBrowse || !exactlyOne;
document.getElementById("delete-btn").disabled = remoteBrowse || !hasSelection;
document.getElementById("copy-btn").disabled = remoteBrowse || !hasSelection;
document.getElementById("move-btn").disabled = remoteBrowse || !hasSelection;
document.getElementById("mkdir-btn").disabled = remoteBrowse;
document.getElementById("upload-btn").disabled = remoteBrowse;
document.getElementById("upload-menu-toggle").disabled = remoteBrowse;
document.getElementById("upload-folder-btn").disabled = remoteBrowse;
}
function isEditableSelection(item) {
@@ -2208,7 +2270,7 @@ function currentParentPath(path) {
if (!normalized) {
return null;
}
if (normalized === "/Volumes") {
if (normalized === "/Volumes" || normalized === "/Clients") {
return null;
}
if (normalized.startsWith("/")) {
@@ -2287,16 +2349,17 @@ function renderBreadcrumbs(pane, path) {
const isHostPath = normalized.startsWith("/");
const parts = normalized.split("/").filter(Boolean);
if (isHostPath) {
const rootTarget = parts.length > 0 ? `/${parts[0]}` : "/Volumes";
const rootCrumb = createButton("/", () => {
setActivePane(pane);
navigateTo(pane, "/Volumes");
navigateTo(pane, rootTarget);
});
rootCrumb.type = "button";
rootCrumb.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
setActivePane(pane);
navigateTo(pane, "/Volumes");
navigateTo(pane, rootTarget);
};
nav.append(rootCrumb);
if (parts.length > 0) {
@@ -2619,6 +2682,7 @@ async function loadBrowsePane(pane) {
});
const data = await apiRequest("GET", `/api/browse?${query.toString()}`);
model.currentPath = data.path;
syncSourceSwitchers();
renderBreadcrumbs(pane, data.path);
const visibleItems = [];
@@ -2682,6 +2746,8 @@ function navigateTo(pane, path) {
model.currentRowIndex = 0;
clearSelectionAnchor(pane);
setSelectedItem(pane, null);
syncSourceSwitchers();
updateActionButtons();
loadBrowsePane(pane);
}
@@ -5305,6 +5371,7 @@ async function init() {
setError("actions-error", "");
applyTheme("default", "dark");
setActivePane("left");
ensureSourceSwitchers();
setupEvents();
await loadSettings();
applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);