1511 lines
52 KiB
TypeScript
1511 lines
52 KiB
TypeScript
import { comfyConfirm, comfyPrompt } from "../../app/dialogs.js";
|
|
import { comfyToast } from "../../app/toast.js";
|
|
import { EVENTS } from "../../app/events.js";
|
|
import { t } from "../../app/i18n.js";
|
|
import { startTimer, endTimer, trackGridRender, mark } from "../../app/metrics.js";
|
|
import { setupStatusPolling, triggerScan, updateStatus } from "../status/StatusDot.js";
|
|
import {
|
|
hydrateGridFromSnapshot,
|
|
loadAssets,
|
|
loadAssetsFromList,
|
|
prepareGridForScopeSwitch,
|
|
disposeGrid,
|
|
refreshGrid,
|
|
captureAnchor,
|
|
restoreAnchor,
|
|
bindGridScanListeners,
|
|
disposeGridScanListeners,
|
|
} from "../grid/gridApi.js";
|
|
import { get, post, getCollectionAssets, getDuplicateAlerts } from "../../api/client.js";
|
|
import { ENDPOINTS } from "../../api/endpoints.js";
|
|
import { showAssetInSidebar, closeSidebar } from "../../components/sidebar/SidebarView.js";
|
|
import { createRatingBadge, createTagsBadge } from "../../components/Badges.js";
|
|
import { loadMajoorSettings } from "../../app/settings.js";
|
|
import { createPopoverManager } from "./views/popoverManager.js";
|
|
|
|
import { createPanelStateBridge } from "../../stores/panelStateBridge.js";
|
|
import { getOptionalPanelStore } from "../../stores/getOptionalPanelStore.js";
|
|
import { bindMessagePopoverController } from "./messages/messagePopoverController.js";
|
|
|
|
import { normalizeQuery } from "./controllers/query.js";
|
|
import { createGridController } from "./controllers/gridController.js";
|
|
import { createBrowserNavigationController } from "./controllers/browserNavigationController.js";
|
|
import { createScopeController } from "./controllers/scopeController.js";
|
|
import { createContextController } from "./controllers/contextController.js";
|
|
import { createCustomRootsController } from "./controllers/customRootsController.js";
|
|
import { createSortController } from "./controllers/sortController.js";
|
|
import { bindSidebarOpen } from "./controllers/sidebarController.js";
|
|
import { createPanelHotkeysController } from "./controllers/panelHotkeysController.js";
|
|
import { createRatingHotkeysController } from "./controllers/ratingHotkeysController.js";
|
|
import { getHotkeysState, setHotkeysScope } from "./controllers/hotkeysState.js";
|
|
import { bindGridContextMenu } from "../contextmenu/GridContextMenu.js";
|
|
import { bindGridSelectionState } from "../../vue/composables/useGridSelection.js";
|
|
import { createAssetsQueryController } from "../../vue/composables/useAssetsQuery.js";
|
|
import { clearActiveGridContainer, setActiveGridContainer } from "./panelRuntimeRefs.js";
|
|
|
|
import { bootstrapPanelContainer } from "./panelBootstrap.js";
|
|
import { createSettingsSync } from "./panelSettingsSync.js";
|
|
import { bindGridEvents } from "./panelGridEventBindings.js";
|
|
import { bindSimilarSearch } from "./panelSimilarSearch.js";
|
|
import { bindPinnedFolders } from "./panelPinnedFolders.js";
|
|
import { setupFiltersInit } from "./panelFiltersInit.js";
|
|
import { buildContextMenuExtraActions } from "./panelContextMenuExtraActions.js";
|
|
import { consumePendingGeneratedAssets } from "../runtime/pendingGeneratedAssets.js";
|
|
import { MAJOOR_SETTINGS_DIALOG_EVENT } from "../../app/settings/MajoorSettingsDialog.js";
|
|
|
|
/**
|
|
* Wires controller/service orchestration against Vue-owned panel surfaces.
|
|
*
|
|
* `external` allows Vue components to provide pre-created DOM elements.
|
|
* Required external keys: statusSection, statusDot, statusText,
|
|
* capabilitiesSection, headerSection, summaryBar, updateSummaryBar,
|
|
* folderBreadcrumb, browseSection, gridWrapper, gridContainer, sidebar.
|
|
*/
|
|
export async function mountAssetsManagerPanelRuntime(
|
|
container: HTMLElement,
|
|
{ useComfyThemeUI = true, external = {} }: { useComfyThemeUI?: boolean; external?: Record<string, any> } = {},
|
|
): Promise<unknown> {
|
|
return _mountPanelRuntimeImpl(container, { useComfyThemeUI, external });
|
|
}
|
|
|
|
async function _mountPanelRuntimeImpl(container: any, { useComfyThemeUI = true, external = {} }: { useComfyThemeUI?: boolean; external?: Record<string, any> } = {}) {
|
|
startTimer("panelRender");
|
|
mark("panelRender");
|
|
|
|
const panelLifecycleAC = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
|
|
// -- 1. BOOTSTRAP ------------------------------------------------------
|
|
const { popovers, hostWrapper, hostWrapperPrevStyle } = bootstrapPanelContainer(container, {
|
|
useComfyThemeUI,
|
|
});
|
|
|
|
// -- 2. VALIDATE STATE + EXTERNAL ELEMENTS -----------------------------
|
|
const state = getOptionalPanelStore();
|
|
if (!state) {
|
|
throw new Error(
|
|
"[Majoor] mountAssetsManagerPanelRuntime requires an active Pinia panel store",
|
|
);
|
|
}
|
|
for (const key of [
|
|
"statusSection",
|
|
"statusDot",
|
|
"statusText",
|
|
"capabilitiesSection",
|
|
"headerSection",
|
|
"summaryBar",
|
|
"updateSummaryBar",
|
|
"folderBreadcrumb",
|
|
"browseSection",
|
|
"gridWrapper",
|
|
"gridContainer",
|
|
"sidebar",
|
|
]) {
|
|
if (!external?.[key]) {
|
|
throw new Error(`[Majoor] mountAssetsManagerPanelRuntime now requires external.${key}`);
|
|
}
|
|
}
|
|
|
|
// -- 3. STATE BRIDGE + READ/WRITE HELPERS ------------------------------
|
|
const panelStateBridge = createPanelStateBridge(null, [
|
|
"searchQuery",
|
|
"scrollTop",
|
|
"scope",
|
|
"customRootId",
|
|
"customRootLabel",
|
|
"currentFolderRelativePath",
|
|
"sort",
|
|
"lastGridCount",
|
|
"lastGridTotal",
|
|
"activeAssetId",
|
|
"selectedAssetIds",
|
|
"sidebarOpen",
|
|
"collectionId",
|
|
"collectionName",
|
|
"viewScope",
|
|
"similarResults",
|
|
"similarTitle",
|
|
"similarSourceAssetId",
|
|
]);
|
|
state.viewScope =
|
|
String(state.viewScope || "")
|
|
.trim()
|
|
.toLowerCase() === "similar"
|
|
? "similar"
|
|
: "";
|
|
state.similarResults = Array.isArray(state.similarResults) ? state.similarResults : [];
|
|
state.similarTitle = String(state.similarTitle || "");
|
|
state.similarSourceAssetId = String(state.similarSourceAssetId || "");
|
|
|
|
const readPanelValue = (key: string, fallback: any = "") =>
|
|
panelStateBridge.read(key, (state as any)?.[key] ?? fallback);
|
|
const writePanelValue = (key: any, value: any) => panelStateBridge.write(key, value);
|
|
const readSelectedAssetIds = () => {
|
|
const selected = readPanelValue("selectedAssetIds", []);
|
|
return Array.isArray(selected) ? selected.map(String).filter(Boolean) : [];
|
|
};
|
|
const readActiveAssetId = () => String(readPanelValue("activeAssetId", "") || "").trim();
|
|
const isSidebarOpen = () => !!readPanelValue("sidebarOpen", false);
|
|
|
|
// -- 4. DESTRUCTURE EXTERNAL ELEMENTS ----------------------------------
|
|
const {
|
|
header,
|
|
tabButtons,
|
|
customMenuBtn,
|
|
filterBtn,
|
|
sortBtn,
|
|
collectionsBtn,
|
|
pinnedFoldersBtn,
|
|
messageBtn,
|
|
settingsBtn,
|
|
customPopover,
|
|
customSelect,
|
|
customAddBtn,
|
|
customRemoveBtn,
|
|
filterPopover,
|
|
kindSelect,
|
|
wfCheckbox,
|
|
workflowTypeSelect,
|
|
workflowIdInput,
|
|
workflowModelInput,
|
|
workflowRunsOnSelect,
|
|
ratingSelect,
|
|
minSizeInput,
|
|
maxSizeInput,
|
|
resolutionPresetSelect,
|
|
minWidthInput,
|
|
minHeightInput,
|
|
maxWidthInput,
|
|
maxHeightInput,
|
|
dateRangeSelect,
|
|
dateExactInput,
|
|
agendaContainer,
|
|
sortPopover,
|
|
sortMenu,
|
|
collectionsPopover,
|
|
pinnedFoldersPopover,
|
|
pinnedFoldersController,
|
|
pinnedFoldersMenu,
|
|
messagePopoverTitle,
|
|
messagePopover,
|
|
messageTabBtn,
|
|
messageTabBadge,
|
|
historyTabBtn,
|
|
historyTabBadge,
|
|
historyTabCount,
|
|
historyPanel,
|
|
shortcutsTabBtn,
|
|
messageList,
|
|
shortcutsPanel,
|
|
markReadBtn,
|
|
searchSection,
|
|
searchInputEl,
|
|
similarBtn,
|
|
similarPopover,
|
|
similarFindBtn,
|
|
similarDuplicatesBtn,
|
|
similarSameNodeBtn,
|
|
similarSameWorkflowBtn,
|
|
setSemanticEnabled,
|
|
_headerDispose,
|
|
} = external.headerSection;
|
|
const hasVueHeaderSection = !!external?.headerSection?.isVueHeader;
|
|
|
|
// Restore persistent search query.
|
|
if (panelStateBridge.read("searchQuery", "")) {
|
|
searchInputEl.value = panelStateBridge.read("searchQuery", "");
|
|
}
|
|
searchInputEl.addEventListener(
|
|
"input",
|
|
(e: any) => {
|
|
panelStateBridge.write("searchQuery", e.target.value);
|
|
},
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
|
|
const summaryBar = external.summaryBar;
|
|
const updateSummaryBar = external.updateSummaryBar;
|
|
const folderBreadcrumb = external.folderBreadcrumb;
|
|
const folderBreadcrumbController = external.folderBreadcrumbController || null;
|
|
const statusSection = external.statusSection;
|
|
const statusDot = external.statusDot;
|
|
const statusText = external.statusText;
|
|
const capabilitiesSection = external.capabilitiesSection;
|
|
const browseSection = external.browseSection;
|
|
const gridWrapper = external.gridWrapper;
|
|
const sidebar = external.sidebar;
|
|
const settings = loadMajoorSettings();
|
|
|
|
const content = document.createElement("div");
|
|
content.classList.add("mjr-am-content");
|
|
let gridContainer: any = null;
|
|
let sidebarController: any = null;
|
|
|
|
container.tabIndex = -1;
|
|
|
|
// -- 5. SCROLL PERSISTENCE + GRID CONTAINER SETUP ----------------------
|
|
let _scrollTimer: any = null;
|
|
let _lastUserInteractionAt = 0;
|
|
const markUserInteraction = () => {
|
|
try {
|
|
_lastUserInteractionAt = Date.now();
|
|
} catch {
|
|
_lastUserInteractionAt = 0;
|
|
}
|
|
};
|
|
const hasVueGridHostState =
|
|
typeof external?.bindGridHostState === "function" &&
|
|
typeof external?.restoreGridUiState === "function";
|
|
const shouldPersistScrollFromPanel = !hasVueGridHostState;
|
|
gridWrapper.addEventListener(
|
|
"scroll",
|
|
() => {
|
|
markUserInteraction();
|
|
if (!shouldPersistScrollFromPanel) return;
|
|
if (_scrollTimer) clearTimeout(_scrollTimer);
|
|
_scrollTimer = setTimeout(() => {
|
|
try {
|
|
panelStateBridge.write("scrollTop", gridWrapper.scrollTop);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}, 100);
|
|
},
|
|
{ passive: true, signal: panelLifecycleAC?.signal },
|
|
);
|
|
|
|
gridContainer = external.gridContainer;
|
|
if (gridContainer.parentElement !== gridWrapper) {
|
|
gridWrapper.appendChild(gridContainer);
|
|
}
|
|
setActiveGridContainer(gridContainer);
|
|
try {
|
|
gridContainer.dataset.mjrScope = String(
|
|
readPanelValue("scope", state.scope || "output") || "output",
|
|
);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const summaryBarDisposers: any[] = [];
|
|
const registerSummaryDispose = (fn: any) => {
|
|
if (typeof fn === "function") summaryBarDisposers.push(fn);
|
|
};
|
|
gridContainer._mjrSummaryBarDispose = () => {
|
|
for (const fn of summaryBarDisposers.splice(0)) {
|
|
try {
|
|
fn();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
};
|
|
try {
|
|
const handler = () => {
|
|
try {
|
|
updateSummaryBar?.({ state, gridContainer });
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
gridContainer.addEventListener("mjr:grid-stats", handler, {
|
|
signal: panelLifecycleAC?.signal,
|
|
});
|
|
registerSummaryDispose(() => {
|
|
try {
|
|
gridContainer.removeEventListener("mjr:grid-stats", handler);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
bindGridScanListeners();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
// -- 6. GRID API FUNCTION RESOLUTION -----------------------------------
|
|
const loadAssetsFn = external.loadAssets ?? loadAssets;
|
|
const loadAssetsFromListFn = external.loadAssetsFromList ?? loadAssetsFromList;
|
|
const prepareGridForScopeSwitchFn =
|
|
external.prepareGridForScopeSwitch ?? prepareGridForScopeSwitch;
|
|
const disposeGridFn = external.disposeGrid ?? disposeGrid;
|
|
const refreshGridFn = external.refreshGrid ?? refreshGrid;
|
|
const captureAnchorFn = external.captureAnchor ?? captureAnchor;
|
|
const restoreAnchorFn = external.restoreAnchor ?? restoreAnchor;
|
|
const hydrateGridFromSnapshotFn = external.hydrateGridFromSnapshot ?? hydrateGridFromSnapshot;
|
|
|
|
// -- 7. GRID CONTEXT MENU ----------------------------------------------
|
|
if (typeof external?.onGridContainerReady === "function") {
|
|
try {
|
|
external.onGridContainerReady(gridContainer, { getState: () => state });
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
} else {
|
|
container._mjrGridContextMenuUnbind = bindGridContextMenu({
|
|
gridContainer,
|
|
getState: () => state as Record<string, any>,
|
|
});
|
|
}
|
|
|
|
// -- 8. SETTINGS SYNC --------------------------------------------------
|
|
const similarEnabledTitle = t("search.findSimilar", "Find Similar");
|
|
const similarDisabledTitle = t(
|
|
"search.similarDisabled",
|
|
"AI features are disabled in settings",
|
|
);
|
|
const { applyWatcherForScope, isAiEnabled } = createSettingsSync({
|
|
container,
|
|
state,
|
|
sidebar,
|
|
browseSection,
|
|
gridWrapper,
|
|
similarBtn,
|
|
setSemanticEnabled,
|
|
similarEnabledTitle,
|
|
similarDisabledTitle,
|
|
refreshGridFn,
|
|
isSidebarOpen,
|
|
readActiveAssetId,
|
|
getSidebarController: () => sidebarController,
|
|
panelLifecycleAC,
|
|
});
|
|
|
|
// -- 9. DOM ASSEMBLY ---------------------------------------------------
|
|
content.appendChild(statusSection);
|
|
content.appendChild(searchSection);
|
|
content.appendChild(folderBreadcrumb);
|
|
content.appendChild(summaryBar);
|
|
content.appendChild(browseSection);
|
|
container.appendChild(header);
|
|
try {
|
|
container._mjrVersionUpdateCleanup?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
container._mjrVersionUpdateCleanup = _headerDispose || header._mjrVersionUpdateCleanup;
|
|
container.appendChild(content);
|
|
|
|
// -- 10. GRID CONTROLLER -----------------------------------------------
|
|
const getQuery = () => normalizeQuery(searchInputEl);
|
|
const gridController = createGridController({
|
|
gridContainer,
|
|
loadAssets: loadAssetsFn,
|
|
loadAssetsFromList: loadAssetsFromListFn,
|
|
getCollectionAssets,
|
|
disposeGrid: disposeGridFn,
|
|
getQuery,
|
|
searchInputEl,
|
|
state,
|
|
});
|
|
|
|
// -- 11. SHARED CONTROLLER VARIABLES ----------------------------------
|
|
let scopeController: any = null;
|
|
let contextController: any = null;
|
|
let _duplicatesAlert: any = null;
|
|
let _dupPollTimer: any = null;
|
|
let _autoLoadTimer: any = null;
|
|
let browserNav: any = null;
|
|
let _unbindBrowserFolderNav: any = null;
|
|
let selectionState: any = null;
|
|
let assetsQueryController: any = null;
|
|
let settingsDialogOpen = false;
|
|
|
|
const isPopoverOpen = (popover: any) =>
|
|
!!popover && String(popover?.style?.display || "").toLowerCase() === "block";
|
|
|
|
const syncOpenPopoverButtonStates = () => {
|
|
const pairs = [
|
|
[filterBtn, filterPopover],
|
|
[sortBtn, sortPopover],
|
|
[collectionsBtn, collectionsPopover],
|
|
[pinnedFoldersBtn, pinnedFoldersPopover],
|
|
[messageBtn, messagePopover],
|
|
[similarBtn, similarPopover],
|
|
];
|
|
for (const [button, popover] of pairs) {
|
|
try {
|
|
button?.classList?.toggle?.("mjr-popover-open", isPopoverOpen(popover));
|
|
button?.setAttribute?.("aria-expanded", isPopoverOpen(popover) ? "true" : "false");
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
try {
|
|
settingsBtn?.classList?.toggle?.("mjr-popover-open", settingsDialogOpen);
|
|
settingsBtn?.setAttribute?.("aria-expanded", settingsDialogOpen ? "true" : "false");
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
const notifyContextChanged = () => {
|
|
try {
|
|
contextController?.update?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
browserNav?.renderBreadcrumb?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
syncOpenPopoverButtonStates();
|
|
};
|
|
|
|
// -- 12. SELECTION STATE + BROWSER NAV --------------------------------
|
|
if (!hasVueGridHostState) {
|
|
selectionState = bindGridSelectionState({
|
|
gridContainer,
|
|
readSelectedAssetIds,
|
|
readActiveAssetId: () => readActiveAssetId(),
|
|
writeSelectedAssetIds: (ids: any) => writePanelValue("selectedAssetIds", ids),
|
|
writeActiveAssetId: (id: any) => writePanelValue("activeAssetId", id),
|
|
onSelectionChanged: () => {
|
|
markUserInteraction();
|
|
notifyContextChanged();
|
|
},
|
|
lifecycleSignal: panelLifecycleAC?.signal || null,
|
|
});
|
|
}
|
|
browserNav = createBrowserNavigationController({
|
|
state,
|
|
gridContainer,
|
|
folderBreadcrumb,
|
|
folderBreadcrumbController,
|
|
customSelect,
|
|
reloadGrid: () => gridController.reloadGrid(),
|
|
clearSelection: () => {
|
|
try {
|
|
gridContainer?._mjrSetSelection?.([], "");
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
onContextChanged: () => {
|
|
try {
|
|
contextController?.update?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
lifecycleSignal: panelLifecycleAC?.signal || null,
|
|
});
|
|
try {
|
|
_unbindBrowserFolderNav = browserNav.bindGridFolderNavigation();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
// -- 13. DUPLICATE ALERTS ---------------------------------------------
|
|
const refreshDuplicateAlerts = async () => {
|
|
const scope = String(
|
|
readPanelValue("scope", state.scope || "output") || "output",
|
|
).toLowerCase();
|
|
const customRootId = String(
|
|
readPanelValue("customRootId", state.customRootId || "") || "",
|
|
).trim();
|
|
if (scope === "custom" && !customRootId) {
|
|
_duplicatesAlert = null;
|
|
notifyContextChanged();
|
|
return;
|
|
}
|
|
try {
|
|
const result = await getDuplicateAlerts(
|
|
{ scope, customRootId, maxGroups: 4, maxPairs: 4 },
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
if (!result?.ok) return;
|
|
const data = result.data || {};
|
|
_duplicatesAlert = {
|
|
exactCount: Array.isArray(data.exact_groups) ? data.exact_groups.length : 0,
|
|
similarCount: Array.isArray(data.similar_pairs) ? data.similar_pairs.length : 0,
|
|
firstGroup: Array.isArray(data.exact_groups) ? data.exact_groups[0] : null,
|
|
firstPair: Array.isArray(data.similar_pairs) ? data.similar_pairs[0] : null,
|
|
};
|
|
notifyContextChanged();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
// -- 14. QUEUED RELOAD -------------------------------------------------
|
|
const REQUEST_QUEUED_RELOAD_DEBOUNCE_MS = 120;
|
|
let _queuedReloadTimer: any = null;
|
|
const requestQueuedReload = () => {
|
|
try {
|
|
if (_queuedReloadTimer) clearTimeout(_queuedReloadTimer);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
_queuedReloadTimer = setTimeout(() => {
|
|
_queuedReloadTimer = null;
|
|
queuedReload().catch((err) => {
|
|
try {
|
|
console.warn("[Majoor] queuedReload failed", err);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
});
|
|
}, REQUEST_QUEUED_RELOAD_DEBOUNCE_MS);
|
|
};
|
|
|
|
// -- 15. GRID EVENT BINDINGS -------------------------------------------
|
|
bindGridEvents({
|
|
gridContainer,
|
|
panelLifecycleAC,
|
|
requestQueuedReload,
|
|
notifyContextChanged,
|
|
markUserInteraction,
|
|
writePanelValue,
|
|
popovers,
|
|
collectionsPopover,
|
|
gridController,
|
|
registerSummaryDispose,
|
|
});
|
|
|
|
// -- 16. CUSTOM ROOTS CONTROLLER ---------------------------------------
|
|
const customRootsController = createCustomRootsController({
|
|
state,
|
|
customSelect,
|
|
customRemoveBtn,
|
|
comfyConfirm,
|
|
comfyPrompt,
|
|
comfyToast,
|
|
get,
|
|
post,
|
|
ENDPOINTS,
|
|
reloadGrid: gridController.reloadGrid,
|
|
onRootChanged: async () => {
|
|
try {
|
|
browserNav?.resetHistory?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
await applyWatcherForScope(state.scope);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
await refreshDuplicateAlerts();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
});
|
|
const disposeCustomRootsBindings = customRootsController.bind({
|
|
customAddBtn,
|
|
customRemoveBtn,
|
|
});
|
|
try {
|
|
window.addEventListener(
|
|
"mjr:custom-roots-changed",
|
|
async (e: Event) => {
|
|
const preferredId = String((e as CustomEvent)?.detail?.preferredId || "").trim() || null;
|
|
await customRootsController.refreshCustomRoots(preferredId);
|
|
},
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
// -- 17. RECONCILE VISIBLE SELECTION ----------------------------------
|
|
const reconcileVisibleSelection = () => {
|
|
try {
|
|
const getAssets = gridContainer?._mjrGetAssets;
|
|
const setSelection = gridContainer?._mjrSetSelection;
|
|
if (typeof getAssets !== "function" || typeof setSelection !== "function") return;
|
|
const visibleIds = (Array.isArray(getAssets()) ? getAssets() : [])
|
|
.map((asset: any) => String(asset?.id ?? "").trim())
|
|
.filter(Boolean);
|
|
const selectedIds = readSelectedAssetIds();
|
|
const next = selectedIds.filter((id) => visibleIds.includes(id));
|
|
const pruned = selectedIds.length - next.length;
|
|
if (pruned <= 0) return;
|
|
const currentActiveId = readActiveAssetId();
|
|
const nextActiveId = next.includes(currentActiveId) ? currentActiveId : next[0] || "";
|
|
setSelection(next, nextActiveId);
|
|
comfyToast(
|
|
t(
|
|
"toast.selectionPruned",
|
|
"{count} items deselected - not visible in current view",
|
|
{ count: pruned },
|
|
),
|
|
"info",
|
|
2600,
|
|
{ noHistory: true },
|
|
);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
// -- 18. SCOPE CONTROLLER ---------------------------------------------
|
|
scopeController = createScopeController({
|
|
state,
|
|
tabButtons,
|
|
customMenuBtn,
|
|
customPopover,
|
|
popovers,
|
|
refreshCustomRoots: customRootsController.refreshCustomRoots,
|
|
reloadGrid: gridController.reloadGrid,
|
|
reconcileSelection: reconcileVisibleSelection,
|
|
onChanged: () => {
|
|
try {
|
|
if (gridContainer) {
|
|
gridContainer.dataset.mjrScope = String(
|
|
readPanelValue("scope", state.scope || "output") || "output",
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
notifyContextChanged();
|
|
},
|
|
onScopeChanged: async () => {
|
|
try {
|
|
await applyWatcherForScope(
|
|
String(readPanelValue("scope", state.scope || "output") || "output"),
|
|
);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
await refreshDuplicateAlerts();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
onBeforeReload: async () => {
|
|
// Sync key dataset fields *before* prepareGridForScopeSwitch so that
|
|
// snapshot-cache hydration uses the correct (new-scope) key.
|
|
try {
|
|
gridContainer.dataset.mjrScope = readPanelValue("scope", "output");
|
|
gridContainer.dataset.mjrCustomRootId = readPanelValue("customRootId", "") || "";
|
|
gridContainer.dataset.mjrSubfolder =
|
|
readPanelValue("currentFolderRelativePath", "") || "";
|
|
gridContainer.dataset.mjrCollectionId = readPanelValue("collectionId", "") || "";
|
|
gridContainer.dataset.mjrViewScope = readPanelValue("viewScope", "") || "";
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
prepareGridForScopeSwitchFn(gridContainer);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
await updateStatus(
|
|
statusDot,
|
|
statusText,
|
|
capabilitiesSection,
|
|
{
|
|
scope: readPanelValue("scope", state.scope || "output"),
|
|
customRootId: readPanelValue("customRootId", state.customRootId || ""),
|
|
},
|
|
null,
|
|
{ signal: panelLifecycleAC?.signal || null },
|
|
);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
});
|
|
|
|
const exitSimilarViewIfActive = async ({ reload = false } = {}) => {
|
|
try {
|
|
if (
|
|
String(readPanelValue("viewScope", state.viewScope || "") || "")
|
|
.trim()
|
|
.toLowerCase() !== "similar"
|
|
)
|
|
return;
|
|
await scopeController?.clearSimilarScope?.({ reload });
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
Object.values(tabButtons as Record<string, any>).forEach((btn) => {
|
|
btn.addEventListener("click", () => scopeController.setScope(btn.dataset.scope), {
|
|
signal: panelLifecycleAC?.signal,
|
|
});
|
|
});
|
|
|
|
// -- 19. MESSAGE POPOVER -----------------------------------------------
|
|
bindMessagePopoverController({
|
|
messageBtn,
|
|
messagePopover,
|
|
title: messagePopoverTitle,
|
|
messageList,
|
|
historyTabBtn,
|
|
historyTabBadge,
|
|
historyTabCount,
|
|
historyPanel,
|
|
shortcutsPanel,
|
|
messageTabBtn,
|
|
messageTabBadge,
|
|
shortcutsTabBtn,
|
|
markReadBtn,
|
|
popovers,
|
|
signal: panelLifecycleAC?.signal,
|
|
onBeforeToggle: () => {
|
|
popovers.close(customPopover);
|
|
popovers.close(filterPopover);
|
|
popovers.close(sortPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(pinnedFoldersPopover);
|
|
},
|
|
});
|
|
|
|
// -- 20. POPOVER BUTTONS -----------------------------------------------
|
|
popovers.setDismissWhitelist([
|
|
customPopover,
|
|
filterPopover,
|
|
sortPopover,
|
|
collectionsPopover,
|
|
pinnedFoldersPopover,
|
|
messagePopover,
|
|
similarPopover,
|
|
customMenuBtn,
|
|
filterBtn,
|
|
sortBtn,
|
|
collectionsBtn,
|
|
pinnedFoldersBtn,
|
|
messageBtn,
|
|
similarBtn,
|
|
]);
|
|
popovers.setOnVisibilityChanged?.(() => syncOpenPopoverButtonStates());
|
|
try {
|
|
window.addEventListener(
|
|
MAJOOR_SETTINGS_DIALOG_EVENT,
|
|
(event: any) => {
|
|
settingsDialogOpen = Boolean(event?.detail?.open);
|
|
syncOpenPopoverButtonStates();
|
|
},
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
syncOpenPopoverButtonStates();
|
|
|
|
customMenuBtn.addEventListener(
|
|
"click",
|
|
(e: any) => {
|
|
e.stopPropagation();
|
|
popovers.close(filterPopover);
|
|
popovers.close(sortPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(similarPopover);
|
|
popovers.toggle(customPopover, customMenuBtn);
|
|
},
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
filterBtn.addEventListener(
|
|
"click",
|
|
(e: any) => {
|
|
e.stopPropagation();
|
|
popovers.close(customPopover);
|
|
popovers.close(sortPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(similarPopover);
|
|
popovers.toggle(filterPopover, filterBtn);
|
|
},
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
collectionsBtn.addEventListener(
|
|
"click",
|
|
(e: any) => {
|
|
e.stopPropagation();
|
|
popovers.close(customPopover);
|
|
popovers.close(filterPopover);
|
|
popovers.close(sortPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(similarPopover);
|
|
popovers.toggle(collectionsPopover, collectionsBtn);
|
|
},
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
|
|
const sortController = hasVueHeaderSection
|
|
? null
|
|
: createSortController({
|
|
state,
|
|
sortBtn,
|
|
sortMenu,
|
|
sortPopover,
|
|
popovers,
|
|
reloadGrid: gridController.reloadGrid,
|
|
onChanged: () => notifyContextChanged(),
|
|
});
|
|
if (sortController) {
|
|
sortController.bind({
|
|
onBeforeToggle: () => {
|
|
popovers.close(customPopover);
|
|
popovers.close(filterPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(similarPopover);
|
|
},
|
|
});
|
|
} else {
|
|
sortBtn.addEventListener(
|
|
"click",
|
|
(e: any) => {
|
|
e.stopPropagation();
|
|
popovers.close(customPopover);
|
|
popovers.close(filterPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(similarPopover);
|
|
popovers.toggle(sortPopover, sortBtn);
|
|
},
|
|
{ signal: panelLifecycleAC?.signal },
|
|
);
|
|
}
|
|
|
|
// -- 21. SIMILAR SEARCH ------------------------------------------------
|
|
bindSimilarSearch({
|
|
similarBtn,
|
|
similarPopover,
|
|
similarFindBtn,
|
|
similarDuplicatesBtn,
|
|
similarSameNodeBtn,
|
|
similarSameWorkflowBtn,
|
|
gridContainer,
|
|
state,
|
|
panelLifecycleAC,
|
|
isAiEnabled,
|
|
similarDisabledTitle,
|
|
readActiveAssetId,
|
|
readSelectedAssetIds,
|
|
readPanelValue,
|
|
writePanelValue,
|
|
scopeController,
|
|
closePopovers: () => {
|
|
popovers.close(customPopover);
|
|
popovers.close(filterPopover);
|
|
popovers.close(sortPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(similarPopover);
|
|
},
|
|
closePeerPopovers: () => {
|
|
popovers.close(customPopover);
|
|
popovers.close(filterPopover);
|
|
popovers.close(sortPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(pinnedFoldersPopover);
|
|
},
|
|
popovers,
|
|
workflowIdInput,
|
|
reloadGrid: () => gridController.reloadGrid(),
|
|
getDuplicatesAlert: () => _duplicatesAlert,
|
|
});
|
|
|
|
// -- 22. PINNED FOLDERS ------------------------------------------------
|
|
bindPinnedFolders({
|
|
pinnedFoldersBtn,
|
|
pinnedFoldersController,
|
|
pinnedFoldersMenu,
|
|
pinnedFoldersPopover,
|
|
popovers,
|
|
closeOtherPopovers: () => {
|
|
popovers.close(customPopover);
|
|
popovers.close(filterPopover);
|
|
popovers.close(sortPopover);
|
|
popovers.close(collectionsPopover);
|
|
popovers.close(messagePopover);
|
|
popovers.close(similarPopover);
|
|
},
|
|
state,
|
|
gridContainer,
|
|
customSelect,
|
|
scopeController,
|
|
customRootsController,
|
|
gridController,
|
|
applyWatcherForScope,
|
|
refreshDuplicateAlerts,
|
|
notifyContextChanged,
|
|
panelLifecycleAC,
|
|
});
|
|
|
|
// -- 23. FILTER INIT ---------------------------------------------------
|
|
const { agendaCalendar, disposeFilters } = setupFiltersInit({
|
|
state,
|
|
hasVueHeaderSection,
|
|
kindSelect,
|
|
wfCheckbox,
|
|
workflowTypeSelect,
|
|
workflowIdInput,
|
|
workflowModelInput,
|
|
workflowRunsOnSelect,
|
|
ratingSelect,
|
|
minSizeInput,
|
|
maxSizeInput,
|
|
resolutionPresetSelect,
|
|
minWidthInput,
|
|
minHeightInput,
|
|
maxWidthInput,
|
|
maxHeightInput,
|
|
dateRangeSelect,
|
|
dateExactInput,
|
|
agendaContainer,
|
|
gridController,
|
|
reconcileVisibleSelection,
|
|
exitSimilarViewIfActive,
|
|
notifyContextChanged,
|
|
panelLifecycleAC,
|
|
popovers,
|
|
filterPopover,
|
|
});
|
|
|
|
// -- 24. CONTEXT CONTROLLER --------------------------------------------
|
|
const extraActions = buildContextMenuExtraActions({
|
|
state,
|
|
scopeController,
|
|
browserNav,
|
|
gridController,
|
|
getDuplicatesAlert: () => _duplicatesAlert,
|
|
refreshDuplicateAlerts,
|
|
});
|
|
try {
|
|
contextController = createContextController({
|
|
state,
|
|
gridContainer,
|
|
filterBtn,
|
|
sortBtn,
|
|
collectionsBtn,
|
|
searchInputEl,
|
|
kindSelect,
|
|
wfCheckbox,
|
|
workflowTypeSelect,
|
|
workflowIdInput,
|
|
workflowModelInput,
|
|
workflowRunsOnSelect,
|
|
ratingSelect,
|
|
minSizeInput,
|
|
maxSizeInput,
|
|
minWidthInput,
|
|
minHeightInput,
|
|
maxWidthInput,
|
|
maxHeightInput,
|
|
resolutionPresetSelect,
|
|
dateRangeSelect,
|
|
dateExactInput,
|
|
scopeController,
|
|
sortController,
|
|
updateSummaryBar,
|
|
reloadGrid: () => gridController.reloadGrid(),
|
|
getExtraContext: () => ({ duplicatesAlert: _duplicatesAlert }),
|
|
extraActions,
|
|
});
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
// -- 25. SUMMARY BAR UPDATES -------------------------------------------
|
|
try {
|
|
notifyContextChanged();
|
|
if (hasVueGridHostState) {
|
|
const cleanup = external.bindGridHostState({
|
|
onContextChanged: notifyContextChanged,
|
|
markUserInteraction,
|
|
});
|
|
registerSummaryDispose(cleanup);
|
|
} else {
|
|
const onStats = () => notifyContextChanged();
|
|
gridContainer.addEventListener("mjr:grid-stats", onStats, {
|
|
signal: panelLifecycleAC?.signal,
|
|
});
|
|
window.addEventListener?.("mjr-settings-changed", onStats, {
|
|
signal: panelLifecycleAC?.signal,
|
|
});
|
|
let raf: any = null;
|
|
let debounceTimer: any = null;
|
|
const schedule = () => {
|
|
// Debounce MutationObserver callbacks during pagination bursts
|
|
// (many cards inserted rapidly). Coalesce to a single update.
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(() => {
|
|
debounceTimer = null;
|
|
if (raf) cancelAnimationFrame(raf);
|
|
raf = requestAnimationFrame(() => {
|
|
raf = null;
|
|
notifyContextChanged();
|
|
});
|
|
}, 250);
|
|
};
|
|
const mo = new MutationObserver(() => schedule());
|
|
mo.observe(gridContainer, { childList: true });
|
|
gridContainer._mjrSummaryBarObserver = mo;
|
|
registerSummaryDispose(() => {
|
|
try {
|
|
gridContainer.removeEventListener("mjr:grid-stats", onStats);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
window.removeEventListener?.("mjr-settings-changed", onStats);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
mo.disconnect();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
// -- 26. SIDEBAR CONTROLLER --------------------------------------------
|
|
sidebarController = bindSidebarOpen({
|
|
gridContainer,
|
|
sidebar,
|
|
createRatingBadge,
|
|
createTagsBadge,
|
|
showAssetInSidebar,
|
|
closeSidebar,
|
|
state,
|
|
});
|
|
|
|
// -- 27. ASSETS QUERY CONTROLLER ---------------------------------------
|
|
const assetsQueryOptions = {
|
|
gridController,
|
|
captureAnchor: captureAnchorFn,
|
|
restoreAnchor: restoreAnchorFn,
|
|
restoreGridUiState: hasVueGridHostState
|
|
? (initialLoadPromise: any) =>
|
|
external.restoreGridUiState(initialLoadPromise, {
|
|
onRestoreSidebar: () => {
|
|
try {
|
|
sidebarController?.toggleDetails?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
})
|
|
: null,
|
|
readScrollTop: () => panelStateBridge.read("scrollTop", (state as any).scrollTop || 0),
|
|
restoreSelectionState: ({ scrollTop }: { scrollTop?: number } = {}) => {
|
|
selectionState?.restoreSelectionState?.({ scrollTop });
|
|
},
|
|
readActiveAssetId,
|
|
isSidebarOpen,
|
|
toggleSidebarDetails: () => {
|
|
sidebarController?.toggleDetails?.();
|
|
},
|
|
getQuery,
|
|
getScope: () => panelStateBridge.read("scope", (state as any).scope),
|
|
loadAssets: loadAssetsFn,
|
|
lifecycleSignal: panelLifecycleAC?.signal || null,
|
|
};
|
|
if (typeof external?.initAssetsQueryController === "function") {
|
|
assetsQueryController = external.initAssetsQueryController(assetsQueryOptions);
|
|
} else {
|
|
assetsQueryController = createAssetsQueryController({
|
|
gridContainer,
|
|
gridWrapper,
|
|
...assetsQueryOptions,
|
|
});
|
|
}
|
|
|
|
// -- 28. HOTKEYS -------------------------------------------------------
|
|
const hotkeys = createPanelHotkeysController({
|
|
onTriggerScan: (ctx: any) => {
|
|
triggerScan(statusDot, statusText, capabilitiesSection, ctx || {});
|
|
},
|
|
onToggleDetails: () => {
|
|
sidebarController?.toggleDetails?.();
|
|
},
|
|
onToggleFloatingViewer: () => {
|
|
try {
|
|
window.dispatchEvent(new Event(EVENTS.MFV_TOGGLE));
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
onFocusSearch: () => {
|
|
try {
|
|
searchInputEl?.focus?.();
|
|
const len = searchInputEl?.value?.length || 0;
|
|
searchInputEl?.setSelectionRange?.(len, len);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
onClearSearch: () => {
|
|
try {
|
|
if (!searchInputEl) return;
|
|
writePanelValue("searchQuery", "");
|
|
searchInputEl.value = "";
|
|
searchInputEl.dispatchEvent?.(new Event("input", { bubbles: true }));
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
},
|
|
getScanContext: () => ({
|
|
scope: panelStateBridge.read("scope", state.scope),
|
|
customRootId: panelStateBridge.read("customRootId", state.customRootId),
|
|
}),
|
|
});
|
|
const ratingHotkeys = createRatingHotkeysController({ gridContainer, createRatingBadge });
|
|
|
|
let disposeSearchBinding: any = null;
|
|
if (container._eventCleanup) {
|
|
try {
|
|
container._eventCleanup();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
hotkeys.bind(content);
|
|
ratingHotkeys.bind();
|
|
setHotkeysScope("grid");
|
|
|
|
try {
|
|
container._mjrHotkeys = hotkeys;
|
|
container._mjrRatingHotkeys = ratingHotkeys;
|
|
container._mjrSidebarController = sidebarController;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
// -- 29. CLEANUP CLOSURE -----------------------------------------------
|
|
container._eventCleanup = () => {
|
|
try {
|
|
panelLifecycleAC?.abort?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
statusSection?._mjrStatusPollDispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
container._mjrVersionUpdateCleanup?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
container._mjrVersionUpdateCleanup = null;
|
|
try {
|
|
if (_scrollTimer) clearTimeout(_scrollTimer);
|
|
_scrollTimer = null;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
hotkeys.dispose();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
ratingHotkeys.dispose();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
if (getHotkeysState().scope === "grid") setHotkeysScope(null);
|
|
try {
|
|
agendaCalendar?.dispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
disposeFilters?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
disposeSearchBinding?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
disposeSearchBinding = null;
|
|
try {
|
|
sidebar?.dispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
sidebarController?.dispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
if (_dupPollTimer) clearInterval(_dupPollTimer);
|
|
_dupPollTimer = null;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
if (_autoLoadTimer) clearTimeout(_autoLoadTimer);
|
|
_autoLoadTimer = null;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
assetsQueryController?.dispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
assetsQueryController = null;
|
|
try {
|
|
selectionState?.dispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
selectionState = null;
|
|
try {
|
|
gridContainer?._mjrSummaryBarDispose?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
gridContainer?._mjrSummaryBarObserver?.disconnect?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
gridContainer?._mjrGridContextMenuUnbind?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
disposeCustomRootsBindings?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
_unbindBrowserFolderNav?.();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
disposeGridScanListeners();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
if (gridContainer) disposeGridFn(gridContainer);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
clearActiveGridContainer(gridContainer);
|
|
gridContainer = null;
|
|
try {
|
|
popovers.dispose();
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
if (container._mjrPopoverManager === popovers) container._mjrPopoverManager = null;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
if (hostWrapper && hostWrapperPrevStyle) {
|
|
hostWrapper.style.height = hostWrapperPrevStyle.height;
|
|
hostWrapper.style.minHeight = hostWrapperPrevStyle.minHeight;
|
|
hostWrapper.style.width = hostWrapperPrevStyle.width;
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
};
|
|
|
|
// -- 30. INITIAL LOAD SEQUENCE -----------------------------------------
|
|
const queuedReload = async () => {
|
|
await assetsQueryController?.queuedReload?.();
|
|
};
|
|
const handleCountersUpdate =
|
|
assetsQueryController?.createCountersUpdateHandler?.({
|
|
state,
|
|
getStableQuery: () => String(gridContainer?.dataset?.mjrQuery || "*").trim() || "*",
|
|
getRecentUserInteractionAt: () => Number(_lastUserInteractionAt || 0),
|
|
}) ??
|
|
(async () => {
|
|
await queuedReload();
|
|
});
|
|
|
|
setupStatusPolling(
|
|
statusDot,
|
|
statusText,
|
|
statusSection,
|
|
capabilitiesSection,
|
|
handleCountersUpdate,
|
|
() => ({
|
|
scope: panelStateBridge.read("scope", state.scope),
|
|
customRootId: panelStateBridge.read("customRootId", state.customRootId),
|
|
}),
|
|
);
|
|
|
|
scopeController.setActiveTabStyles();
|
|
browserNav?.renderBreadcrumb?.();
|
|
customRootsController.refreshCustomRoots().catch(() => {});
|
|
|
|
disposeSearchBinding = assetsQueryController?.bindSearchInput?.({
|
|
searchInputEl,
|
|
lifecycleSignal: panelLifecycleAC?.signal || null,
|
|
debounceMs: 120,
|
|
onQueryChanged: () => {
|
|
void exitSimilarViewIfActive({ reload: false });
|
|
notifyContextChanged();
|
|
},
|
|
onBeforeReload: () => {
|
|
void exitSimilarViewIfActive({ reload: false });
|
|
notifyContextChanged();
|
|
},
|
|
startSearchTimer: () => {
|
|
startTimer("searchQuery");
|
|
},
|
|
reloadGrid: () => {
|
|
gridController.reloadGrid();
|
|
},
|
|
});
|
|
|
|
let didHydrateFromSnapshot = false;
|
|
try {
|
|
didHydrateFromSnapshot = await hydrateGridFromSnapshotFn(
|
|
gridContainer,
|
|
{
|
|
scope: readPanelValue("scope", state.scope || "output") || "output",
|
|
query: String(
|
|
normalizeQuery(
|
|
panelStateBridge.read("searchQuery", state.searchQuery || "*") || "*",
|
|
) || "*",
|
|
),
|
|
customRootId: readPanelValue("customRootId", state.customRootId || "") || "",
|
|
subfolder:
|
|
readPanelValue(
|
|
"currentFolderRelativePath",
|
|
state.currentFolderRelativePath || "",
|
|
) || "",
|
|
collectionId: readPanelValue("collectionId", state.collectionId || "") || "",
|
|
viewScope: readPanelValue("viewScope", state.viewScope || "") || "",
|
|
kind: readPanelValue("kindFilter", state.kindFilter || "") || "",
|
|
workflowOnly: !!readPanelValue("workflowOnly", state.workflowOnly),
|
|
minRating: readPanelValue("minRating", state.minRating || "") || "",
|
|
minSizeMB: readPanelValue("minSizeMB", state.minSizeMB || "") || "",
|
|
maxSizeMB: readPanelValue("maxSizeMB", state.maxSizeMB || "") || "",
|
|
resolutionCompare:
|
|
readPanelValue("resolutionCompare", state.resolutionCompare || "") || "",
|
|
minWidth: readPanelValue("minWidth", state.minWidth || "") || "",
|
|
minHeight: readPanelValue("minHeight", state.minHeight || "") || "",
|
|
maxWidth: readPanelValue("maxWidth", state.maxWidth || "") || "",
|
|
maxHeight: readPanelValue("maxHeight", state.maxHeight || "") || "",
|
|
workflowType: readPanelValue("workflowType", state.workflowType || "") || "",
|
|
workflowId: readPanelValue("workflowId", state.workflowId || "") || "",
|
|
workflowModel: readPanelValue(
|
|
"workflowModelFilter",
|
|
state.workflowModelFilter || "",
|
|
) || "",
|
|
workflowRunsOn: readPanelValue(
|
|
"workflowRunsOnFilter",
|
|
state.workflowRunsOnFilter || "",
|
|
) || "",
|
|
dateRange: readPanelValue("dateRangeFilter", state.dateRangeFilter || "") || "",
|
|
dateExact: readPanelValue("dateExactFilter", state.dateExactFilter || "") || "",
|
|
sort: readPanelValue("sort", state.sort || "mtime_desc") || "mtime_desc",
|
|
},
|
|
{
|
|
title:
|
|
readPanelValue("collectionName", state.collectionName || "") ||
|
|
panelStateBridge.read("searchQuery", state.searchQuery || "") ||
|
|
"Cached",
|
|
},
|
|
);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
let hasExistingGridCards = false;
|
|
try {
|
|
hasExistingGridCards = !!gridContainer?.querySelector?.(".mjr-asset-card");
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
const pendingGeneratedAssetCount = consumePendingGeneratedAssets();
|
|
|
|
const initialLoadPromise =
|
|
!pendingGeneratedAssetCount && (didHydrateFromSnapshot || hasExistingGridCards)
|
|
? Promise.resolve({
|
|
ok: true,
|
|
hydrated: !!didHydrateFromSnapshot,
|
|
cached: !!hasExistingGridCards,
|
|
})
|
|
: gridController.reloadGrid({
|
|
reason: pendingGeneratedAssetCount
|
|
? "pending-generated-assets"
|
|
: "initial-load",
|
|
preserveVisibleUntilReady: !pendingGeneratedAssetCount,
|
|
});
|
|
|
|
// When we showed cached snapshot data on first launch, schedule a silent
|
|
// background refresh so the user always sees an up-to-date grid without
|
|
// waiting for the API on cold open. The refresh reuses preserveVisibleUntilReady
|
|
// so the cached cards remain visible until the fresh page arrives.
|
|
if (didHydrateFromSnapshot) {
|
|
try {
|
|
setTimeout(() => {
|
|
try {
|
|
if (panelLifecycleAC?.signal?.aborted) return;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
gridController.reloadGrid().catch(() => {});
|
|
}, 200);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
// Fire-and-forget: do not block the panel mount on the duplicate alerts
|
|
// request. It contended with the initial /list call on the single-threaded
|
|
// backend and added perceptible latency before the first paint.
|
|
refreshDuplicateAlerts().catch(() => {});
|
|
try {
|
|
_dupPollTimer = setInterval(() => {
|
|
try {
|
|
if (panelLifecycleAC?.signal?.aborted) return;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
refreshDuplicateAlerts().catch(() => {});
|
|
}, 15000);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
assetsQueryController?.restoreUiState?.(initialLoadPromise);
|
|
|
|
try {
|
|
(window as any).__mjrLastAssetUpsert = 0;
|
|
(window as any).__mjrLastAssetUpsertCount = 0;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
|
|
const renderDuration = endTimer("panelRender", "gridRender");
|
|
trackGridRender(renderDuration ?? 0);
|
|
|
|
return { gridContainer };
|
|
}
|