Files
MajoorWaldi-ComfyUI-Majoor-…/ui/features/panel/controllers/browserNavigationController.ts
T

423 lines
14 KiB
TypeScript

import { t } from "../../../app/i18n.js";
import { createPanelStateBridge } from "../../../stores/panelStateBridge.js";
export function createBrowserNavigationController({
state,
gridContainer,
folderBreadcrumb,
folderBreadcrumbController = null,
customSelect,
reloadGrid,
clearSelection = null,
onContextChanged = null,
lifecycleSignal = null,
}: Record<string, any> = {}): Record<string, any> {
const { read, write } = createPanelStateBridge(state, [
"scope",
"customRootId",
"currentFolderRelativePath",
]);
const folderBackStack: any[] = [];
const normPath = (value: any) =>
String(value || "")
.trim()
.replaceAll("\\", "/");
const isWindowsDrive = (part: any) => /^[a-zA-Z]:$/.test(String(part || "").trim());
const currentFolderPath = () => normPath(read("currentFolderRelativePath", "") || "");
const pushUniqueHistory = (stack: any, value: any) => {
const v = normPath(value);
if (!Array.isArray(stack)) return;
if (!v && v !== "") return;
const top = stack.length ? normPath(stack[stack.length - 1]) : null;
if (top === v) return;
stack.push(v);
};
const popDistinctHistory = (stack: any, current: any) => {
const cur = normPath(current);
if (!Array.isArray(stack)) return null;
while (stack.length) {
const candidate = normPath(stack.pop());
if (candidate !== cur) return candidate;
}
return null;
};
const setFolderPath = (next: any) => {
const v = normPath(next);
write("currentFolderRelativePath", v);
try {
if (gridContainer?.dataset) gridContainer.dataset.mjrSubfolder = v;
} catch (e) {
console.debug?.(e);
}
};
const resolveFolderTargetPath = (asset: any) => {
// ".." parent folder: always navigate to parent of current path
if (normPath(asset?.filename || "") === "..") {
return parentFolderPath(currentFolderPath());
}
const rawSubfolder = normPath(asset?.subfolder || "");
if (rawSubfolder) return rawSubfolder;
const rawFilepath = normPath(asset?.filepath || asset?.path || "");
if (rawFilepath) {
const kind = String(asset?.kind || "").toLowerCase();
if (kind === "folder") return rawFilepath;
// Defensive fallback: if a file path is emitted, browse its parent folder.
const parent = parentFolderPath(rawFilepath);
return parent || rawFilepath;
}
const name = normPath(asset?.filename || "");
if (!name) return currentFolderPath();
const base = currentFolderPath();
if (!base) return name;
return `${base}/${name}`.replace(/\/{2,}/g, "/");
};
const parentFolderPath = (pathValue: any) => {
const cur = normPath(pathValue);
if (!cur) return "";
const parts = cur.split("/").filter(Boolean);
if (!parts.length) return "";
if (parts.length === 1) return isWindowsDrive(parts[0]) ? `${parts[0]}/` : "";
parts.pop();
if (parts.length === 1 && isWindowsDrive(parts[0])) return `${parts[0]}/`;
return parts.join("/");
};
const notifyContext = () => {
try {
onContextChanged?.();
} catch (e) {
console.debug?.(e);
}
try {
renderBreadcrumb();
} catch (e) {
console.debug?.(e);
}
};
const resetGridScrollToTop = () => {
try {
if (gridContainer && typeof gridContainer.scrollTop === "number") {
gridContainer.scrollTop = 0;
}
} catch (e) {
console.debug?.(e);
}
try {
const parent = gridContainer?.parentElement || null;
if (parent && typeof parent.scrollTop === "number") {
parent.scrollTop = 0;
}
} catch (e) {
console.debug?.(e);
}
};
const navigateFolder = async (target: any, { pushHistory = true } = {}) => {
const prev = currentFolderPath();
const next = normPath(target);
if (prev === next) return;
if (pushHistory) pushUniqueHistory(folderBackStack, prev);
setFolderPath(next);
resetGridScrollToTop();
notifyContext();
try {
clearSelection?.();
} catch (e) {
console.debug?.(e);
}
await reloadGrid?.();
};
const resetToBrowserRoot = async () => {
write("customRootId", "");
setFolderPath("");
resetGridScrollToTop();
notifyContext();
try {
clearSelection?.();
} catch (e) {
console.debug?.(e);
}
await reloadGrid?.();
};
const setVueBreadcrumb = (payload: any) => {
if (typeof folderBreadcrumbController?.setFolderBreadcrumb !== "function") return false;
try {
folderBreadcrumbController.setFolderBreadcrumb(payload);
return true;
} catch (e) {
console.debug?.(e);
return false;
}
};
const renderBreadcrumb = () => {
const isCustom = String(read("scope", "") || "") === "custom";
const rel = String(read("currentFolderRelativePath", "") || "")
.trim()
.replaceAll("\\", "/");
const folderBrowsingActive = isCustom || !!rel;
const selectedRootId = String(read("customRootId", "") || "").trim();
const selectedRootLabel = (() => {
if (!selectedRootId) return "";
try {
const opt = customSelect?.selectedOptions?.[0] || null;
return String(opt?.textContent || "").trim();
} catch {
return "";
}
})();
if (!folderBrowsingActive) {
folderBackStack.length = 0;
if (setVueBreadcrumb({ visible: false })) return;
folderBreadcrumb.style.display = "none";
folderBreadcrumb.replaceChildren();
return;
}
const hasSelectedRoot = !!String(read("customRootId", "") || "").trim();
const canBackToBrowserRoot = hasSelectedRoot && !rel;
const canBackByPath = !!rel;
const backDisabled =
folderBackStack.length === 0 && !canBackToBrowserRoot && !canBackByPath;
const upDisabled = !rel || parentFolderPath(currentFolderPath()) === currentFolderPath();
const handleBack = async () => {
const prev = popDistinctHistory(folderBackStack, currentFolderPath());
if (prev != null) {
await navigateFolder(prev, { pushHistory: false });
return;
}
const cur = currentFolderPath();
if (cur) {
const parent = parentFolderPath(cur);
if (parent === cur) {
if (hasSelectedRoot) await resetToBrowserRoot();
return;
}
await navigateFolder(parent, { pushHistory: false });
return;
}
if (canBackToBrowserRoot) await resetToBrowserRoot();
};
const handleUp = async () => {
const parent = parentFolderPath(currentFolderPath());
if (parent === currentFolderPath()) {
if (hasSelectedRoot) await resetToBrowserRoot();
return;
}
await navigateFolder(parent);
};
const buildItem = (label: any, target: any, isCurrent = false) => ({
label: String(label || ""),
target: String(target || ""),
current: !!isCurrent,
onClick: async () => {
if (label === "Computer" && String(read("customRootId", "") || "").trim()) {
await resetToBrowserRoot();
return;
}
await navigateFolder(target);
},
});
const breadcrumbItems = [buildItem(t("label.computer", "Computer"), "", !rel)];
if (selectedRootId) {
breadcrumbItems.push(buildItem(selectedRootLabel || selectedRootId, "", !rel));
}
if (rel) {
const parts = rel.split("/").filter(Boolean);
let acc = "";
for (let i = 0; i < parts.length; i++) {
if (!acc) acc = isWindowsDrive(parts[i]) ? `${parts[i]}/` : parts[i];
else acc = `${acc}/${parts[i]}`.replace(/\/{2,}/g, "/");
breadcrumbItems.push(buildItem(parts[i], acc, i === parts.length - 1));
}
}
if (
setVueBreadcrumb({
visible: true,
back: {
label: t("btn.back", "Back"),
disabled: backDisabled,
onClick: handleBack,
},
up: {
label: t("btn.up", "Up"),
disabled: upDisabled,
onClick: handleUp,
},
items: breadcrumbItems,
})
) {
return;
}
folderBreadcrumb.style.display = "flex";
folderBreadcrumb.replaceChildren();
const backBtn = document.createElement("button");
backBtn.type = "button";
backBtn.textContent = t("btn.back", "Back");
backBtn.className = "mjr-btn-link mjr-folder-breadcrumb-action";
backBtn.disabled = backDisabled;
backBtn.addEventListener("click", handleBack, { signal: lifecycleSignal || undefined });
folderBreadcrumb.appendChild(backBtn);
const upBtn = document.createElement("button");
upBtn.type = "button";
upBtn.textContent = t("btn.up", "Up");
upBtn.className = "mjr-btn-link mjr-folder-breadcrumb-action";
upBtn.disabled = upDisabled;
upBtn.addEventListener("click", handleUp, { signal: lifecycleSignal || undefined });
folderBreadcrumb.appendChild(upBtn);
const sep0 = document.createElement("span");
sep0.textContent = "|";
sep0.className = "mjr-folder-breadcrumb-separator";
folderBreadcrumb.appendChild(sep0);
const mk = (label: any, target: any, isCurrent = false) => {
const el = document.createElement("button");
el.type = "button";
el.textContent = label;
el.className = isCurrent
? "mjr-btn-link mjr-folder-breadcrumb-segment is-current"
: "mjr-btn-link mjr-folder-breadcrumb-segment";
if (!isCurrent) {
el.addEventListener(
"click",
async () => {
if (label === "Computer" && String(read("customRootId", "") || "").trim()) {
await resetToBrowserRoot();
return;
}
await navigateFolder(target);
},
{ signal: lifecycleSignal || undefined },
);
} else {
el.disabled = true;
}
return el;
};
for (let i = 0; i < breadcrumbItems.length; i++) {
if (i > 0) {
const sep = document.createElement("span");
sep.textContent = "/";
sep.className = "mjr-folder-breadcrumb-separator";
folderBreadcrumb.appendChild(sep);
}
const item = breadcrumbItems[i];
folderBreadcrumb.appendChild(mk(item.label, item.target, item.current));
}
};
const bindGridFolderNavigation = () => {
const onOpenFolderAsset = async (e: any) => {
const asset = e?.detail?.asset || null;
const target = resolveFolderTargetPath(asset || {});
const prev = currentFolderPath();
if (target !== prev) {
pushUniqueHistory(folderBackStack, prev);
setFolderPath(target);
resetGridScrollToTop();
notifyContext();
try {
clearSelection?.();
} catch (e) {
console.debug?.(e);
}
try {
await reloadGrid?.();
} catch (e) {
console.debug?.(e);
}
}
};
const onCustomSubfolderChanged = async (e: any) => {
const next = String(e?.detail?.subfolder || "")
.trim()
.replaceAll("\\", "/");
const prev = currentFolderPath();
if (next !== prev) {
pushUniqueHistory(folderBackStack, prev);
setFolderPath(next);
resetGridScrollToTop();
notifyContext();
try {
clearSelection?.();
} catch (e) {
console.debug?.(e);
}
try {
await reloadGrid?.();
} catch (e) {
console.debug?.(e);
}
}
};
gridContainer?.addEventListener?.(
"mjr:custom-subfolder-changed",
onCustomSubfolderChanged,
{
signal: lifecycleSignal || undefined,
},
);
gridContainer?.addEventListener?.("mjr:open-folder-asset", onOpenFolderAsset, {
signal: lifecycleSignal || undefined,
});
try {
gridContainer._mjrHasCustomSubfolderHandler = true;
} catch (e) {
console.debug?.(e);
}
return () => {
try {
gridContainer?.removeEventListener?.(
"mjr:custom-subfolder-changed",
onCustomSubfolderChanged,
);
} catch (e) {
console.debug?.(e);
}
try {
gridContainer?.removeEventListener?.("mjr:open-folder-asset", onOpenFolderAsset);
} catch (e) {
console.debug?.(e);
}
try {
gridContainer._mjrHasCustomSubfolderHandler = false;
} catch (e) {
console.debug?.(e);
}
};
};
return {
renderBreadcrumb,
resetHistory: () => {
folderBackStack.length = 0;
},
bindGridFolderNavigation,
notifyContext,
};
}