- 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>
290 lines
13 KiB
JavaScript
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);
|
|
}
|