Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_settings_page.js
T
DemonGatanjieuandClaude Opus 5.5 6c8d3e8eb4 Model Check bar: "Don't show again" and a check-on-open switch
- The bar starts with "Model Check:" so it is clear it comes from the browser.
- "Don't show again" turns the check on opening off and says where to turn it
  back on; Settings → Workflows has the same switch (on by default), and turning
  it on checks the open workflow right away. While off, the bar never shows.
- The tour's Model Check step mentions the button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:27:24 +08:00

290 lines
13 KiB
JavaScript

/**
* The settings page (the rail's gear): one card per subject — how the browser looks,
* model cards and what they cost in memory (with the card image cache), model folders
* (a view of its own, ui_folder_manager.js, `owner.settingsView === 'folders'`),
* workflows, how the browser opens, and help. A tool page like the scan page, with a
* way back to the page you came from. Display preferences go through `owner.displayPrefs`
* (ui_settings_hub.js); language, theme and opening mode are ComfyUI settings.
*/
import { app } from "../../../scripts/app.js";
import { translate } from './locales.js';
import { ABYSSAL_SCARLET_SETTING_ID, LANGUAGE_SETTING_ID, applyLanguagePreference, setAbyssalScarletTheme } from './interface_settings.js';
import { ENTRY_MODE_SETTING_ID } from './browser_entry.js';
import { showUpdateGuide } from './ui_update_guide.js';
import { startSpotlightTour } from './ui_spotlight_tour.js';
import { copyDiagnostics } from './feedback.js';
import { renderFolderPage } from './ui_folder_manager.js';
import { openFeedbackDialog } from './ui_feedback_dialog.js';
import { checkOnOpen, setCheckOnOpen } from './ui_doctor_banner.js';
const t = (key, params) => translate(key, params);
// The full written guide: the README, at its Chinese half for Chinese.
const README_URL = 'https://github.com/DemonGatanjieu/Anomalous_Model_Browser#readme';
const README_ZH_URL = 'https://github.com/DemonGatanjieu/Anomalous_Model_Browser#-视频演示与教程';
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function button(className, label, onClick) {
const node = el('button', className, label);
node.type = 'button';
node.onclick = onClick;
return node;
}
function comfySetting(id) {
try { return app.extensionManager?.setting?.get(id); } catch (_) { return undefined; }
}
async function setComfySetting(id, value) {
const settings = app.extensionManager?.setting;
if (typeof settings?.set !== 'function') return false;
try {
await settings.set(id, value);
return true;
} catch (_) {
return false;
}
}
/** A row: name and what it does on the left, its control on the right. */
function row(titleKey, helpKey, control, { help = null } = {}) {
const line = el('div', 'anomalous-scan-setting');
const copy = el('span', 'anomalous-scan-setting-copy');
copy.append(el('span', 'anomalous-scan-setting-title', t(titleKey)));
const helpText = help ?? (helpKey ? t(helpKey) : '');
if (helpText) copy.append(el('small', 'anomalous-scan-setting-help', helpText));
line.append(copy, control);
return line;
}
/** Choices side by side; `onPick(value)` when one is clicked. */
function segment(choices, current, onPick) {
const box = el('div', 'anomalous-scan-segment');
box.setAttribute('role', 'radiogroup');
for (const [value, labelKey] of choices) {
const choice = button('anomalous-scan-segment-btn', t(labelKey), () => onPick(value));
choice.setAttribute('role', 'radio');
choice.setAttribute('aria-checked', String(value === current));
box.append(choice);
}
return box;
}
function toggleSwitch(checked, onChange) {
const input = el('input', 'anomalous-scan-switch');
input.type = 'checkbox';
input.checked = checked;
input.onchange = () => onChange(input.checked);
return input;
}
/** A slider with its value shown as a percentage. */
function slider(value, { min, max, step }, onCommit) {
const box = el('label', 'anomalous-settings-slider');
const input = el('input');
input.type = 'range';
Object.assign(input, { min: String(min), max: String(max), step: String(step), value: String(value) });
const shown = el('span', 'anomalous-settings-slider-value', `${Math.round(value * 100)}%`);
input.oninput = () => { shown.textContent = `${Math.round(Number(input.value) * 100)}%`; onCommit(Number(input.value)); };
box.append(input, shown);
return box;
}
function group(titleKey, ...rows) {
const box = el('section', 'anomalous-scan-card anomalous-scan-group');
box.append(el('h3', 'anomalous-scan-group-title', t(titleKey)), ...rows);
return box;
}
function formatBytes(bytes) {
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
}
/** The card image cache: how much it holds, and a way to empty it. */
function cacheRow(owner) {
const clear = button('anomalous-scan-secondary anomalous-scan-small-btn', t('settingsCacheClear'), async () => {
clear.disabled = true;
try {
await fetch('/anomalous/card_cache/clear', { method: 'POST' });
} finally {
clear.disabled = false;
refresh();
}
});
const line = row('settingsCacheTitle', null, clear, { help: t('settingsCacheLoading') });
const help = line.querySelector('.anomalous-scan-setting-help');
const refresh = async () => {
try {
const data = await (await fetch('/anomalous/card_cache')).json();
help.textContent = t('settingsCacheHelp', { count: data.count, size: formatBytes(data.bytes), limit: formatBytes(data.limit) });
clear.disabled = !data.count;
} catch (_) {
help.textContent = t('settingsCacheUnknown');
}
};
refresh();
return line;
}
function appearanceGroup(owner, redraw) {
const prefs = owner.displayPrefs.get();
const language = localStorage.getItem('anomalous_lang') || 'auto';
const abyssal = localStorage.getItem('anomalous_theme_abyssal_scarlet') === 'true';
const rows = [
row('sidebarViewMode', 'settingsViewModeHelp', segment(
[['compact', 'sidebarViewModeCompact'], ['standard', 'sidebarViewModeStandard'], ['aesthetic', 'sidebarViewModeAesthetic']],
prefs.viewMode, mode => { owner.displayPrefs.setViewMode(mode); redraw(); })),
];
if (prefs.viewMode === 'aesthetic') {
rows.push(row('sidebarBgAtmosphere', 'settingsAtmosphereHelp',
slider(prefs.bgOpacity, { min: 0, max: 1, step: 0.05 }, value => owner.displayPrefs.setBgOpacity(value))));
}
rows.push(
row('sidebarUiScale', 'settingsScaleHelp', slider(prefs.scale, { min: 0.5, max: 1.5, step: 0.1 }, value => owner.displayPrefs.setScale(value))),
row('settingsTheme', 'settingsThemeHelp', segment([['default', 'settingsThemeDefault'], ['abyssal', 'settingsThemeAbyssal']],
abyssal ? 'abyssal' : 'default', value => { setAbyssalScarletTheme(value === 'abyssal', true); redraw(); })),
row('mainLanguageSetting', null, segment([['auto', 'mainLanguageAuto'], ['zh', 'mainLanguageChinese'], ['en', 'mainLanguageEnglish']],
language, async value => {
// The ComfyUI setting's own change handler switches the language; without it, switch here.
if (!await setComfySetting(LANGUAGE_SETTING_ID, value)) applyLanguagePreference(value);
})),
);
return group('settingsAppearance', ...rows);
}
function cardsGroup(owner, redraw) {
const reload = () => { owner.loadModels(); redraw(); };
return group('settingsCards',
row('sidebarCardQuality', 'settingsCardQualityHelp', segment(
[['balanced', 'sidebarOptimizedThumbnail'], ['original', 'sidebarOriginalCover']], owner.cardThumbnailMode, value => {
owner.cardThumbnailMode = value === 'original' ? 'original' : 'balanced';
localStorage.setItem('anomalous_card_thumbnail_mode', owner.cardThumbnailMode);
reload();
})),
row('sidebarVideoPlayback', 'settingsVideoHelp', segment(
[['hover', 'sidebarHoverPlay'], ['always', 'sidebarAlwaysPlay']], owner.energySaving ? 'hover' : 'always', value => {
owner.energySaving = value === 'hover';
localStorage.setItem('anomalous_energy_saving', String(owner.energySaving));
reload();
})),
cacheRow(owner),
);
}
function foldersGroup(owner) {
return group('settingsFolders',
row('sidebarManageFolders', 'settingsFoldersHelp',
button('anomalous-scan-secondary anomalous-scan-small-btn', t('settingsOpen'), () => {
owner.settingsView = 'folders';
render(owner);
})));
}
function workflowGroup(owner) {
return group('settingsWorkflows',
row('settingsCheckOnOpen', 'settingsCheckOnOpenHelp', toggleSwitch(checkOnOpen(), on => setCheckOnOpen(owner, on))),
row('sidebarProvenance', 'sidebarProvenanceDesc', toggleSwitch(localStorage.getItem('anomalous_inject_hash') !== 'false', on => {
// Read by hash_resolver.js when a workflow is saved.
localStorage.setItem('anomalous_inject_hash', on ? 'true' : 'false');
})));
}
function openingGroup(owner, redraw) {
const mode = comfySetting(ENTRY_MODE_SETTING_ID) || 'floating';
return group('settingsOpening',
row('mainEntryModeSetting', 'settingsEntryHelp', segment(
[['floating', 'mainEntryModeFloating'], ['topbar', 'mainEntryModeTopbar'], ['menu', 'mainEntryModeMenu']], mode, async value => {
await setComfySetting(ENTRY_MODE_SETTING_ID, value);
redraw();
})),
row('settingsLayout', 'settingsLayoutHelp', button('anomalous-scan-secondary anomalous-scan-small-btn', t('settingsReset'), () => {
if (!confirm(t('sidebarResetConfirm'))) return;
owner.displayPrefs.resetLayout();
redraw();
})),
);
}
function helpGroup(owner) {
const small = (label, onClick) => button('anomalous-scan-secondary anomalous-scan-small-btn', label, onClick);
return group('settingsHelp',
row('settingsTour', 'settingsTourHelp', small(t('settingsStart'), () => startSpotlightTour(owner))),
row('updateGuideReplay', 'settingsNewsHelp', small(t('settingsOpen'), () => showUpdateGuide(owner, { force: true }))),
row('settingsGuide', 'settingsGuideHelp', small(t('settingsOpenGuide'), () => {
window.open(window.anomalous_browser_lang === 'zh' ? README_ZH_URL : README_URL, '_blank', 'noopener');
})),
row('feedbackReport', 'settingsReportHelp', small(t('feedbackWrite'), () => openFeedbackDialog(owner, 'bug'))),
row('feedbackSuggest', 'settingsSuggestHelp', small(t('feedbackWrite'), () => openFeedbackDialog(owner, 'idea'))),
row('feedbackCopy', 'feedbackCopyHint', (() => {
const copy = small(t('feedbackCopy'), async () => {
copy.textContent = t(await copyDiagnostics(owner) ? 'feedbackCopied' : 'feedbackCopyFailed');
setTimeout(() => { copy.textContent = t('feedbackCopy'); }, 1600);
});
return copy;
})()),
);
}
function render(owner) {
const panel = owner.settingsPanel;
const view = owner.settingsView || '';
const top = panel._settingsView === view ? panel.scrollTop : 0;
panel._settingsView = view;
const redraw = () => render(owner);
const page = el('div', 'anomalous-scan-page');
if (view === 'folders') {
panel.replaceChildren(page);
panel.scrollTop = 0;
renderFolderPage(owner, page, () => {
owner.settingsView = '';
render(owner);
});
return;
}
page.append(
button('anomalous-scan-back', t('settingsBack'), () => leaveSettingsPage(owner)),
el('h1', 'anomalous-scan-title', t('sidebarSettings')),
appearanceGroup(owner, redraw),
cardsGroup(owner, redraw),
foldersGroup(owner),
workflowGroup(owner),
openingGroup(owner, redraw),
helpGroup(owner),
);
panel.replaceChildren(page);
panel.scrollTop = top;
}
export function isSettingsPageOpen(owner) {
return Boolean(owner.settingsPanel && owner.settingsPanel.style.display !== 'none');
}
/** Opens the page; `keepReturn`: drawn again in place (language change), Back still goes where it went. */
export function openSettingsPage(owner, { keepReturn = false } = {}) {
if (!keepReturn && !isSettingsPageOpen(owner)) {
owner.settingsReturn = owner.currentShellPage?.() || 'home';
owner.settingsView = '';
}
owner.enterToolPage?.('settings');
owner.hideAllPanels();
owner.settingsPanel.style.display = 'flex';
document.getElementById('anomalous-global-settings-btn')?.classList.add('is-active');
render(owner);
}
/** Back to the page the gear was pressed on. */
export function leaveSettingsPage(owner) {
const back = owner.settingsReturn || 'home';
owner.settingsReturn = null;
if (back === 'scan') owner.openScanPage();
else if (back === 'doctor') owner.openDoctorPage();
else owner.goTo(['assistant', 'settings'].includes(back) ? 'home' : back);
}