517 lines
20 KiB
TypeScript
517 lines
20 KiB
TypeScript
/**
|
|
* Majoor settings integration with ComfyUI settings panel.
|
|
*
|
|
* This module now has two separate responsibilities:
|
|
* - initialize runtime settings side effects once
|
|
* - build the static `settings:` extension payload used by ComfyUI_frontend
|
|
*/
|
|
|
|
import { safeDispatchCustomEvent } from "../../utils/events.js";
|
|
import { initI18n, setFollowComfyLanguage, startComfyLanguageSync } from "../i18n.js";
|
|
import { debounce } from "../../utils/debounce.js";
|
|
import { getWatcherStatus, toggleWatcher } from "../../api/client.js";
|
|
import { getSettingForApp, setSettingForApp } from "../hostAdapter.js";
|
|
|
|
import {
|
|
DEFAULT_SETTINGS,
|
|
loadMajoorSettings,
|
|
saveMajoorSettings,
|
|
applySettingsToConfig,
|
|
syncBackendSecuritySettings,
|
|
syncBackendVectorSearchSettings,
|
|
syncBackendExecutionGroupingSettings,
|
|
} from "./settingsCore.js";
|
|
import { startRuntimeStatusDashboard } from "./settingsRuntime.js";
|
|
import { registerGridSettings } from "./settingsGrid.js";
|
|
import { registerViewerSettings } from "./settingsViewer.js";
|
|
import { registerScanningSettings } from "./settingsScanning.js";
|
|
import { registerFeedSettings } from "./settingsFeed.js";
|
|
import { registerSecuritySettings } from "./settingsSecurity.js";
|
|
import { registerAdvancedSettings } from "./settingsAdvanced.js";
|
|
import { registerSearchSettings } from "./settingsSearch.js";
|
|
|
|
import { SETTINGS_KEY } from "../settingsStore.js";
|
|
|
|
const SETTINGS_CATEGORY = "Majoor Assets Manager";
|
|
const SETTINGS_NAME_PREFIX_RE = /^\s*Majoor:\s*/i;
|
|
|
|
const SETTINGS_NATIVE_SECTIONS = Object.freeze({
|
|
ASSETS_PANEL: "Assets Panel",
|
|
GENERATED_FEED: "Generated Feed",
|
|
VIEWER: "Viewer & Floating Viewer",
|
|
INDEXING: "Indexing & Watcher",
|
|
SEARCH_AI: "Search & AI",
|
|
GENERAL: "General",
|
|
ADVANCED: "Advanced",
|
|
SECURITY: "Security",
|
|
});
|
|
|
|
const COLOR_SETTING_KEYS = new Set([
|
|
"grid.starColor",
|
|
"grid.badgeImageColor",
|
|
"grid.badgeVideoColor",
|
|
"grid.badgeAudioColor",
|
|
"grid.badgeModel3dColor",
|
|
"grid.badgeDuplicateAlertColor",
|
|
"ui.cardHoverColor",
|
|
"ui.cardSelectionColor",
|
|
"ui.ratingColor",
|
|
"ui.tagColor",
|
|
]);
|
|
|
|
const GLOBAL_RESET_SETTING_ID = "Majoor.General.ResetAllSettings";
|
|
const RESET_BUTTON_CLASS = "mjr-settings-reset-btn";
|
|
let _settingsResetObserver: MutationObserver | null = null;
|
|
let _settingsResetBindings: any = null;
|
|
|
|
function settingSectionForId(id: any): string {
|
|
const safeId = String(id || "").trim();
|
|
if (!safeId) return SETTINGS_NATIVE_SECTIONS.GENERAL;
|
|
if (safeId === GLOBAL_RESET_SETTING_ID || safeId === "Majoor.Language") {
|
|
return SETTINGS_NATIVE_SECTIONS.GENERAL;
|
|
}
|
|
if (/^Majoor\.(Safety|Security)\./.test(safeId)) {
|
|
return SETTINGS_NATIVE_SECTIONS.SECURITY;
|
|
}
|
|
if (
|
|
/^Majoor\.(Paths|Db|ProbeBackend|MetadataFallback|Observability)\./.test(safeId) ||
|
|
safeId === "Majoor.EnvVars.Reference"
|
|
) {
|
|
return SETTINGS_NATIVE_SECTIONS.ADVANCED;
|
|
}
|
|
if (/^Majoor\.(Viewer|WorkflowMinimap)\./.test(safeId)) {
|
|
return SETTINGS_NATIVE_SECTIONS.VIEWER;
|
|
}
|
|
if (/^Majoor\.Feed\./.test(safeId)) {
|
|
return SETTINGS_NATIVE_SECTIONS.GENERATED_FEED;
|
|
}
|
|
if (/^Majoor\.(AutoScan|Scan|Watcher|ExecutionGrouping|RatingTagsSync)\./.test(safeId)) {
|
|
return SETTINGS_NATIVE_SECTIONS.INDEXING;
|
|
}
|
|
if (safeId === "Majoor.RtHydrate.Concurrency") {
|
|
return SETTINGS_NATIVE_SECTIONS.INDEXING;
|
|
}
|
|
if (
|
|
/^Majoor\.AI\.(HuggingFaceTokenVisible|HuggingFaceToken|VerboseLogs|VectorStats|VectorBackfillAction|VectorSearchEnabled|VectorCaptionOnIndex|VectorIndexOnScan|VectorConcurrency|VectorUnloadAfterUse|VectorUnloadNow)$/.test(safeId) ||
|
|
/^Majoor\.Search\./.test(safeId)
|
|
) {
|
|
return SETTINGS_NATIVE_SECTIONS.SEARCH_AI;
|
|
}
|
|
if (
|
|
/^Majoor\.(Grid|Cards|Badges|Sidebar|InfiniteScroll|General)\./.test(safeId)
|
|
) {
|
|
return SETTINGS_NATIVE_SECTIONS.ASSETS_PANEL;
|
|
}
|
|
if (safeId === "Majoor.Language") {
|
|
return SETTINGS_NATIVE_SECTIONS.GENERAL;
|
|
}
|
|
return SETTINGS_NATIVE_SECTIONS.GENERAL;
|
|
}
|
|
|
|
function normalizeComfyCategory(payload: any): string[] {
|
|
const raw = Array.isArray(payload?.category) ? payload.category.filter(Boolean) : [];
|
|
const section = settingSectionForId(payload?.id);
|
|
const priorSection = String(raw[1] || raw[0] || "").trim();
|
|
const priorLabel = String(raw[2] || "").trim();
|
|
const name = String(payload?.name || "").replace(SETTINGS_NAME_PREFIX_RE, "").trim();
|
|
const label = priorLabel || priorSection || name || section;
|
|
return [SETTINGS_CATEGORY, section, label];
|
|
}
|
|
|
|
let _settingsStorageListenerBound = false;
|
|
let _settingsStorageListenerRef: any = null;
|
|
let _settingsContext: any = null;
|
|
let _settingsRuntimeInitialized = false;
|
|
const _syncingComfySettingIds = new Set();
|
|
|
|
function normalizeSettingPayload(payload: any) {
|
|
if (!payload || typeof payload !== "object") return null;
|
|
const normalized = { ...payload };
|
|
try {
|
|
if (typeof normalized.name === "string") {
|
|
normalized.name = normalized.name.replace(SETTINGS_NAME_PREFIX_RE, "").trim();
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
try {
|
|
normalized.category = normalizeComfyCategory(normalized);
|
|
} catch {
|
|
normalized.category = [SETTINGS_CATEGORY, SETTINGS_NATIVE_SECTIONS.GENERAL];
|
|
}
|
|
if (!normalized.tooltip && typeof normalized.name === "string" && normalized.name.trim()) {
|
|
normalized.tooltip = normalized.name.trim();
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function syncComfySettingValue(app: any, definition: any, value: any) {
|
|
const id = String(definition?.id || "").trim();
|
|
if (!id) return false;
|
|
if (_syncingComfySettingIds.has(id)) return false;
|
|
_syncingComfySettingIds.add(id);
|
|
try {
|
|
return setSettingForApp(app, id, value);
|
|
} finally {
|
|
_syncingComfySettingIds.delete(id);
|
|
}
|
|
}
|
|
|
|
function wrapSettingForComfySync(app: any, definition: any) {
|
|
if (!definition || typeof definition !== "object") return definition;
|
|
const wrapped = { ...definition };
|
|
syncComfySettingValue(app, wrapped, wrapped.defaultValue);
|
|
const originalOnChange = wrapped.onChange;
|
|
wrapped.onChange = (value: any, ...args: any[]) => {
|
|
syncComfySettingValue(app, wrapped, value);
|
|
if (typeof originalOnChange === "function") {
|
|
return originalOnChange(value, ...args);
|
|
}
|
|
wrapped.defaultValue = value;
|
|
return undefined;
|
|
};
|
|
return wrapped;
|
|
}
|
|
|
|
function cloneSettings(settings: any): Record<string, any> {
|
|
try {
|
|
return JSON.parse(JSON.stringify(settings || {}));
|
|
} catch {
|
|
return { ...DEFAULT_SETTINGS };
|
|
}
|
|
}
|
|
|
|
function buildSettingsDefinitionsFromSnapshot(
|
|
settingsSnapshot: Record<string, any>,
|
|
app: any,
|
|
notifyApplied: (...args: any[]) => void,
|
|
{ wrapForComfy = true } = {},
|
|
) {
|
|
const definitions: any[] = [];
|
|
const addSetting = (payload: any) => {
|
|
const normalized = normalizeSettingPayload(payload);
|
|
if (normalized) {
|
|
definitions.push(wrapForComfy ? wrapSettingForComfySync(app, normalized) : normalized);
|
|
}
|
|
};
|
|
registerGridSettings(addSetting, settingsSnapshot, notifyApplied);
|
|
registerFeedSettings(addSetting, settingsSnapshot, notifyApplied);
|
|
registerViewerSettings(addSetting, settingsSnapshot, notifyApplied);
|
|
registerScanningSettings(addSetting, settingsSnapshot, notifyApplied);
|
|
registerSecuritySettings(addSetting, settingsSnapshot, notifyApplied);
|
|
registerAdvancedSettings(addSetting, settingsSnapshot, notifyApplied, app);
|
|
registerSearchSettings(addSetting, settingsSnapshot, notifyApplied);
|
|
return definitions;
|
|
}
|
|
|
|
function sameSettingValue(a: any, b: any): boolean {
|
|
if (a === b) return true;
|
|
try {
|
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function findSettingInputContainer(row: Element | null): HTMLElement | null {
|
|
if (!row) return null;
|
|
return (
|
|
row.querySelector<HTMLElement>(".form-input") ||
|
|
row.querySelector<HTMLElement>(".p-inputgroup") ||
|
|
row.querySelector<HTMLElement>(".setting-input") ||
|
|
row.querySelector<HTMLElement>("[class*='input']")
|
|
);
|
|
}
|
|
|
|
function createResetButton(label: string, title: string): HTMLButtonElement {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = RESET_BUTTON_CLASS;
|
|
button.textContent = label;
|
|
button.title = title;
|
|
button.style.marginLeft = "8px";
|
|
button.style.minWidth = label.length > 2 ? "auto" : "24px";
|
|
button.style.height = "24px";
|
|
button.style.padding = label.length > 2 ? "0 10px" : "0";
|
|
button.style.borderRadius = "6px";
|
|
button.style.border = "1px solid var(--border-color, #555)";
|
|
button.style.background = "var(--comfy-input-bg, #2b2b2b)";
|
|
button.style.color = "var(--input-text, inherit)";
|
|
button.style.cursor = "pointer";
|
|
button.style.fontSize = "12px";
|
|
button.style.lineHeight = "22px";
|
|
button.style.flexShrink = "0";
|
|
return button;
|
|
}
|
|
|
|
function applySettingDefault(definition: any, defaultValue: any, app: any): void {
|
|
const id = String(definition?.id || "").trim();
|
|
if (!id) return;
|
|
syncComfySettingValue(app, definition, defaultValue);
|
|
if (typeof definition?.onChange === "function") {
|
|
definition.onChange(defaultValue);
|
|
}
|
|
}
|
|
|
|
function updateResetButtonState(button: HTMLButtonElement, definition: any, defaultValue: any, app: any): void {
|
|
const currentValue = getSettingForApp(app, definition.id, definition.defaultValue);
|
|
const changed = !sameSettingValue(currentValue, defaultValue);
|
|
button.disabled = !changed;
|
|
button.style.opacity = changed ? "1" : "0.45";
|
|
}
|
|
|
|
function injectSettingsResetButtons() {
|
|
if (typeof document === "undefined" || !_settingsResetBindings) return;
|
|
const { app, definitions, defaultValues } = _settingsResetBindings;
|
|
|
|
const globalRow = document.querySelector(`[data-setting-id="${GLOBAL_RESET_SETTING_ID}"]`);
|
|
const globalContainer = findSettingInputContainer(globalRow);
|
|
if (globalRow && globalContainer && !globalRow.getAttribute("data-mjr-reset-injected")) {
|
|
globalRow.setAttribute("data-mjr-reset-injected", "true");
|
|
globalContainer.innerHTML = "";
|
|
const resetAllButton = createResetButton("Reset all settings", "Reset all Majoor settings to defaults");
|
|
resetAllButton.onclick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
for (const definition of definitions) {
|
|
if (definition.id === GLOBAL_RESET_SETTING_ID) continue;
|
|
if (!defaultValues.has(definition.id)) continue;
|
|
applySettingDefault(definition, defaultValues.get(definition.id), app);
|
|
}
|
|
injectSettingsResetButtons();
|
|
};
|
|
globalContainer.appendChild(resetAllButton);
|
|
}
|
|
|
|
for (const definition of definitions) {
|
|
if (!definition?.id || definition.id === GLOBAL_RESET_SETTING_ID) continue;
|
|
if (!defaultValues.has(definition.id)) continue;
|
|
const row = document.querySelector(`[data-setting-id="${definition.id}"]`);
|
|
if (!row || row.getAttribute("data-mjr-reset-injected")) continue;
|
|
const inputContainer = findSettingInputContainer(row);
|
|
if (!inputContainer) continue;
|
|
row.setAttribute("data-mjr-reset-injected", "true");
|
|
const resetButton = createResetButton("Reset", "Reset this setting to default");
|
|
updateResetButtonState(resetButton, definition, defaultValues.get(definition.id), app);
|
|
resetButton.onclick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const defaultValue = defaultValues.get(definition.id);
|
|
applySettingDefault(definition, defaultValue, app);
|
|
updateResetButtonState(resetButton, definition, defaultValue, app);
|
|
};
|
|
inputContainer.appendChild(resetButton);
|
|
}
|
|
}
|
|
|
|
function ensureSettingsResetObserver(app: any, definitions: any[], defaultDefinitions: any[]) {
|
|
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
|
|
const defaultValues = new Map(
|
|
defaultDefinitions
|
|
.filter((definition) => definition?.id && definition.id !== GLOBAL_RESET_SETTING_ID)
|
|
.map((definition) => [definition.id, definition.defaultValue]),
|
|
);
|
|
_settingsResetBindings = { app, definitions, defaultValues };
|
|
injectSettingsResetButtons();
|
|
if (_settingsResetObserver) return;
|
|
_settingsResetObserver = new MutationObserver(() => injectSettingsResetButtons());
|
|
_settingsResetObserver.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
|
|
function ensureMajoorSettingsContext(app: any, onApplied: any, { initRuntime = false } = {}) {
|
|
if (_settingsContext) {
|
|
if (typeof onApplied === "function") {
|
|
_settingsContext.onAppliedListeners.add(onApplied);
|
|
}
|
|
if (app && !_settingsContext.app) {
|
|
_settingsContext.app = app;
|
|
}
|
|
} else {
|
|
const settings = loadMajoorSettings();
|
|
settings.i18n = settings.i18n || {};
|
|
if (typeof settings.i18n.followComfyLanguage !== "boolean") {
|
|
settings.i18n.followComfyLanguage = true;
|
|
setFollowComfyLanguage(true);
|
|
saveMajoorSettings(settings);
|
|
} else {
|
|
setFollowComfyLanguage(!!settings.i18n.followComfyLanguage);
|
|
}
|
|
|
|
const onAppliedListeners = new Set<(...args: any[]) => void>();
|
|
if (typeof onApplied === "function") {
|
|
onAppliedListeners.add(onApplied);
|
|
}
|
|
|
|
const pendingKeys = new Set();
|
|
const pendingColorKeys = new Set();
|
|
const COLOR_NOTIFY_DEBOUNCE_MS = 450;
|
|
|
|
const flushNotify = () => {
|
|
if (!pendingKeys.size) return;
|
|
const keys = Array.from(pendingKeys);
|
|
pendingKeys.clear();
|
|
for (const pendingKey of keys) {
|
|
safeDispatchCustomEvent(
|
|
"mjr-settings-changed",
|
|
{ key: pendingKey },
|
|
{ warnPrefix: "[Majoor]" },
|
|
);
|
|
}
|
|
};
|
|
const flushColorNotify = () => {
|
|
if (!pendingColorKeys.size) return;
|
|
const keys = Array.from(pendingColorKeys);
|
|
pendingColorKeys.clear();
|
|
for (const pendingKey of keys) {
|
|
safeDispatchCustomEvent(
|
|
"mjr-settings-changed",
|
|
{ key: pendingKey },
|
|
{ warnPrefix: "[Majoor]" },
|
|
);
|
|
}
|
|
};
|
|
|
|
const scheduleNotifyFlush = debounce(flushNotify, 120);
|
|
const scheduleColorNotifyFlush = debounce(flushColorNotify, COLOR_NOTIFY_DEBOUNCE_MS);
|
|
|
|
const scheduleNotify = (key: any) => {
|
|
if (typeof key === "string") pendingKeys.add(key);
|
|
scheduleNotifyFlush();
|
|
};
|
|
const scheduleColorNotify = (key: any) => {
|
|
if (typeof key === "string") pendingColorKeys.add(key);
|
|
scheduleColorNotifyFlush();
|
|
};
|
|
|
|
const refreshFromStorage = () => {
|
|
const latest = loadMajoorSettings();
|
|
Object.assign(settings, latest);
|
|
applySettingsToConfig(settings);
|
|
scheduleNotify("storage");
|
|
};
|
|
|
|
const storageListener = (event: any) => {
|
|
if (!event || event.key !== SETTINGS_KEY) return;
|
|
if (event.newValue === event.oldValue) return;
|
|
refreshFromStorage();
|
|
};
|
|
|
|
if (!_settingsStorageListenerBound) {
|
|
// Remove stale listener from a previous init (hot-reload safety)
|
|
if (_settingsStorageListenerRef && typeof window !== "undefined") {
|
|
try {
|
|
window.removeEventListener("storage", _settingsStorageListenerRef);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
try {
|
|
window.addEventListener("storage", storageListener);
|
|
_settingsStorageListenerBound = true;
|
|
_settingsStorageListenerRef = storageListener;
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
const notifyApplied = (key: any) => {
|
|
for (const listener of onAppliedListeners) {
|
|
try {
|
|
listener(settings, key);
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
if (COLOR_SETTING_KEYS.has(String(key || ""))) {
|
|
scheduleColorNotify(key);
|
|
} else {
|
|
scheduleNotify(key);
|
|
}
|
|
};
|
|
|
|
_settingsContext = {
|
|
app,
|
|
notifyApplied,
|
|
onAppliedListeners,
|
|
refreshFromStorage,
|
|
settings,
|
|
};
|
|
}
|
|
if (initRuntime && !_settingsRuntimeInitialized) {
|
|
const runtimeApp = app || _settingsContext.app;
|
|
const settings = _settingsContext.settings;
|
|
initI18n(runtimeApp);
|
|
applySettingsToConfig(settings);
|
|
startComfyLanguageSync(runtimeApp);
|
|
void syncBackendSecuritySettings();
|
|
void syncBackendVectorSearchSettings();
|
|
void syncBackendExecutionGroupingSettings();
|
|
if (settings?.watcher && typeof settings.watcher.enabled === "boolean") {
|
|
void toggleWatcher(!!settings.watcher.enabled).catch(() => {});
|
|
}
|
|
startRuntimeStatusDashboard();
|
|
_settingsRuntimeInitialized = true;
|
|
}
|
|
return _settingsContext;
|
|
}
|
|
|
|
export const registerMajoorSettings = (app: any | null | undefined, onApplied?: () => void): void => {
|
|
const context = ensureMajoorSettingsContext(app, onApplied, { initRuntime: true });
|
|
return context.settings;
|
|
};
|
|
|
|
export const buildMajoorSettings = (app: any | null | undefined, onApplied?: () => void): any[] => {
|
|
const context = ensureMajoorSettingsContext(app, onApplied, { initRuntime: false });
|
|
Object.assign(context.settings, loadMajoorSettings());
|
|
|
|
const runtimeApp = app || context.app;
|
|
const settingsDefinitions = buildSettingsDefinitionsFromSnapshot(
|
|
context.settings,
|
|
runtimeApp,
|
|
context.notifyApplied,
|
|
);
|
|
const defaultDefinitions = buildSettingsDefinitionsFromSnapshot(
|
|
cloneSettings(DEFAULT_SETTINGS),
|
|
runtimeApp,
|
|
() => {},
|
|
{ wrapForComfy: false },
|
|
);
|
|
settingsDefinitions.unshift(
|
|
wrapSettingForComfySync(runtimeApp, {
|
|
id: GLOBAL_RESET_SETTING_ID,
|
|
category: [SETTINGS_CATEGORY, SETTINGS_NATIVE_SECTIONS.GENERAL, "Reset"],
|
|
name: "Reset all settings to defaults",
|
|
tooltip: "Reset every Majoor Assets Manager setting to its default value.",
|
|
type: "text",
|
|
defaultValue: "",
|
|
}),
|
|
);
|
|
ensureSettingsResetObserver(runtimeApp, settingsDefinitions, defaultDefinitions);
|
|
return settingsDefinitions;
|
|
};
|
|
|
|
// Best-effort backend sync (watcher status) at startup.
|
|
try {
|
|
const settings = loadMajoorSettings();
|
|
if (settings?.watcher && typeof settings.watcher.enabled === "boolean") {
|
|
getWatcherStatus()
|
|
.then((res) => {
|
|
const enabled = !!res?.ok && !!res?.data?.enabled;
|
|
const latest = loadMajoorSettings();
|
|
latest.watcher = latest.watcher || {};
|
|
if (typeof enabled === "boolean" && enabled !== !!latest.watcher.enabled) {
|
|
latest.watcher.enabled = enabled;
|
|
saveMajoorSettings(latest);
|
|
safeDispatchCustomEvent(
|
|
"mjr-settings-changed",
|
|
{ key: "watcher.enabled" },
|
|
{ warnPrefix: "[Majoor]" },
|
|
);
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
} catch (e) {
|
|
console.debug?.(e);
|
|
}
|