Settings -> Appearance -> Theme (and the ComfyUI setting Anomalous.ModelBrowser.Theme, which replaces the Abyssal Scarlet switch) picks Follow ComfyUI (default), Dark, Light or Abyssal Scarlet. Following ComfyUI watches the dark-theme class ComfyUI sets on <html> for a dark palette and repaints at once; an older Abyssal switch reads as Abyssal. The light theme is html.amb-theme-light with its own --amb-* values. Hard coded colors were turned into tokens whose dark values are the colors they replace (checked: every stylesheet resolves to the same dark values): rgba(var(--amb-ink-rgb), a) for tints, status shades, tooltip, float, dialog, sunken, scrim and note tokens. Older rules and inline styles that wrote a dark color now write var(--amb-lt-<role>, <that color>); the --amb-lt-* tokens exist only in the light theme. Inside the plugin's windows ComfyUI's palette variables (--bg-color, --fg-color ...) follow the plugin's theme when the two differ. Surfaces over images stay dark, and the image viewer gets the dark tokens back. The ready banner on a recipe reads green instead of pink. Stylesheet cache versions are bumped. Checked on a test ComfyUI in light and dark: home, models, gallery and its viewer, workflows and a recipe's tabs, combos and a combo, prompts and the docked drawer, voices, audio library, settings, scan, Model Check, current node and its model picker, a model's detail with notes, share, save recipe, update guide, versions, GPT-SoVITS settings and the prompt dialog; light with a dark ComfyUI, and Abyssal Scarlet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
9.4 KiB
JavaScript
237 lines
9.4 KiB
JavaScript
import { app } from '../../../scripts/app.js';
|
|
import { normalizeLocale, resolveLocale, translate } from './locales.js';
|
|
|
|
export const LANGUAGE_SETTING_ID = 'Anomalous.ModelBrowser.Language';
|
|
export const THEME_SETTING_ID = 'Anomalous.ModelBrowser.Theme';
|
|
|
|
let defaultLang = 'zh';
|
|
try {
|
|
let comfyDetected = false;
|
|
const aglLang = localStorage.getItem('Comfy.Settings.AIGODLIKE-COMFYUI-TRANSLATION.Language');
|
|
if (aglLang) {
|
|
defaultLang = aglLang.toLowerCase().includes('en') ? 'en' : 'zh';
|
|
comfyDetected = true;
|
|
} else {
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (!key || (!key.toLowerCase().includes('lang') && !key.toLowerCase().includes('locale'))) continue;
|
|
const value = localStorage.getItem(key);
|
|
if (typeof value !== 'string') continue;
|
|
const normalizedValue = value.toLowerCase();
|
|
if (normalizedValue.includes('zh') || normalizedValue.includes('chinese')) {
|
|
defaultLang = 'zh';
|
|
comfyDetected = true;
|
|
break;
|
|
}
|
|
if (normalizedValue.includes('en') || normalizedValue.includes('english')) {
|
|
defaultLang = 'en';
|
|
comfyDetected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!comfyDetected && navigator.language && !navigator.language.toLowerCase().startsWith('zh')) {
|
|
defaultLang = 'en';
|
|
}
|
|
} catch (_) {
|
|
if (navigator.language && !navigator.language.toLowerCase().startsWith('zh')) {
|
|
defaultLang = 'en';
|
|
}
|
|
}
|
|
|
|
let currentLang = resolveLocale(localStorage.getItem('anomalous_lang') || defaultLang);
|
|
window.anomalous_browser_lang = currentLang;
|
|
|
|
export const t = (key, params) => translate(key, params, window.anomalous_browser_lang || currentLang);
|
|
export const getCurrentLanguage = () => currentLang;
|
|
|
|
function normalizeLanguagePreference(value) {
|
|
return value === 'zh' || value === 'en' ? value : 'auto';
|
|
}
|
|
|
|
function resolveComfyLanguage() {
|
|
try {
|
|
const settings = app.extensionManager?.setting;
|
|
const locale = settings?.get('Comfy.Locale')
|
|
|| app.ui?.settings?.getSettingValue?.('Comfy.Locale')
|
|
|| app.ui?.settings?.getSettingValue?.('Comfy.Locale.Language');
|
|
return normalizeLocale(locale) || defaultLang;
|
|
} catch (_) {
|
|
return defaultLang;
|
|
}
|
|
}
|
|
|
|
function getSettingTranslationPatches() {
|
|
const category = t('mainInterfaceCategory');
|
|
return {
|
|
[LANGUAGE_SETTING_ID]: {
|
|
name: t('mainLanguageSetting'),
|
|
category: ['Anomalous Model Browser', category, 'language'],
|
|
tooltip: t('mainLanguageTooltip'),
|
|
options: [
|
|
{ value: 'auto', text: t('mainLanguageAuto') },
|
|
{ value: 'zh', text: t('mainLanguageChinese') },
|
|
{ value: 'en', text: t('mainLanguageEnglish') }
|
|
]
|
|
},
|
|
[THEME_SETTING_ID]: {
|
|
name: t('settingsTheme'),
|
|
category: ['Anomalous Model Browser', category, 'theme'],
|
|
tooltip: t('settingsThemeHelp'),
|
|
options: THEMES.map(value => ({ value, text: t(THEME_LABELS[value]) }))
|
|
}
|
|
};
|
|
}
|
|
|
|
function refreshRegisteredSettings() {
|
|
const settingsApi = app.extensionManager?.setting;
|
|
const registry = settingsApi?.settings?.value || settingsApi?.settings;
|
|
if (!registry || typeof registry !== 'object') return;
|
|
for (const [id, patch] of Object.entries(getSettingTranslationPatches())) {
|
|
if (registry[id]) registry[id] = { ...registry[id], ...patch };
|
|
}
|
|
}
|
|
|
|
/** 'auto' | 'zh' | 'en': the ComfyUI setting's change handler, also used without the settings API. */
|
|
export function applyLanguagePreference(value) {
|
|
const preference = normalizeLanguagePreference(value);
|
|
if (preference === 'auto') localStorage.removeItem('anomalous_lang');
|
|
else localStorage.setItem('anomalous_lang', preference);
|
|
|
|
const nextLanguage = preference === 'auto' ? resolveComfyLanguage() : preference;
|
|
const changed = nextLanguage !== window.anomalous_browser_lang;
|
|
currentLang = nextLanguage;
|
|
window.anomalous_browser_lang = nextLanguage;
|
|
refreshRegisteredSettings();
|
|
if (changed) {
|
|
window.dispatchEvent(new CustomEvent('anomalous-language-change', {
|
|
detail: { language: nextLanguage, preference }
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* "Follow ComfyUI" (no language chosen here): takes up a ComfyUI language changed since the
|
|
* page loaded. Called when the browser opens, since ComfyUI switches its language without a reload.
|
|
*/
|
|
export function followComfyLanguage() {
|
|
if (!localStorage.getItem('anomalous_lang')) applyLanguagePreference('auto');
|
|
}
|
|
|
|
if (!localStorage.getItem('anomalous_lang')) {
|
|
currentLang = resolveComfyLanguage();
|
|
window.anomalous_browser_lang = currentLang;
|
|
}
|
|
|
|
function showThemeNoticeToast(isEnabled) {
|
|
document.getElementById('anomalous-theme-toast')?.remove();
|
|
const toast = document.createElement('div');
|
|
toast.id = 'anomalous-theme-toast';
|
|
toast.className = 'anomalous-theme-toast' + (isEnabled ? ' is-abyssal' : '');
|
|
toast.textContent = isEnabled ? t('themeDomainActivated') : t('themeDomainDeactivated');
|
|
document.body.appendChild(toast);
|
|
requestAnimationFrame(() => toast.classList.add('is-show'));
|
|
setTimeout(() => {
|
|
toast.classList.remove('is-show');
|
|
setTimeout(() => toast.remove(), 400);
|
|
}, 2400);
|
|
}
|
|
|
|
/*
|
|
* The theme: 'auto' follows ComfyUI's palette (ComfyUI marks a dark one with `dark-theme` on
|
|
* <html>, a light one without), 'dark', 'light', or 'abyssal' (Abyssal Scarlet, a dark palette).
|
|
* Shown through classes on <html>: `amb-theme-light` and `theme-abyssal-scarlet`; the colors are
|
|
* the --amb-* tokens of 00-foundation-models.css.
|
|
*/
|
|
const THEME_KEY = 'anomalous_theme';
|
|
const THEMES = ['auto', 'dark', 'light', 'abyssal'];
|
|
const THEME_LABELS = { auto: 'settingsThemeAuto', dark: 'settingsThemeDark', light: 'settingsThemeLight', abyssal: 'settingsThemeAbyssal' };
|
|
|
|
/** The theme chosen ('auto' unless one was picked; an older Abyssal Scarlet switch reads as 'abyssal'). */
|
|
export function themePreference() {
|
|
try {
|
|
const saved = localStorage.getItem(THEME_KEY);
|
|
if (THEMES.includes(saved)) return saved;
|
|
return localStorage.getItem('anomalous_theme_abyssal_scarlet') === 'true' ? 'abyssal' : 'auto';
|
|
} catch (_) {
|
|
return 'auto';
|
|
}
|
|
}
|
|
|
|
/** What is shown: 'dark', 'light' or 'abyssal'. */
|
|
export function effectiveTheme(preference = themePreference()) {
|
|
if (preference !== 'auto') return preference;
|
|
return document.documentElement.classList.contains('dark-theme') ? 'dark' : 'light';
|
|
}
|
|
|
|
let shownTheme = null;
|
|
|
|
function paintTheme() {
|
|
const theme = effectiveTheme();
|
|
const abyssal = theme === 'abyssal';
|
|
document.documentElement.classList.toggle('theme-abyssal-scarlet', abyssal);
|
|
document.documentElement.classList.toggle('amb-theme-light', theme === 'light');
|
|
document.getElementById('anomalous-modal')?.classList.toggle('theme-abyssal-scarlet', abyssal);
|
|
document.getElementById('anomalous-container')?.classList.toggle('theme-abyssal-scarlet', abyssal);
|
|
if (theme === shownTheme) return;
|
|
shownTheme = theme;
|
|
window.dispatchEvent(new CustomEvent('anomalous-theme-change', {
|
|
detail: { theme: abyssal ? 'abyssal-scarlet' : theme, enabled: abyssal }
|
|
}));
|
|
}
|
|
|
|
/** Picks the theme (Settings, ComfyUI's settings); `notify`: the toast when Abyssal Scarlet comes or goes. */
|
|
export function setThemePreference(value, notify = false) {
|
|
const preference = THEMES.includes(value) ? value : 'auto';
|
|
const wasAbyssal = effectiveTheme() === 'abyssal';
|
|
try {
|
|
localStorage.setItem(THEME_KEY, preference);
|
|
localStorage.removeItem('anomalous_theme_abyssal_scarlet');
|
|
} catch (_) {}
|
|
paintTheme();
|
|
try {
|
|
const settings = app.extensionManager?.setting;
|
|
if (settings && typeof settings.set === 'function' && settings.get(THEME_SETTING_ID) !== preference) {
|
|
settings.set(THEME_SETTING_ID, preference);
|
|
}
|
|
} catch (_) {}
|
|
const isAbyssal = preference === 'abyssal';
|
|
if (notify && isAbyssal !== wasAbyssal) showThemeNoticeToast(isAbyssal);
|
|
}
|
|
|
|
/** The logo's easter egg: Abyssal Scarlet on, or back to following ComfyUI. */
|
|
export function setAbyssalScarletTheme(enabled, notify = false) {
|
|
setThemePreference(enabled ? 'abyssal' : 'auto', notify);
|
|
}
|
|
|
|
window.setAbyssalScarletTheme = setAbyssalScarletTheme;
|
|
paintTheme();
|
|
// Following ComfyUI: its palette switch changes <html>'s classes (ours are toggled only when they
|
|
// differ, so painting causes no further change).
|
|
new MutationObserver(() => {
|
|
if (themePreference() === 'auto') paintTheme();
|
|
}).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
|
|
export function createInterfaceSettings() {
|
|
const translations = getSettingTranslationPatches();
|
|
return [
|
|
{
|
|
id: LANGUAGE_SETTING_ID,
|
|
...translations[LANGUAGE_SETTING_ID],
|
|
type: 'combo',
|
|
defaultValue: () => normalizeLanguagePreference(localStorage.getItem('anomalous_lang')),
|
|
onChange: applyLanguagePreference
|
|
},
|
|
{
|
|
id: THEME_SETTING_ID,
|
|
...translations[THEME_SETTING_ID],
|
|
type: 'combo',
|
|
defaultValue: () => themePreference(),
|
|
onChange(value) {
|
|
setThemePreference(value, true);
|
|
}
|
|
}
|
|
];
|
|
}
|