Files
MajoorWaldi-ComfyUI-Majoor-…/js/app/settings.js
T
Your Name 0debbd53a2 feat: Enhance context and custom roots management
- Added resetBrowserHistory functionality to contextController for better state management.
- Improved handling of custom root states in customRootsController, including resetting labels and paths.
- Removed unused refreshCustomRoots from scopeController.
- Updated sidebarController to provide more context in asset status updates.
- Introduced new folder details section in the sidebar to display folder metadata.
- Created pinned folders popover view for better folder management.
- Implemented custom status dot themes for improved visual feedback in asset status.
- Added folder info endpoint in the backend to retrieve folder statistics and metadata.
2026-02-15 23:44:26 +01:00

1955 lines
90 KiB
JavaScript

/**
* Majoor settings integration with ComfyUI settings panel.
*/
import { APP_CONFIG, APP_DEFAULTS } from "./config.js";
import { getSecuritySettings, setSecuritySettings, setProbeBackendMode, getMetadataFallbackSettings, setMetadataFallbackSettings, getOutputDirectorySetting, setOutputDirectorySetting, getWatcherStatus, toggleWatcher, getWatcherSettings, updateWatcherSettings, getRuntimeStatus } from "../api/client.js";
import { getSettingsApi } from "./comfyApiBridge.js";
import { comfyToast } from "./toast.js";
import { safeDispatchCustomEvent } from "../utils/events.js";
import { t, initI18n, setLang, getCurrentLang, getSupportedLanguages } from "./i18n.js";
import { SETTINGS_KEY } from "./settingsStore.js";
const SETTINGS_PREFIX = "Majoor";
const SETTINGS_CATEGORY = "Majoor Assets Manager";
const SETTINGS_REG_FLAG = "__mjrSettingsRegistered";
const SETTINGS_DEBUG_KEY = "__mjrSettingsDebug";
const SETTINGS_NAME_PREFIX_RE = /^\s*Majoor:\s*/i;
const RUNTIME_DASHBOARD_ID = "mjr-runtime-status-dashboard";
const DEFAULT_SETTINGS = {
debug: {
safeCall: APP_DEFAULTS.DEBUG_SAFE_CALL,
safeListeners: APP_DEFAULTS.DEBUG_SAFE_LISTENERS,
viewer: APP_DEFAULTS.DEBUG_VIEWER,
},
grid: {
pageSize: APP_DEFAULTS.DEFAULT_PAGE_SIZE,
minSize: APP_DEFAULTS.GRID_MIN_SIZE,
minSizePreset: "small",
gap: APP_DEFAULTS.GRID_GAP,
showExtBadge: APP_DEFAULTS.GRID_SHOW_BADGES_EXTENSION,
showRatingBadge: APP_DEFAULTS.GRID_SHOW_BADGES_RATING,
showTagsBadge: APP_DEFAULTS.GRID_SHOW_BADGES_TAGS,
showDetails: APP_DEFAULTS.GRID_SHOW_DETAILS,
showFilename: APP_DEFAULTS.GRID_SHOW_DETAILS_FILENAME,
showDate: APP_DEFAULTS.GRID_SHOW_DETAILS_DATE,
showDimensions: APP_DEFAULTS.GRID_SHOW_DETAILS_DIMENSIONS,
showGenTime: APP_DEFAULTS.GRID_SHOW_DETAILS_GENTIME,
showWorkflowDot: APP_DEFAULTS.GRID_SHOW_WORKFLOW_DOT,
videoAutoplayMode: APP_DEFAULTS.GRID_VIDEO_AUTOPLAY_MODE,
starColor: APP_DEFAULTS.BADGE_STAR_COLOR,
badgeImageColor: APP_DEFAULTS.BADGE_IMAGE_COLOR,
badgeVideoColor: APP_DEFAULTS.BADGE_VIDEO_COLOR,
badgeAudioColor: APP_DEFAULTS.BADGE_AUDIO_COLOR,
badgeModel3dColor: APP_DEFAULTS.BADGE_MODEL3D_COLOR,
badgeDuplicateAlertColor: APP_DEFAULTS.BADGE_DUPLICATE_ALERT_COLOR,
},
infiniteScroll: {
enabled: APP_DEFAULTS.INFINITE_SCROLL_ENABLED,
rootMargin: APP_DEFAULTS.INFINITE_SCROLL_ROOT_MARGIN,
threshold: APP_DEFAULTS.INFINITE_SCROLL_THRESHOLD,
bottomGapPx: APP_DEFAULTS.BOTTOM_GAP_PX,
},
siblings: {
hidePngSiblings: true,
},
autoScan: {
onStartup: APP_DEFAULTS.AUTO_SCAN_ON_STARTUP,
},
watcher: {
enabled: true,
debounceMs: APP_DEFAULTS.WATCHER_DEBOUNCE_MS,
dedupeTtlMs: APP_DEFAULTS.WATCHER_DEDUPE_TTL_MS,
maxPending: 500,
minSize: 100,
maxSize: 4294967296,
},
safety: {
confirmDeletion: true,
},
status: {
pollInterval: APP_DEFAULTS.STATUS_POLL_INTERVAL,
},
viewer: {
allowPanAtZoom1: APP_DEFAULTS.VIEWER_ALLOW_PAN_AT_ZOOM_1,
disableWebGL: APP_DEFAULTS.VIEWER_DISABLE_WEBGL_VIDEO,
videoGradeThrottleFps: APP_DEFAULTS.VIEWER_VIDEO_GRADE_THROTTLE_FPS,
scopesFps: APP_DEFAULTS.VIEWER_SCOPES_FPS,
metaTtlMs: APP_DEFAULTS.VIEWER_META_TTL_MS,
metaMaxEntries: APP_DEFAULTS.VIEWER_META_MAX_ENTRIES,
},
rtHydrate: {
concurrency: APP_DEFAULTS.RT_HYDRATE_CONCURRENCY,
queueMax: APP_DEFAULTS.RT_HYDRATE_QUEUE_MAX,
seenMax: APP_DEFAULTS.RT_HYDRATE_SEEN_MAX,
pruneBudget: APP_DEFAULTS.RT_HYDRATE_PRUNE_BUDGET,
seenTtlMs: APP_DEFAULTS.RT_HYDRATE_SEEN_TTL_MS,
},
observability: {
enabled: false,
verboseErrors: false,
},
sidebar: {
position: "right",
showPreviewThumb: true,
},
probeBackend: {
mode: "auto",
},
metadataFallback: {
image: true,
media: true,
},
paths: {
outputDirectory: "",
},
db: {
timeoutMs: 5000,
maxConnections: 10,
queryTimeoutMs: 1000,
},
ratingTagsSync: {
enabled: true,
},
cache: {
tagsTTLms: 30_000,
},
search: {
maxResults: APP_DEFAULTS.SEARCH_DEFAULT_LIMIT,
},
workflowMinimap: {
enabled: false,
nodeColors: true,
showLinks: true,
showGroups: true,
renderBypassState: true,
renderErrorState: true,
showViewport: true,
},
ui: {
cardHoverColor: APP_DEFAULTS.UI_CARD_HOVER_COLOR,
cardSelectionColor: APP_DEFAULTS.UI_CARD_SELECTION_COLOR,
ratingColor: APP_DEFAULTS.UI_RATING_COLOR,
tagColor: APP_DEFAULTS.UI_TAG_COLOR,
},
security: {
safeMode: false,
allowWrite: true,
allowRemoteWrite: true,
allowDelete: true,
allowRename: true,
allowOpenInFolder: true,
allowResetIndex: true,
apiToken: "",
},
};
const _safeBool = (value, fallback) => {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) return true;
if (["0", "false", "no", "off"].includes(normalized)) return false;
}
return !!fallback;
};
const _safeNum = (value, fallback) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : Number(fallback);
};
const _safeOneOf = (value, allowed, fallback) => {
const candidate = typeof value === "string" ? value.trim() : String(value ?? "");
return allowed.includes(candidate) ? candidate : fallback;
};
const GRID_SIZE_PRESETS = Object.freeze({
small: 140,
medium: 200,
large: 280,
});
const GRID_SIZE_PRESET_OPTIONS = Object.freeze(["small", "medium", "large"]);
const resolveGridMinSize = (grid = {}) => {
const preset = _safeOneOf(String(grid?.minSizePreset || "").toLowerCase(), GRID_SIZE_PRESET_OPTIONS, "");
if (preset) return GRID_SIZE_PRESETS[preset];
return Math.max(60, Math.min(600, Math.round(_safeNum(grid?.minSize, APP_DEFAULTS.GRID_MIN_SIZE))));
};
const detectGridSizePreset = (minSize) => {
const val = Math.round(_safeNum(minSize, APP_DEFAULTS.GRID_MIN_SIZE));
if (val <= 160) return "small";
if (val >= 250) return "large";
return "medium";
};
const _isUnsafeMergeKey = (key) =>
key === "__proto__" || key === "prototype" || key === "constructor";
const deepMerge = (base, next) => {
const output = { ...base };
if (!next || typeof next !== "object") {
return output;
}
Object.keys(next).forEach((key) => {
if (_isUnsafeMergeKey(key)) return;
const value = next[key];
if (value && typeof value === "object" && !Array.isArray(value)) {
output[key] = deepMerge(base[key] || {}, value);
} else if (value !== undefined) {
output[key] = value;
}
});
return output;
};
function ensureRuntimeStatusDashboard() {
try {
const host = document.querySelector(".mjr-assets-manager.mjr-am-container");
const existing = document.getElementById(RUNTIME_DASHBOARD_ID);
if (!host) {
try {
existing?.remove?.();
} catch {}
return null;
}
// Anchor to the Assets Manager container (not the scrollable grid),
// so the widget stays fixed while the grid scrolls and disappears when panel closes.
try {
const hostPos = String(getComputedStyle(host).position || "").toLowerCase();
if (!hostPos || hostPos === "static") {
host.style.position = "relative";
}
} catch {}
let el = document.getElementById(RUNTIME_DASHBOARD_ID);
if (!el) {
el = document.createElement("div");
el.id = RUNTIME_DASHBOARD_ID;
el.style.position = "absolute";
el.style.bottom = "10px";
el.style.right = "10px";
el.style.zIndex = "9999";
el.style.padding = "6px 10px";
el.style.borderRadius = "10px";
el.style.border = "1px solid rgba(255,255,255,0.16)";
el.style.background = "rgba(0,0,0,0.45)";
el.style.backdropFilter = "blur(4px)";
el.style.color = "var(--content-fg, #fff)";
el.style.fontSize = "11px";
el.style.pointerEvents = "none";
host.appendChild(el);
} else if (el.parentElement !== host) {
host.appendChild(el);
}
return el;
} catch {
return null;
}
}
async function refreshRuntimeStatusDashboard() {
const el = ensureRuntimeStatusDashboard();
if (!el) return false;
try {
const res = await getRuntimeStatus();
if (!res?.ok || !res?.data) {
el.textContent = "Runtime: unavailable";
el.title = "Runtime metrics unavailable";
return true;
}
const db = res.data.db || {};
const idx = res.data.index || {};
const w = res.data.watcher || {};
const active = Number(db.active_connections || 0);
const enrichQ = Number(idx.enrichment_queue_length || 0);
const pending = Number(w.pending_files || 0);
el.textContent = `DB active: ${active} | Enrich Q: ${enrichQ} | Watcher pending: ${pending}`;
el.title = `Runtime Metrics\nDB active connections: ${active}\nEnrichment queue: ${enrichQ}\nWatcher pending files: ${pending}`;
return true;
} catch {
el.textContent = "Runtime: unavailable";
el.title = "Runtime metrics unavailable";
return true;
}
}
export function startRuntimeStatusDashboard() {
try {
refreshRuntimeStatusDashboard().catch(() => {});
if (window.__MJR_RUNTIME_STATUS_INFLIGHT__ == null) {
window.__MJR_RUNTIME_STATUS_INFLIGHT__ = false;
}
if (window.__MJR_RUNTIME_STATUS_MISS_COUNT__ == null) {
window.__MJR_RUNTIME_STATUS_MISS_COUNT__ = 0;
}
if (!window.__MJR_RUNTIME_STATUS_INTERVAL__) {
window.__MJR_RUNTIME_STATUS_INTERVAL__ = setInterval(() => {
if (window.__MJR_RUNTIME_STATUS_INFLIGHT__) return;
window.__MJR_RUNTIME_STATUS_INFLIGHT__ = true;
refreshRuntimeStatusDashboard()
.then((visible) => {
if (visible) {
window.__MJR_RUNTIME_STATUS_MISS_COUNT__ = 0;
return;
}
window.__MJR_RUNTIME_STATUS_MISS_COUNT__ = Number(window.__MJR_RUNTIME_STATUS_MISS_COUNT__ || 0) + 1;
// Stop background polling if panel is absent for ~30s.
if (window.__MJR_RUNTIME_STATUS_MISS_COUNT__ >= 10 && window.__MJR_RUNTIME_STATUS_INTERVAL__) {
clearInterval(window.__MJR_RUNTIME_STATUS_INTERVAL__);
window.__MJR_RUNTIME_STATUS_INTERVAL__ = null;
}
})
.catch(() => {})
.finally(() => {
window.__MJR_RUNTIME_STATUS_INFLIGHT__ = false;
});
}, 3000);
}
} catch {}
}
export const loadMajoorSettings = () => {
if (typeof localStorage === "undefined") return { ...DEFAULT_SETTINGS };
try {
const raw = localStorage.getItem(SETTINGS_KEY);
if (!raw) return { ...DEFAULT_SETTINGS };
const parsed = JSON.parse(raw);
const allowed = new Set([
"debug",
"grid",
"infiniteScroll",
"siblings",
"autoScan",
"watcher",
"status",
"viewer",
"rtHydrate",
"observability",
"sidebar",
"probeBackend",
"paths",
"db",
"ratingTagsSync",
"cache",
"search",
"workflowMinimap",
"ui",
"security",
"safety",
]);
const sanitized = {};
if (parsed && typeof parsed === "object") {
for (const [key, value] of Object.entries(parsed)) {
if (allowed.has(key)) sanitized[key] = value;
}
}
return deepMerge(DEFAULT_SETTINGS, sanitized);
} catch (error) {
console.warn("[Majoor] settings load failed, using defaults", error);
return { ...DEFAULT_SETTINGS };
}
};
export const saveMajoorSettings = (settings) => {
if (typeof localStorage === "undefined") return;
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
} catch (error) {
console.warn("[Majoor] settings save failed", error);
try {
const now = Date.now();
const last = Number(window?._mjrSettingsSaveFailAt || 0) || 0;
if (now - last > 30_000) {
window._mjrSettingsSaveFailAt = now;
import("./dialogs.js")
.then(({ comfyAlert }) => comfyAlert("Majoor: Failed to save settings (browser storage full or blocked)."))
.catch(() => {});
}
} catch {}
try {
safeDispatchCustomEvent("mjr-settings-save-failed", { error: String(error?.message || error || "") }, { warnPrefix: "[Majoor]" });
} catch {}
}
};
const applySettingsToConfig = (settings) => {
const maxPage = Number(APP_DEFAULTS.MAX_PAGE_SIZE) || 2000;
const pageSize = Math.max(
50,
Math.min(maxPage, Number(settings.grid?.pageSize) || APP_DEFAULTS.DEFAULT_PAGE_SIZE)
);
APP_CONFIG.DEFAULT_PAGE_SIZE = pageSize;
APP_CONFIG.AUTO_SCAN_ON_STARTUP = !!settings.autoScan?.onStartup;
APP_CONFIG.STATUS_POLL_INTERVAL = Math.max(1000, Number(settings.status?.pollInterval) || APP_DEFAULTS.STATUS_POLL_INTERVAL);
APP_CONFIG.DEBUG_SAFE_CALL = !!settings.debug?.safeCall;
APP_CONFIG.DEBUG_SAFE_LISTENERS = !!settings.debug?.safeListeners;
APP_CONFIG.DEBUG_VIEWER = !!settings.debug?.viewer;
APP_CONFIG.GRID_MIN_SIZE = resolveGridMinSize(settings.grid);
APP_CONFIG.GRID_GAP = Math.max(0, Math.min(40, Math.round(_safeNum(settings.grid?.gap, APP_DEFAULTS.GRID_GAP))));
APP_CONFIG.GRID_SHOW_BADGES_EXTENSION = !!(settings.grid?.showExtBadge ?? APP_DEFAULTS.GRID_SHOW_BADGES_EXTENSION);
APP_CONFIG.GRID_SHOW_BADGES_RATING = !!(settings.grid?.showRatingBadge ?? APP_DEFAULTS.GRID_SHOW_BADGES_RATING);
APP_CONFIG.GRID_SHOW_BADGES_TAGS = !!(settings.grid?.showTagsBadge ?? APP_DEFAULTS.GRID_SHOW_BADGES_TAGS);
APP_CONFIG.GRID_SHOW_DETAILS = !!(settings.grid?.showDetails ?? APP_DEFAULTS.GRID_SHOW_DETAILS);
APP_CONFIG.GRID_SHOW_DETAILS_FILENAME = !!(settings.grid?.showFilename ?? APP_DEFAULTS.GRID_SHOW_DETAILS_FILENAME);
APP_CONFIG.GRID_SHOW_DETAILS_DATE = !!(settings.grid?.showDate ?? APP_DEFAULTS.GRID_SHOW_DETAILS_DATE);
APP_CONFIG.GRID_SHOW_DETAILS_DIMENSIONS = !!(settings.grid?.showDimensions ?? APP_DEFAULTS.GRID_SHOW_DETAILS_DIMENSIONS);
APP_CONFIG.GRID_SHOW_DETAILS_GENTIME = !!(settings.grid?.showGenTime ?? APP_DEFAULTS.GRID_SHOW_DETAILS_GENTIME);
APP_CONFIG.GRID_SHOW_WORKFLOW_DOT = !!(settings.grid?.showWorkflowDot ?? APP_DEFAULTS.GRID_SHOW_WORKFLOW_DOT);
// Video autoplay mode: migrate old boolean → new tri-state
{
let mode = settings.grid?.videoAutoplayMode ?? APP_DEFAULTS.GRID_VIDEO_AUTOPLAY_MODE;
// Backward compat: old boolean "videoHoverAutoplay" → "hover"
if (mode === undefined || mode === null) {
mode = settings.grid?.videoHoverAutoplay === false ? "off" : "hover";
}
if (mode === true) mode = "hover";
if (mode === false) mode = "off";
if (mode !== "hover" && mode !== "always" && mode !== "off") mode = "hover";
APP_CONFIG.GRID_VIDEO_AUTOPLAY_MODE = mode;
}
const _safeColor = (v, fallback) => {
let c = String(v || "").trim();
if (/^[0-9a-fA-F]{6}$/.test(c)) c = `#${c}`;
return /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : fallback;
};
APP_CONFIG.BADGE_STAR_COLOR = _safeColor(settings.grid?.starColor, APP_DEFAULTS.BADGE_STAR_COLOR);
APP_CONFIG.BADGE_IMAGE_COLOR = _safeColor(settings.grid?.badgeImageColor, APP_DEFAULTS.BADGE_IMAGE_COLOR);
APP_CONFIG.BADGE_VIDEO_COLOR = _safeColor(settings.grid?.badgeVideoColor, APP_DEFAULTS.BADGE_VIDEO_COLOR);
APP_CONFIG.BADGE_AUDIO_COLOR = _safeColor(settings.grid?.badgeAudioColor, APP_DEFAULTS.BADGE_AUDIO_COLOR);
APP_CONFIG.BADGE_MODEL3D_COLOR = _safeColor(settings.grid?.badgeModel3dColor, APP_DEFAULTS.BADGE_MODEL3D_COLOR);
APP_CONFIG.BADGE_DUPLICATE_ALERT_COLOR = _safeColor(settings.grid?.badgeDuplicateAlertColor, APP_DEFAULTS.BADGE_DUPLICATE_ALERT_COLOR);
APP_CONFIG.UI_CARD_HOVER_COLOR = _safeColor(settings.ui?.cardHoverColor, APP_DEFAULTS.UI_CARD_HOVER_COLOR);
APP_CONFIG.UI_CARD_SELECTION_COLOR = _safeColor(settings.ui?.cardSelectionColor, APP_DEFAULTS.UI_CARD_SELECTION_COLOR);
APP_CONFIG.UI_RATING_COLOR = _safeColor(settings.ui?.ratingColor, APP_DEFAULTS.UI_RATING_COLOR);
APP_CONFIG.UI_TAG_COLOR = _safeColor(settings.ui?.tagColor, APP_DEFAULTS.UI_TAG_COLOR);
try {
const roots = Array.from(document.querySelectorAll(".mjr-assets-manager"));
for (const root of roots) {
root.style.setProperty("--mjr-star-active", APP_CONFIG.BADGE_STAR_COLOR);
root.style.setProperty("--mjr-badge-image", APP_CONFIG.BADGE_IMAGE_COLOR);
root.style.setProperty("--mjr-badge-video", APP_CONFIG.BADGE_VIDEO_COLOR);
root.style.setProperty("--mjr-badge-audio", APP_CONFIG.BADGE_AUDIO_COLOR);
root.style.setProperty("--mjr-badge-model3d", APP_CONFIG.BADGE_MODEL3D_COLOR);
root.style.setProperty("--mjr-badge-duplicate-alert", APP_CONFIG.BADGE_DUPLICATE_ALERT_COLOR);
root.style.setProperty("--mjr-card-hover-color", APP_CONFIG.UI_CARD_HOVER_COLOR);
root.style.setProperty("--mjr-card-selection-color", APP_CONFIG.UI_CARD_SELECTION_COLOR);
root.style.setProperty("--mjr-rating-color", APP_CONFIG.UI_RATING_COLOR);
root.style.setProperty("--mjr-tag-color", APP_CONFIG.UI_TAG_COLOR);
}
} catch {}
APP_CONFIG.INFINITE_SCROLL_ENABLED = !!settings.infiniteScroll?.enabled;
APP_CONFIG.INFINITE_SCROLL_ROOT_MARGIN = String(settings.infiniteScroll?.rootMargin || APP_DEFAULTS.INFINITE_SCROLL_ROOT_MARGIN);
APP_CONFIG.INFINITE_SCROLL_THRESHOLD = Math.max(0, Math.min(1, _safeNum(settings.infiniteScroll?.threshold, APP_DEFAULTS.INFINITE_SCROLL_THRESHOLD)));
APP_CONFIG.BOTTOM_GAP_PX = Math.max(0, Math.min(5000, Math.round(_safeNum(settings.infiniteScroll?.bottomGapPx, APP_DEFAULTS.BOTTOM_GAP_PX))));
APP_CONFIG.VIEWER_ALLOW_PAN_AT_ZOOM_1 = !!settings.viewer?.allowPanAtZoom1;
APP_CONFIG.VIEWER_DISABLE_WEBGL_VIDEO = !!settings.viewer?.disableWebGL;
APP_CONFIG.VIEWER_VIDEO_GRADE_THROTTLE_FPS = Math.max(1, Math.min(60, Math.round(_safeNum(settings.viewer?.videoGradeThrottleFps, APP_DEFAULTS.VIEWER_VIDEO_GRADE_THROTTLE_FPS))));
APP_CONFIG.VIEWER_SCOPES_FPS = Math.max(1, Math.min(60, Math.round(_safeNum(settings.viewer?.scopesFps, APP_DEFAULTS.VIEWER_SCOPES_FPS))));
APP_CONFIG.VIEWER_META_TTL_MS = Math.max(1000, Math.min(10 * 60_000, Math.round(_safeNum(settings.viewer?.metaTtlMs, APP_DEFAULTS.VIEWER_META_TTL_MS))));
APP_CONFIG.VIEWER_META_MAX_ENTRIES = Math.max(50, Math.min(5000, Math.round(_safeNum(settings.viewer?.metaMaxEntries, APP_DEFAULTS.VIEWER_META_MAX_ENTRIES))));
APP_CONFIG.WORKFLOW_MINIMAP_ENABLED = !!(settings.workflowMinimap?.enabled ?? false);
APP_CONFIG.RT_HYDRATE_CONCURRENCY = Math.max(1, Math.min(16, Math.round(_safeNum(settings.rtHydrate?.concurrency, APP_DEFAULTS.RT_HYDRATE_CONCURRENCY))));
APP_CONFIG.RT_HYDRATE_QUEUE_MAX = Math.max(10, Math.min(5000, Math.round(_safeNum(settings.rtHydrate?.queueMax, APP_DEFAULTS.RT_HYDRATE_QUEUE_MAX))));
APP_CONFIG.RT_HYDRATE_SEEN_MAX = Math.max(1000, Math.min(200_000, Math.round(_safeNum(settings.rtHydrate?.seenMax, APP_DEFAULTS.RT_HYDRATE_SEEN_MAX))));
APP_CONFIG.RT_HYDRATE_PRUNE_BUDGET = Math.max(10, Math.min(10_000, Math.round(_safeNum(settings.rtHydrate?.pruneBudget, APP_DEFAULTS.RT_HYDRATE_PRUNE_BUDGET))));
APP_CONFIG.RT_HYDRATE_SEEN_TTL_MS = Math.max(5_000, Math.min(6 * 60 * 60_000, Math.round(_safeNum(settings.rtHydrate?.seenTtlMs, APP_DEFAULTS.RT_HYDRATE_SEEN_TTL_MS))));
APP_CONFIG.DELETE_CONFIRMATION = !!settings.safety?.confirmDeletion;
APP_CONFIG.DEBUG_VERBOSE_ERRORS = !!settings.observability?.verboseErrors;
APP_CONFIG.WATCHER_MAX_PENDING = Math.max(10, Math.min(5000, Math.round(_safeNum(settings.watcher?.maxPending, 500))));
APP_CONFIG.WATCHER_MIN_SIZE = Math.max(0, Math.min(1000000, Math.round(_safeNum(settings.watcher?.minSize, 100))));
APP_CONFIG.WATCHER_MAX_SIZE = Math.max(100000, Math.min(17179869184, Math.round(_safeNum(settings.watcher?.maxSize, 4294967296))));
APP_CONFIG.DB_TIMEOUT_MS = Math.max(1000, Math.min(30000, Math.round(_safeNum(settings.db?.timeoutMs, 5000))));
APP_CONFIG.DB_MAX_CONNECTIONS = Math.max(1, Math.min(100, Math.round(_safeNum(settings.db?.maxConnections, 10))));
APP_CONFIG.DB_QUERY_TIMEOUT_MS = Math.max(500, Math.min(10000, Math.round(_safeNum(settings.db?.queryTimeoutMs, 1000))));
// Search request limit (client-side); backend still enforces MAJOOR_SEARCH_MAX_LIMIT
APP_CONFIG.SEARCH_REQUEST_LIMIT = Math.max(
10,
Math.min(APP_DEFAULTS.MAX_PAGE_SIZE || 2000, Math.round(_safeNum(settings.search?.maxResults, APP_DEFAULTS.SEARCH_DEFAULT_LIMIT)))
);
};
export async function syncBackendSecuritySettings() {
try {
const res = await getSecuritySettings();
if (!res?.ok) return;
const prefs = res.data?.prefs;
if (!prefs || typeof prefs !== "object") return;
const settings = loadMajoorSettings();
settings.security = settings.security || {};
settings.security.safeMode = _safeBool(prefs.safe_mode, settings.security.safeMode);
settings.security.allowWrite = _safeBool(prefs.allow_write, settings.security.allowWrite);
settings.security.allowRemoteWrite = _safeBool(prefs.allow_remote_write, settings.security.allowRemoteWrite);
settings.security.allowDelete = _safeBool(prefs.allow_delete, settings.security.allowDelete);
settings.security.allowRename = _safeBool(prefs.allow_rename, settings.security.allowRename);
settings.security.allowOpenInFolder = _safeBool(prefs.allow_open_in_folder, settings.security.allowOpenInFolder);
settings.security.allowResetIndex = _safeBool(prefs.allow_reset_index, settings.security.allowResetIndex);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
safeDispatchCustomEvent("mjr-settings-changed", { key: "security" }, { warnPrefix: "[Majoor]" });
} catch (error) {
console.warn("[Majoor] failed to sync backend security settings", error);
}
}
// Guard to prevent multiple registrations
let _settingsStorageListenerBound = false;
export const registerMajoorSettings = (app, onApplied) => {
initI18n(app);
const settings = loadMajoorSettings();
applySettingsToConfig(settings);
void syncBackendSecuritySettings();
let notifyTimer = null;
let notifyColorTimer = null;
const pendingKeys = new Set();
const pendingColorKeys = new Set();
const COLOR_NOTIFY_DEBOUNCE_MS = 450;
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 flushNotify = () => {
notifyTimer = null;
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 = () => {
notifyColorTimer = null;
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 scheduleNotify = (key) => {
if (typeof key === "string") pendingKeys.add(key);
if (notifyTimer) return;
notifyTimer = setTimeout(flushNotify, 120);
};
const scheduleColorNotify = (key) => {
if (typeof key === "string") pendingColorKeys.add(key);
if (notifyColorTimer) clearTimeout(notifyColorTimer);
notifyColorTimer = setTimeout(flushColorNotify, COLOR_NOTIFY_DEBOUNCE_MS);
};
const refreshFromStorage = () => {
const latest = loadMajoorSettings();
Object.assign(settings, latest);
applySettingsToConfig(settings);
scheduleNotify("storage");
};
const storageListener = (event) => {
if (!event || event.key !== SETTINGS_KEY) return;
if (event.newValue === event.oldValue) return;
refreshFromStorage();
};
// Only add storage listener once to prevent leaks
if (!_settingsStorageListenerBound) {
try {
window.addEventListener("storage", storageListener);
_settingsStorageListenerBound = true;
} catch {}
}
const tryRegister = () => {
const settingsApi = getSettingsApi(app);
const addSetting = settingsApi?.addSetting;
if (typeof addSetting !== "function") {
try {
if (!window?.[SETTINGS_DEBUG_KEY]?.waitingLogged) {
console.info("[Majoor] Waiting for ComfyUI settings API...");
window[SETTINGS_DEBUG_KEY] = { ...(window?.[SETTINGS_DEBUG_KEY] || {}), waitingLogged: true };
}
} catch {}
return false;
}
try {
if (settingsApi[SETTINGS_REG_FLAG]) {
return true;
}
} catch {}
const safeAddSetting = (payload) => {
if (!payload || typeof payload !== "object") return;
// Normalize setting label: remove "Majoor:" prefix from all UI names.
try {
if (typeof payload.name === "string") {
payload.name = payload.name.replace(SETTINGS_NAME_PREFIX_RE, "").trim();
}
} catch {}
// Enforce Majoor namespace for every setting category.
try {
const cat = payload.category;
if (!Array.isArray(cat) || String(cat[0] || "") !== SETTINGS_CATEGORY) {
payload.category = [SETTINGS_CATEGORY];
}
} catch {
payload.category = [SETTINGS_CATEGORY];
}
try {
addSetting(payload);
return;
} catch (err1) {
// Some ComfyUI builds reject optional attrs payloads.
// Retry gracefully so settings remain visible.
try {
const noAttrs = { ...payload };
delete noAttrs.attrs;
addSetting(noAttrs);
return;
} catch (err2) {
try {
const { attrs: _attrs, ...rest } = payload;
if (!Array.isArray(rest.category) || String(rest.category[0] || "") !== SETTINGS_CATEGORY) {
rest.category = [SETTINGS_CATEGORY];
}
addSetting(rest);
return;
} catch (err3) {
try {
console.warn("[Majoor] addSetting failed", {
id: payload?.id,
name: payload?.name,
err1: String(err1?.message || err1 || ""),
err2: String(err2?.message || err2 || ""),
err3: String(err3?.message || err3 || ""),
});
} catch {}
}
}
}
};
const notifyApplied = (key) => {
if (typeof onApplied === "function") {
onApplied(settings, key);
}
if (COLOR_SETTING_KEYS.has(String(key || ""))) {
scheduleColorNotify(key);
} else {
scheduleNotify(key);
}
};
const cat = (section, label) => [SETTINGS_CATEGORY, section, label];
const cardCat = (label) => [SETTINGS_CATEGORY, t("cat.cards", "Cards"), label];
const badgeCat = (label) => [SETTINGS_CATEGORY, t("cat.badges", "Badges"), label];
const colorCat = (label) => [SETTINGS_CATEGORY, t("cat.badges", "Badges"), label];
const normalizeHexColor = (value, fallback) => {
let v = String(value || "").trim();
// PrimeVue ColorPicker returns hex without '#' prefix
if (/^[0-9a-fA-F]{6}$/.test(v)) v = `#${v}`;
return /^#[0-9a-fA-F]{6}$/.test(v) ? v.toUpperCase() : fallback;
};
if (!settings.grid?.minSizePreset) {
settings.grid = settings.grid || {};
settings.grid.minSizePreset = detectGridSizePreset(settings.grid.minSize);
saveMajoorSettings(settings);
}
// ──────────────────────────────────────────────
// Section: Cards
// ──────────────────────────────────────────────
safeAddSetting({
id: `${SETTINGS_PREFIX}.Cards.ThumbSize`,
category: cardCat(t("setting.grid.cardSize.group", "Card size")),
name: t("setting.grid.cardSize.name", "Majoor: Card Size"),
tooltip: t("setting.grid.cardSize.desc", "Choose the card size preset used by the grid layout."),
type: "combo",
defaultValue: (() => {
const preset = _safeOneOf(
String(settings.grid?.minSizePreset || "").toLowerCase(),
GRID_SIZE_PRESET_OPTIONS,
detectGridSizePreset(settings.grid?.minSize)
);
const labels = {
small: t("setting.grid.cardSize.small", "Small"),
medium: t("setting.grid.cardSize.medium", "Medium"),
large: t("setting.grid.cardSize.large", "Large"),
};
return labels[preset] || labels.medium;
})(),
options: [
t("setting.grid.cardSize.small", "Small"),
t("setting.grid.cardSize.medium", "Medium"),
t("setting.grid.cardSize.large", "Large"),
],
onChange: (value) => {
const label = String(value || "").trim().toLowerCase();
const smallLabel = t("setting.grid.cardSize.small", "Small").toLowerCase();
const mediumLabel = t("setting.grid.cardSize.medium", "Medium").toLowerCase();
const largeLabel = t("setting.grid.cardSize.large", "Large").toLowerCase();
let preset = "medium";
if (label === smallLabel || label === "small" || label === "petit") preset = "small";
else if (label === largeLabel || label === "large" || label === "grand") preset = "large";
else if (label === mediumLabel || label === "medium" || label === "moyen") preset = "medium";
settings.grid.minSizePreset = preset;
settings.grid.minSize = GRID_SIZE_PRESETS[preset];
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.minSizePreset");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowDetails`,
category: cardCat("Show card details"),
name: "Show metadata panel",
tooltip: "Show the bottom details panel on asset cards (filename, date, etc.)",
type: "boolean",
defaultValue: !!settings.grid?.showDetails,
onChange: (value) => {
settings.grid.showDetails = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showDetails");
},
});
if (settings.grid?.showDetails !== false) {
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowFilename`,
category: cardCat("Show filename"),
name: "Show filename",
tooltip: "Display filename in details panel",
type: "boolean",
defaultValue: !!settings.grid?.showFilename,
onChange: (value) => {
settings.grid.showFilename = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showFilename");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowDate`,
category: cardCat("Show date/time"),
name: "Show date/time",
tooltip: "Display date and time in details panel",
type: "boolean",
defaultValue: !!settings.grid?.showDate,
onChange: (value) => {
settings.grid.showDate = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showDate");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowDimensions`,
category: cardCat("Show dimensions"),
name: "Show dimensions",
tooltip: "Display resolution (WxH) in details panel",
type: "boolean",
defaultValue: !!settings.grid?.showDimensions,
onChange: (value) => {
settings.grid.showDimensions = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showDimensions");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowGenTime`,
category: cardCat("Show generation time"),
name: "Show generation time",
tooltip: "Display seconds taken to generate the asset (if available)",
type: "boolean",
defaultValue: !!settings.grid?.showGenTime,
onChange: (value) => {
settings.grid.showGenTime = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showGenTime");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowWorkflowDot`,
category: cardCat("Show workflow dot"),
name: "Show workflow indicator",
tooltip: "Display the green dot indicating workflow metadata availability (bottom right of card)",
type: "boolean",
defaultValue: !!settings.grid?.showWorkflowDot,
onChange: (value) => {
settings.grid.showWorkflowDot = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showWorkflowDot");
},
});
}
// ──────────────────────────────────────────────
// Section: Badges
// ──────────────────────────────────────────────
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowExtBadge`,
category: badgeCat("Show format badges"),
name: "Show format badges",
tooltip: "Display format badges (e.g. JPG, MP4) on thumbnails",
type: "boolean",
defaultValue: !!settings.grid?.showExtBadge,
onChange: (value) => {
settings.grid.showExtBadge = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showExtBadge");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowRatingBadge`,
category: badgeCat("Show rating badges"),
name: "Show ratings",
tooltip: "Display star ratings on thumbnails",
type: "boolean",
defaultValue: !!settings.grid?.showRatingBadge,
onChange: (value) => {
settings.grid.showRatingBadge = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showRatingBadge");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.ShowTagsBadge`,
category: badgeCat("Show tags badges"),
name: "Show tags",
tooltip: "Display a small indicator if an asset has tags",
type: "boolean",
defaultValue: !!settings.grid?.showTagsBadge,
onChange: (value) => {
settings.grid.showTagsBadge = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.showTagsBadge");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.StarColor`,
category: colorCat(t("setting.starColor", "Star color")),
name: t("setting.starColor", "Majoor: Star color"),
tooltip: t("setting.starColor.tooltip", "Color of rating stars on thumbnails (hex, e.g. #FFD45A)"),
type: "color",
defaultValue: normalizeHexColor(settings.grid?.starColor, APP_DEFAULTS.BADGE_STAR_COLOR),
onChange: (value) => {
settings.grid.starColor = normalizeHexColor(value, APP_DEFAULTS.BADGE_STAR_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.starColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.ImageColor`,
category: colorCat(t("setting.badgeImageColor", "Image badge color")),
name: t("setting.badgeImageColor", "Majoor: Image badge color"),
tooltip: t("setting.badgeImageColor.tooltip", "Color for image badges: PNG, JPG, WEBP, GIF, BMP, TIF (hex)"),
type: "color",
defaultValue: normalizeHexColor(settings.grid?.badgeImageColor, APP_DEFAULTS.BADGE_IMAGE_COLOR),
onChange: (value) => {
settings.grid.badgeImageColor = normalizeHexColor(value, APP_DEFAULTS.BADGE_IMAGE_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.badgeImageColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.VideoColor`,
category: colorCat(t("setting.badgeVideoColor", "Video badge color")),
name: t("setting.badgeVideoColor", "Majoor: Video badge color"),
tooltip: t("setting.badgeVideoColor.tooltip", "Color for video badges: MP4, WEBM, MOV, AVI, MKV (hex)"),
type: "color",
defaultValue: normalizeHexColor(settings.grid?.badgeVideoColor, APP_DEFAULTS.BADGE_VIDEO_COLOR),
onChange: (value) => {
settings.grid.badgeVideoColor = normalizeHexColor(value, APP_DEFAULTS.BADGE_VIDEO_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.badgeVideoColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.AudioColor`,
category: colorCat(t("setting.badgeAudioColor", "Audio badge color")),
name: t("setting.badgeAudioColor", "Majoor: Audio badge color"),
tooltip: t("setting.badgeAudioColor.tooltip", "Color for audio badges: MP3, WAV, OGG, FLAC (hex)"),
type: "color",
defaultValue: normalizeHexColor(settings.grid?.badgeAudioColor, APP_DEFAULTS.BADGE_AUDIO_COLOR),
onChange: (value) => {
settings.grid.badgeAudioColor = normalizeHexColor(value, APP_DEFAULTS.BADGE_AUDIO_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.badgeAudioColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.Model3dColor`,
category: colorCat(t("setting.badgeModel3dColor", "3D model badge color")),
name: t("setting.badgeModel3dColor", "Majoor: 3D model badge color"),
tooltip: t("setting.badgeModel3dColor.tooltip", "Color for 3D model badges: OBJ, FBX, GLB, GLTF (hex)"),
type: "color",
defaultValue: normalizeHexColor(settings.grid?.badgeModel3dColor, APP_DEFAULTS.BADGE_MODEL3D_COLOR),
onChange: (value) => {
settings.grid.badgeModel3dColor = normalizeHexColor(value, APP_DEFAULTS.BADGE_MODEL3D_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.badgeModel3dColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.DuplicateAlertColor`,
category: colorCat(t("setting.badgeDuplicateAlertColor", "Duplicate alert badge color")),
name: t("setting.badgeDuplicateAlertColor", "Majoor: Duplicate alert badge color"),
tooltip: t("setting.badgeDuplicateAlertColor.tooltip", "Color for duplicate extension badges (PNG+, JPG+, etc)."),
type: "color",
defaultValue: normalizeHexColor(settings.grid?.badgeDuplicateAlertColor, APP_DEFAULTS.BADGE_DUPLICATE_ALERT_COLOR),
onChange: (value) => {
settings.grid.badgeDuplicateAlertColor = normalizeHexColor(value, APP_DEFAULTS.BADGE_DUPLICATE_ALERT_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.badgeDuplicateAlertColor");
},
});
// ──────────────────────────────────────────────
// Section: Grid
// ──────────────────────────────────────────────
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.PageSize`,
category: cat(t("cat.grid"), t("setting.grid.pagesize.name").replace("Majoor: ", "")),
name: t("setting.grid.pagesize.name"),
tooltip: t("setting.grid.pagesize.desc"),
type: "number",
defaultValue: settings.grid.pageSize,
attrs: { min: 50, max: Number(APP_CONFIG.MAX_PAGE_SIZE) || 2000, step: 50 },
onChange: (value) => {
const maxPage = Number(APP_CONFIG.MAX_PAGE_SIZE) || 2000;
settings.grid.pageSize = Math.max(50, Math.min(maxPage, Number(value) || DEFAULT_SETTINGS.grid.pageSize));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.pageSize");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.InfiniteScroll.Enabled`,
category: cat(t("cat.grid"), t("setting.nav.infinite.name").replace("Majoor: ", "")),
name: t("setting.nav.infinite.name"),
tooltip: t("setting.nav.infinite.desc"),
type: "boolean",
defaultValue: !!settings.infiniteScroll?.enabled,
onChange: (value) => {
settings.infiniteScroll = settings.infiniteScroll || {};
settings.infiniteScroll.enabled = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("infiniteScroll.enabled");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Sidebar.Position`,
category: cat(t("cat.grid"), t("setting.sidebar.pos.name").replace("Majoor: ", "")),
name: t("setting.sidebar.pos.name"),
tooltip: t("setting.sidebar.pos.desc"),
type: "combo",
defaultValue: settings.sidebar?.position || "right",
options: ["left", "right"],
onChange: (value) => {
settings.sidebar = settings.sidebar || {};
settings.sidebar.position = value === "left" ? "left" : "right";
saveMajoorSettings(settings);
notifyApplied("sidebar.position");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Sidebar.ShowPreviewThumb`,
category: cat(t("cat.grid"), "Sidebar preview"),
name: "Show sidebar preview thumb",
tooltip: "Show/hide the large media preview at the top of the sidebar metadata panel.",
type: "boolean",
defaultValue: !!(settings.sidebar?.showPreviewThumb ?? true),
onChange: (value) => {
settings.sidebar = settings.sidebar || {};
settings.sidebar.showPreviewThumb = !!value;
saveMajoorSettings(settings);
notifyApplied("sidebar.showPreviewThumb");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.General.HideSiblings`,
category: cat(t("cat.grid"), t("setting.siblings.hide.name").replace("Majoor: ", "")),
name: t("setting.siblings.hide.name"),
tooltip: t("setting.siblings.hide.desc"),
type: "boolean",
defaultValue: !!settings.siblings?.hidePngSiblings,
onChange: (value) => {
settings.siblings = settings.siblings || {};
settings.siblings.hidePngSiblings = !!value;
saveMajoorSettings(settings);
notifyApplied("siblings.hidePngSiblings");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Grid.VideoAutoplayMode`,
category: cat(t("cat.grid"), t("setting.grid.videoAutoplayMode.name", "Video autoplay").replace("Majoor: ", "")),
name: t("setting.grid.videoAutoplayMode.name", "Majoor: Video autoplay"),
tooltip: t(
"setting.grid.videoAutoplayMode.desc",
"Controls video thumbnail playback in the grid. Off: static frame. Hover: play on mouse hover. Always: loop while visible."
),
type: "combo",
defaultValue: (() => {
let mode = settings.grid?.videoAutoplayMode;
// Backward compat from old boolean
if (mode === undefined || mode === null) {
mode = settings.grid?.videoHoverAutoplay === false ? "off" : "hover";
}
if (mode === true) mode = "hover";
if (mode === false) mode = "off";
if (mode !== "hover" && mode !== "always" && mode !== "off") mode = "hover";
const labels = {
off: t("setting.grid.videoAutoplayMode.off", "Off"),
hover: t("setting.grid.videoAutoplayMode.hover", "Hover"),
always: t("setting.grid.videoAutoplayMode.always", "Always"),
};
return labels[mode] || labels.off;
})(),
options: [
t("setting.grid.videoAutoplayMode.off", "Off"),
t("setting.grid.videoAutoplayMode.hover", "Hover"),
t("setting.grid.videoAutoplayMode.always", "Always"),
],
onChange: (value) => {
const labelToMode = {
[t("setting.grid.videoAutoplayMode.off", "Off")]: "off",
[t("setting.grid.videoAutoplayMode.hover", "Hover")]: "hover",
[t("setting.grid.videoAutoplayMode.always", "Always")]: "always",
};
const mode = labelToMode[value] || "off";
settings.grid = settings.grid || {};
settings.grid.videoAutoplayMode = mode;
// Clean up legacy key
delete settings.grid.videoHoverAutoplay;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("grid.videoAutoplayMode");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Cards.HoverColor`,
category: cardCat("Hover color"),
name: "Majoor: Card hover color",
tooltip: "Background tint used when hovering a card (hex, e.g. #3D3D3D).",
type: "color",
defaultValue: normalizeHexColor(settings.ui?.cardHoverColor, APP_DEFAULTS.UI_CARD_HOVER_COLOR),
onChange: (value) => {
settings.ui = settings.ui || {};
settings.ui.cardHoverColor = normalizeHexColor(value, APP_DEFAULTS.UI_CARD_HOVER_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("ui.cardHoverColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Cards.SelectionColor`,
category: cardCat("Selection color"),
name: "Majoor: Card selection color",
tooltip: "Outline/accent color used for selected cards (hex, e.g. #4A90E2).",
type: "color",
defaultValue: normalizeHexColor(settings.ui?.cardSelectionColor, APP_DEFAULTS.UI_CARD_SELECTION_COLOR),
onChange: (value) => {
settings.ui = settings.ui || {};
settings.ui.cardSelectionColor = normalizeHexColor(value, APP_DEFAULTS.UI_CARD_SELECTION_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("ui.cardSelectionColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.RatingColor`,
category: badgeCat("Rating color"),
name: "Majoor: Rating badge color",
tooltip: "Color used for rating badge text/accent (hex, e.g. #FF9500).",
type: "color",
defaultValue: normalizeHexColor(settings.ui?.ratingColor, APP_DEFAULTS.UI_RATING_COLOR),
onChange: (value) => {
settings.ui = settings.ui || {};
settings.ui.ratingColor = normalizeHexColor(value, APP_DEFAULTS.UI_RATING_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("ui.ratingColor");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Badges.TagColor`,
category: badgeCat("Tag color"),
name: "Majoor: Tags badge color",
tooltip: "Color used for tags badge text/accent (hex, e.g. #4A90E2).",
type: "color",
defaultValue: normalizeHexColor(settings.ui?.tagColor, APP_DEFAULTS.UI_TAG_COLOR),
onChange: (value) => {
settings.ui = settings.ui || {};
settings.ui.tagColor = normalizeHexColor(value, APP_DEFAULTS.UI_TAG_COLOR);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("ui.tagColor");
},
});
// ──────────────────────────────────────────────
// Section: Viewer
// ──────────────────────────────────────────────
safeAddSetting({
id: `${SETTINGS_PREFIX}.Viewer.AllowPanAtZoom1`,
category: cat(t("cat.viewer"), t("setting.viewer.pan.name").replace("Majoor: ", "")),
name: t("setting.viewer.pan.name"),
tooltip: t("setting.viewer.pan.desc"),
type: "boolean",
defaultValue: !!settings.viewer?.allowPanAtZoom1,
onChange: (value) => {
settings.viewer = settings.viewer || {};
settings.viewer.allowPanAtZoom1 = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("viewer.allowPanAtZoom1");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Viewer.DisableWebGL`,
category: cat(t("cat.viewer"), "Disable WebGL Video"),
name: "Disable WebGL Video",
tooltip: "Use CPU rendering (Canvas 2D) for video playback. Fixes 'black screen' issues on incompatible hardware/browsers.",
type: "boolean",
defaultValue: !!settings.viewer?.disableWebGL,
onChange: (value) => {
settings.viewer = settings.viewer || {};
settings.viewer.disableWebGL = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("viewer.disableWebGL");
},
});
const registerMinimapToggle = (idKey, stateKey, nameKey, descKey) => {
safeAddSetting({
id: `${SETTINGS_PREFIX}.WorkflowMinimap.${idKey}`,
category: cat(t("cat.viewer"), t(nameKey).replace("Majoor: ", "")),
name: t(nameKey),
tooltip: t(descKey),
type: "boolean",
defaultValue: !!settings.workflowMinimap?.[stateKey],
onChange: (value) => {
settings.workflowMinimap = settings.workflowMinimap || {};
settings.workflowMinimap[stateKey] = !!value;
saveMajoorSettings(settings);
notifyApplied(`workflowMinimap.${stateKey}`);
},
});
};
registerMinimapToggle("Enabled", "enabled", "setting.minimap.enabled.name", "setting.minimap.enabled.desc");
// ──────────────────────────────────────────────
// Section: Scanning
// ──────────────────────────────────────────────
safeAddSetting({
id: `${SETTINGS_PREFIX}.AutoScan.OnStartup`,
category: cat(t("cat.scanning"), t("setting.scan.startup.name").replace("Majoor: ", "")),
name: t("setting.scan.startup.name"),
tooltip: t("setting.scan.startup.desc"),
type: "boolean",
defaultValue: !!settings.autoScan?.onStartup,
onChange: (value) => {
settings.autoScan = settings.autoScan || {};
settings.autoScan.onStartup = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("autoScan.onStartup");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.RtHydrate.Concurrency`,
category: cat(t("cat.scanning"), "Hydration"),
name: "Hydrate Concurrency",
tooltip: "Maximum concurrent hydration requests for rating/tags.",
type: "number",
defaultValue: Number(settings.rtHydrate?.concurrency || APP_DEFAULTS.RT_HYDRATE_CONCURRENCY || 5),
attrs: { min: 1, max: 20, step: 1 },
onChange: (value) => {
settings.rtHydrate = settings.rtHydrate || {};
settings.rtHydrate.concurrency = Math.max(
1,
Math.min(20, Math.round(_safeNum(value, APP_DEFAULTS.RT_HYDRATE_CONCURRENCY || 5)))
);
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("rtHydrate.concurrency");
},
});
const _clampWatcherValue = (raw, fallback, min, max) => {
const parsed = Math.round(_safeNum(raw, fallback));
return Math.max(min, Math.min(max, parsed));
};
const applyWatcherSettingsFromBackend = (payload = {}) => {
const changed = [];
settings.watcher = settings.watcher || {};
if (typeof payload.debounce_ms === "number") {
const normalized = Math.max(50, Math.min(5000, Math.round(payload.debounce_ms)));
if (settings.watcher.debounceMs !== normalized) {
settings.watcher.debounceMs = normalized;
changed.push("watcher.debounceMs");
}
}
if (typeof payload.dedupe_ttl_ms === "number") {
const normalized = Math.max(100, Math.min(30000, Math.round(payload.dedupe_ttl_ms)));
if (settings.watcher.dedupeTtlMs !== normalized) {
settings.watcher.dedupeTtlMs = normalized;
changed.push("watcher.dedupeTtlMs");
}
}
if (!changed.length) {
return;
}
saveMajoorSettings(settings);
changed.forEach((key) => notifyApplied(key));
};
const syncWatcherRuntimeSettings = async () => {
try {
const res = await getWatcherSettings();
if (!res?.ok) {
return;
}
applyWatcherSettingsFromBackend(res.data || {});
} catch {}
};
safeAddSetting({
id: `${SETTINGS_PREFIX}.Watcher.Enabled`,
category: cat(t("cat.scanning"), t("setting.watcher.enabled.label", "Enable watcher")),
name: t("setting.watcher.name"),
tooltip: t("setting.watcher.desc") + " (env: MJR_ENABLE_WATCHER)",
type: "boolean",
defaultValue: !!settings.watcher?.enabled,
onChange: async (value) => {
settings.watcher = settings.watcher || {};
settings.watcher.enabled = !!value;
saveMajoorSettings(settings);
notifyApplied("watcher.enabled");
try {
const res = await toggleWatcher(!!value);
if (!res?.ok) {
settings.watcher.enabled = !value;
saveMajoorSettings(settings);
notifyApplied("watcher.enabled");
comfyToast(res?.error || "Failed to toggle watcher", "error");
}
} catch {
settings.watcher.enabled = !value;
saveMajoorSettings(settings);
notifyApplied("watcher.enabled");
}
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Watcher.DebounceDelay`,
category: cat(t("cat.scanning"), t("setting.watcher.debounce.label", "Watcher debounce delay")),
name: t("setting.watcher.debounce.name"),
tooltip: t("setting.watcher.debounce.desc") + " (env: MJR_WATCHER_DEBOUNCE_MS)",
type: "number",
defaultValue: settings.watcher?.debounceMs ?? APP_DEFAULTS.WATCHER_DEBOUNCE_MS,
attrs: { min: 50, max: 5000, step: 50 },
onChange: async (value) => {
const fallback = APP_DEFAULTS.WATCHER_DEBOUNCE_MS;
const clamped = _clampWatcherValue(value, fallback, 50, 5000);
const previous = settings.watcher?.debounceMs ?? fallback;
if (clamped === previous) return;
settings.watcher = settings.watcher || {};
settings.watcher.debounceMs = clamped;
saveMajoorSettings(settings);
try {
const res = await updateWatcherSettings({ debounce_ms: clamped });
if (!res?.ok) {
throw new Error(res?.error || t("setting.watcher.debounce.error"));
}
const backendValue = Math.round(Number(res?.data?.debounce_ms ?? clamped));
settings.watcher.debounceMs = backendValue;
saveMajoorSettings(settings);
notifyApplied("watcher.debounceMs");
} catch (error) {
settings.watcher.debounceMs = previous;
saveMajoorSettings(settings);
notifyApplied("watcher.debounceMs");
comfyToast(error?.message || t("setting.watcher.debounce.error"), "error");
}
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Watcher.DedupeWindow`,
category: cat(t("cat.scanning"), t("setting.watcher.dedupe.label", "Watcher dedupe window")),
name: t("setting.watcher.dedupe.name"),
tooltip: t("setting.watcher.dedupe.desc") + " (env: MJR_WATCHER_DEDUPE_TTL_MS)",
type: "number",
defaultValue: settings.watcher?.dedupeTtlMs ?? APP_DEFAULTS.WATCHER_DEDUPE_TTL_MS,
attrs: { min: 100, max: 30000, step: 100 },
onChange: async (value) => {
const fallback = APP_DEFAULTS.WATCHER_DEDUPE_TTL_MS;
const clamped = _clampWatcherValue(value, fallback, 100, 30000);
const previous = settings.watcher?.dedupeTtlMs ?? fallback;
if (clamped === previous) return;
settings.watcher = settings.watcher || {};
settings.watcher.dedupeTtlMs = clamped;
saveMajoorSettings(settings);
try {
const res = await updateWatcherSettings({ dedupe_ttl_ms: clamped });
if (!res?.ok) {
throw new Error(res?.error || t("setting.watcher.dedupe.error"));
}
const backendValue = Math.round(Number(res?.data?.dedupe_ttl_ms ?? clamped));
settings.watcher.dedupeTtlMs = backendValue;
saveMajoorSettings(settings);
notifyApplied("watcher.dedupeTtlMs");
} catch (error) {
settings.watcher.dedupeTtlMs = previous;
saveMajoorSettings(settings);
notifyApplied("watcher.dedupeTtlMs");
comfyToast(error?.message || t("setting.watcher.dedupe.error"), "error");
}
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Watcher.MaxPending`,
category: cat(t("cat.scanning"), "Watcher queue"),
name: "Watcher: max pending files",
tooltip: "Maximum number of pending watcher files kept in memory.",
type: "number",
defaultValue: Number(settings.watcher?.maxPending ?? 500),
attrs: { min: 10, max: 5000, step: 10 },
onChange: (value) => {
settings.watcher = settings.watcher || {};
settings.watcher.maxPending = Math.max(10, Math.min(5000, Math.round(_safeNum(value, 500))));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("watcher.maxPending");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Watcher.MinSize`,
category: cat(t("cat.scanning"), "Watcher file size"),
name: "Watcher: min size (bytes)",
tooltip: "Minimum file size indexed by watcher.",
type: "number",
defaultValue: Number(settings.watcher?.minSize ?? 100),
attrs: { min: 0, max: 1000000, step: 100 },
onChange: (value) => {
settings.watcher = settings.watcher || {};
settings.watcher.minSize = Math.max(0, Math.min(1000000, Math.round(_safeNum(value, 100))));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("watcher.minSize");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Watcher.MaxSize`,
category: cat(t("cat.scanning"), "Watcher file size"),
name: "Watcher: max size (bytes)",
tooltip: "Maximum file size indexed by watcher.",
type: "number",
defaultValue: Number(settings.watcher?.maxSize ?? 4294967296),
attrs: { min: 100000, max: 17179869184, step: 100000 },
onChange: (value) => {
settings.watcher = settings.watcher || {};
settings.watcher.maxSize = Math.max(100000, Math.min(17179869184, Math.round(_safeNum(value, 4294967296))));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("watcher.maxSize");
},
});
try {
syncWatcherRuntimeSettings().catch(() => {});
} catch {}
safeAddSetting({
id: `${SETTINGS_PREFIX}.RatingTagsSync.Enabled`,
category: cat(t("cat.scanning"), t("setting.sync.rating.name").replace("Majoor: ", "")),
name: t("setting.sync.rating.name"),
tooltip: t("setting.sync.rating.desc"),
type: "boolean",
defaultValue: !!settings.ratingTagsSync?.enabled,
onChange: (value) => {
settings.ratingTagsSync = settings.ratingTagsSync || {};
settings.ratingTagsSync.enabled = !!value;
saveMajoorSettings(settings);
notifyApplied("ratingTagsSync.enabled");
},
});
// ──────────────────────────────────────────────
// Section: Security
// ──────────────────────────────────────────────
safeAddSetting({
id: `${SETTINGS_PREFIX}.Safety.ConfirmDeletion`,
category: cat(t("cat.security"), "Confirm before deleting"),
name: "Confirm before deleting",
tooltip: "Show a confirmation dialog before deleting files. Disabling this allows instant deletion.",
type: "boolean",
defaultValue: settings.safety?.confirmDeletion !== false,
onChange: (value) => {
settings.safety = settings.safety || {};
settings.safety.confirmDeletion = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("safety.confirmDeletion");
},
});
const registerSecurityToggle = (key, nameKey, descKey, sectionKey = "cat.security") => {
safeAddSetting({
id: `${SETTINGS_PREFIX}.Security.${key}`,
category: cat(t(sectionKey), t(nameKey).replace("Majoor: ", "")),
name: t(nameKey),
tooltip: t(descKey),
type: "boolean",
defaultValue: !!settings.security?.[key],
onChange: (value) => {
settings.security = settings.security || {};
settings.security[key] = !!value;
saveMajoorSettings(settings);
notifyApplied(`security.${key}`);
try {
const sec = settings.security || {};
setSecuritySettings({
safe_mode: _safeBool(sec.safeMode, false),
allow_write: _safeBool(sec.allowWrite, true),
allow_remote_write: _safeBool(sec.allowRemoteWrite, true),
allow_delete: _safeBool(sec.allowDelete, true),
allow_rename: _safeBool(sec.allowRename, true),
allow_open_in_folder: _safeBool(sec.allowOpenInFolder, true),
allow_reset_index: _safeBool(sec.allowResetIndex, false),
})
.then((res) => {
if (res?.ok && res.data?.prefs) {
syncBackendSecuritySettings();
} else if (res && res.ok === false) {
console.warn("[Majoor] backend security settings update failed", res.error || res);
}
})
.catch(() => {});
} catch {}
},
});
};
registerSecurityToggle("safeMode", "setting.sec.safe.name", "setting.sec.safe.desc");
registerSecurityToggle("allowWrite", "setting.sec.write.name", "setting.sec.write.desc");
registerSecurityToggle("allowDelete", "setting.sec.del.name", "setting.sec.del.desc");
registerSecurityToggle("allowRename", "setting.sec.ren.name", "setting.sec.ren.desc");
registerSecurityToggle("allowOpenInFolder", "setting.sec.open.name", "setting.sec.open.desc");
registerSecurityToggle("allowResetIndex", "setting.sec.reset.name", "setting.sec.reset.desc");
// ──────────────────────────────────────────────
// Section: Remote
// ──────────────────────────────────────────────
safeAddSetting({
id: `${SETTINGS_PREFIX}.Security.ApiToken`,
category: cat(t("cat.remote"), t("setting.sec.token.name").replace("Majoor: ", "")),
name: t("setting.sec.token.name", "Majoor: API Token"),
tooltip: t(
"setting.sec.token.desc",
"Store the API token used for write operations. Majoor sends it via X-MJR-Token and Authorization headers."
),
type: "text",
defaultValue: settings.security?.apiToken || "",
attrs: {
placeholder: t("setting.sec.token.placeholder", "Leave blank to disable."),
},
onChange: (value) => {
settings.security = settings.security || {};
settings.security.apiToken = typeof value === "string" ? value.trim() : "";
saveMajoorSettings(settings);
notifyApplied("security.apiToken");
},
});
registerSecurityToggle(
"allowRemoteWrite",
"setting.sec.remote.name",
"setting.sec.remote.desc",
"cat.remote"
);
// ──────────────────────────────────────────────
// Section: Advanced
// ──────────────────────────────────────────────
const languages = getSupportedLanguages();
const langOptions = languages.map(l => l.code);
safeAddSetting({
id: `${SETTINGS_PREFIX}.Language`,
category: cat(t("cat.advanced"), t("setting.language.name", "Language")),
name: t("setting.language.name", "Majoor: Language"),
tooltip: t("setting.language.desc", "Choose the language for the Assets Manager interface. Reload required to fully apply."),
type: "combo",
defaultValue: getCurrentLang(),
options: langOptions,
onChange: (value) => {
if (langOptions.includes(value)) {
setLang(value);
notifyApplied("language");
}
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.ProbeBackend.Mode`,
category: cat(t("cat.advanced"), t("setting.probe.mode.name").replace("Majoor: ", "")),
name: t("setting.probe.mode.name"),
tooltip: t("setting.probe.mode.desc") + " (env: MAJOOR_MEDIA_PROBE_BACKEND)",
type: "combo",
defaultValue: settings.probeBackend?.mode || DEFAULT_SETTINGS.probeBackend.mode,
options: ["auto", "exiftool", "ffprobe", "both"],
onChange: (value) => {
const mode = _safeOneOf(value, ["auto", "exiftool", "ffprobe", "both"], DEFAULT_SETTINGS.probeBackend.mode);
settings.probeBackend = settings.probeBackend || {};
settings.probeBackend.mode = mode;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("probeBackend.mode");
setProbeBackendMode(mode).catch(() => {});
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.MetadataFallback.Image`,
category: cat(t("cat.advanced"), "Metadata"),
name: "Majoor: Metadata Fallback (Images)",
tooltip: "Enable Pillow fallback when ExifTool is missing or fails.",
type: "boolean",
defaultValue: settings.metadataFallback?.image ?? DEFAULT_SETTINGS.metadataFallback.image,
onChange: async (value) => {
const next = !!value;
const previous = !!(settings.metadataFallback?.image ?? DEFAULT_SETTINGS.metadataFallback.image);
settings.metadataFallback = settings.metadataFallback || {};
settings.metadataFallback.image = next;
saveMajoorSettings(settings);
notifyApplied("metadataFallback.image");
try {
const res = await setMetadataFallbackSettings({
image: next,
media: settings.metadataFallback?.media ?? DEFAULT_SETTINGS.metadataFallback.media,
});
if (!res?.ok) throw new Error(res?.error || "Failed to update metadata fallback settings");
} catch (error) {
settings.metadataFallback.image = previous;
saveMajoorSettings(settings);
notifyApplied("metadataFallback.image");
comfyToast(error?.message || "Failed to update metadata fallback settings", "error");
}
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.MetadataFallback.Media`,
category: cat(t("cat.advanced"), "Metadata"),
name: "Majoor: Metadata Fallback (Audio/Video)",
tooltip: "Enable hachoir fallback when ffprobe is missing or fails.",
type: "boolean",
defaultValue: settings.metadataFallback?.media ?? DEFAULT_SETTINGS.metadataFallback.media,
onChange: async (value) => {
const next = !!value;
const previous = !!(settings.metadataFallback?.media ?? DEFAULT_SETTINGS.metadataFallback.media);
settings.metadataFallback = settings.metadataFallback || {};
settings.metadataFallback.media = next;
saveMajoorSettings(settings);
notifyApplied("metadataFallback.media");
try {
const res = await setMetadataFallbackSettings({
image: settings.metadataFallback?.image ?? DEFAULT_SETTINGS.metadataFallback.image,
media: next,
});
if (!res?.ok) throw new Error(res?.error || "Failed to update metadata fallback settings");
} catch (error) {
settings.metadataFallback.media = previous;
saveMajoorSettings(settings);
notifyApplied("metadataFallback.media");
comfyToast(error?.message || "Failed to update metadata fallback settings", "error");
}
},
});
try {
getMetadataFallbackSettings()
.then((res) => {
if (!res?.ok || !res?.data?.prefs) return;
const prefs = res.data.prefs || {};
const image = !!(prefs.image ?? DEFAULT_SETTINGS.metadataFallback.image);
const media = !!(prefs.media ?? DEFAULT_SETTINGS.metadataFallback.media);
settings.metadataFallback = settings.metadataFallback || {};
let changed = false;
if (settings.metadataFallback.image !== image) {
settings.metadataFallback.image = image;
changed = true;
}
if (settings.metadataFallback.media !== media) {
settings.metadataFallback.media = media;
changed = true;
}
if (changed) {
saveMajoorSettings(settings);
notifyApplied("metadataFallback");
}
})
.catch(() => {});
} catch {}
let _outputDirCommittedValue = String(settings.paths?.outputDirectory || "");
let _outputDirSaveTimer = null;
let _outputDirSaveSeq = 0;
let _outputDirSaveAbort = null;
safeAddSetting({
id: `${SETTINGS_PREFIX}.Paths.OutputDirectory`,
category: cat(t("cat.advanced"), "Paths"),
name: "Majoor: Output Directory Override",
tooltip: "Override ComfyUI output directory used by Majoor (equivalent to --output-directory). Leave empty to keep current backend default.",
type: "text",
defaultValue: String(settings.paths?.outputDirectory || ""),
attrs: {
placeholder: "D:\\\\____COMFY_OUTPUTS",
},
onChange: async (value) => {
const next = String(value || "").trim();
settings.paths = settings.paths || {};
settings.paths.outputDirectory = next;
saveMajoorSettings(settings);
// The Comfy settings text input can fire on each keystroke.
// Debounce + cancel in-flight requests to prevent backend/UI stalls.
try {
if (_outputDirSaveTimer) {
clearTimeout(_outputDirSaveTimer);
_outputDirSaveTimer = null;
}
} catch {}
_outputDirSaveTimer = setTimeout(async () => {
_outputDirSaveTimer = null;
const seq = ++_outputDirSaveSeq;
try {
_outputDirSaveAbort?.abort?.();
} catch {}
_outputDirSaveAbort = typeof AbortController !== "undefined" ? new AbortController() : null;
try {
const res = await setOutputDirectorySetting(
next,
_outputDirSaveAbort ? { signal: _outputDirSaveAbort.signal } : {}
);
if (seq !== _outputDirSaveSeq) return;
if (!res?.ok) {
throw new Error(res?.error || "Failed to set output directory");
}
const serverValue = String(res?.data?.output_directory || next).trim();
settings.paths.outputDirectory = serverValue;
_outputDirCommittedValue = serverValue;
saveMajoorSettings(settings);
notifyApplied("paths.outputDirectory");
} catch (error) {
if (seq !== _outputDirSaveSeq) return;
const aborted = String(error?.name || "") === "AbortError" || String(error?.code || "") === "ABORTED";
if (aborted) return;
settings.paths.outputDirectory = _outputDirCommittedValue;
saveMajoorSettings(settings);
notifyApplied("paths.outputDirectory");
comfyToast(error?.message || "Failed to set output directory", "error");
}
}, 700);
},
});
try {
getOutputDirectorySetting()
.then((res) => {
if (!res?.ok) return;
const serverValue = String(res?.data?.output_directory || "").trim();
settings.paths = settings.paths || {};
if (settings.paths.outputDirectory !== serverValue) {
settings.paths.outputDirectory = serverValue;
_outputDirCommittedValue = serverValue;
saveMajoorSettings(settings);
notifyApplied("paths.outputDirectory");
}
})
.catch(() => {});
} catch {}
safeAddSetting({
id: `${SETTINGS_PREFIX}.Db.Timeout`,
category: cat(t("cat.advanced"), "Database"),
name: "DB Timeout (ms)",
tooltip: "Client-side DB timeout preference (stored locally).",
type: "number",
defaultValue: Number(settings.db?.timeoutMs || 5000),
attrs: { min: 1000, max: 30000, step: 1000 },
onChange: (value) => {
settings.db = settings.db || {};
settings.db.timeoutMs = Math.max(1000, Math.min(30000, Math.round(_safeNum(value, 5000))));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("db.timeoutMs");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Db.MaxConnections`,
category: cat(t("cat.advanced"), "Database"),
name: "DB Max Connections",
tooltip: "Client-side DB max connections preference (stored locally).",
type: "number",
defaultValue: Number(settings.db?.maxConnections || 10),
attrs: { min: 1, max: 100, step: 1 },
onChange: (value) => {
settings.db = settings.db || {};
settings.db.maxConnections = Math.max(1, Math.min(100, Math.round(_safeNum(value, 10))));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("db.maxConnections");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Db.QueryTimeout`,
category: cat(t("cat.advanced"), "Database"),
name: "DB Query Timeout (ms)",
tooltip: "Client-side DB query timeout preference (stored locally).",
type: "number",
defaultValue: Number(settings.db?.queryTimeoutMs || 1000),
attrs: { min: 500, max: 10000, step: 500 },
onChange: (value) => {
settings.db = settings.db || {};
settings.db.queryTimeoutMs = Math.max(500, Math.min(10000, Math.round(_safeNum(value, 1000))));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("db.queryTimeoutMs");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Observability.Enabled`,
category: cat(t("cat.advanced"), t("setting.obs.enabled.name").replace("Majoor: ", "")),
name: t("setting.obs.enabled.name"),
tooltip: t("setting.obs.enabled.desc"),
type: "boolean",
defaultValue: !!settings.observability?.enabled,
onChange: (value) => {
settings.observability = settings.observability || {};
settings.observability.enabled = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("observability.enabled");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.Observability.VerboseErrors`,
category: cat(t("cat.advanced"), "Verbose error logging"),
name: "Verbose error logging",
tooltip: "Show detailed error messages in toasts and console. Useful for debugging.",
type: "boolean",
defaultValue: !!settings.observability?.verboseErrors,
onChange: (value) => {
settings.observability = settings.observability || {};
settings.observability.verboseErrors = !!value;
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("observability.verboseErrors");
},
});
// ------------------------
// Search UI setting
// ------------------------
safeAddSetting({
id: `${SETTINGS_PREFIX}.Search.MaxResults`,
category: cat(t("cat.search", "Search")),
name: t("setting.search.maxResults.name", "Max search results (client)"),
tooltip: t(
"setting.search.maxResults.desc",
"Maximum number of results requested per search. The backend still enforces MAJOOR_SEARCH_MAX_LIMIT; increase that env var if you need a higher hard cap."
),
type: "number",
defaultValue: Number(settings.search?.maxResults || APP_DEFAULTS.SEARCH_DEFAULT_LIMIT),
attrs: { min: 10, max: APP_DEFAULTS.MAX_PAGE_SIZE || 2000, step: 1 },
onChange: (value) => {
settings.search = settings.search || {};
settings.search.maxResults = Math.max(10, Math.min(APP_DEFAULTS.MAX_PAGE_SIZE || 2000, Number(value) || APP_DEFAULTS.SEARCH_DEFAULT_LIMIT));
saveMajoorSettings(settings);
applySettingsToConfig(settings);
notifyApplied("search.maxResults");
},
});
safeAddSetting({
id: `${SETTINGS_PREFIX}.EnvVars.Reference`,
category: cat(t("cat.advanced"), "Environment variables"),
name: "Environment variables reference",
tooltip: [
"Set these env vars before starting ComfyUI to override defaults:",
"",
"MAJOOR_OUTPUT_DIRECTORY — Override output root directory",
"MAJOOR_EXIFTOOL_PATH — Path to exiftool binary",
"MAJOOR_FFPROBE_PATH — Path to ffprobe binary",
"MAJOOR_MEDIA_PROBE_BACKEND — Probe mode: auto|exiftool|ffprobe|both",
"MAJOOR_EXIFTOOL_TIMEOUT — ExifTool timeout in seconds (default: 15)",
"MAJOOR_FFPROBE_TIMEOUT — FFprobe timeout in seconds (default: 10)",
"MAJOOR_DB_TIMEOUT — Database timeout in seconds (default: 30)",
"MAJOOR_DB_MAX_CONNECTIONS — Max DB connections (default: 8)",
"MAJOOR_METADATA_CACHE_MAX — Metadata cache max entries (default: 100000)",
"MAJOOR_METADATA_EXTRACT_CONCURRENCY — Parallel metadata workers (default: 1)",
"MJR_ENABLE_WATCHER — Enable file watcher: 1|0 (default: 1)",
"MJR_WATCHER_DEBOUNCE_MS — Watcher debounce delay in ms (default: 3000)",
"MJR_WATCHER_DEDUPE_TTL_MS — Watcher dedupe window in ms (default: 3000)",
"MJR_WATCHER_MAX_FILE_SIZE_BYTES — Max file size to index (default: 512MB)",
"MJR_WATCHER_FLUSH_MAX_FILES — Max files per flush batch (default: 256)",
"MJR_WATCHER_PENDING_MAX — Max pending watcher queue (default: 5000)",
"MAJOOR_SEARCH_MAX_LIMIT — Max search results (default: 500)",
"MAJOOR_BG_SCAN_ON_LIST — Scan on directory list: 0|1 (default: 0)",
].join("\n"),
type: "text",
defaultValue: "Hover for full list of environment variables",
});
try {
settingsApi[SETTINGS_REG_FLAG] = true;
} catch {}
try {
console.info("[Majoor] Settings registered successfully.");
window[SETTINGS_DEBUG_KEY] = { ...(window?.[SETTINGS_DEBUG_KEY] || {}), registered: true };
} catch {}
return true;
};
if (!tryRegister()) {
try {
const maxAttempts = 30;
const delayMs = 250;
let attempts = 0;
const tick = () => {
attempts += 1;
if (tryRegister()) return;
if (attempts >= maxAttempts) {
try {
const debug = window?.[SETTINGS_DEBUG_KEY] || {};
if (!debug.failedLogged) {
console.warn(
"[Majoor] Failed to register settings after retries. ComfyUI settings API may be unavailable in this build."
);
window[SETTINGS_DEBUG_KEY] = { ...debug, failedLogged: true };
}
} catch {}
return;
}
setTimeout(tick, delayMs);
};
setTimeout(tick, delayMs);
} catch {}
}
startRuntimeStatusDashboard();
// Best-effort: apply watcher state to backend to match saved settings.
try {
const desired = !!settings?.watcher?.enabled;
setTimeout(() => {
toggleWatcher(desired).catch(() => {});
}, 0);
} catch {}
return settings;
};
// 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;
if (typeof enabled === "boolean" && enabled !== !!settings.watcher.enabled) {
settings.watcher.enabled = enabled;
saveMajoorSettings(settings);
safeDispatchCustomEvent("mjr-settings-changed", { key: "watcher.enabled" }, { warnPrefix: "[Majoor]" });
}
})
.catch(() => {});
}
} catch {}