feat: system theme option + fix toast timer overlap
Lint & Test / test (push) Successful in 1m27s

Add third theme mode (system) that follows OS prefers-color-scheme.
Theme button cycles dark → light → system with monitor icon.
Listens for OS preference changes in real time when in system mode.

Fix showToast clearing previous timer so rapid calls don't cause
the toast to disappear early.
This commit is contained in:
2026-03-30 13:55:38 +03:00
parent 4b7a8d75f4
commit db5008aaeb
5 changed files with 174 additions and 149 deletions
@@ -115,12 +115,16 @@ export function closeLightbox(event?: Event) {
unlockBody(); unlockBody();
} }
let _toastTimer: ReturnType<typeof setTimeout> | null = null;
export function showToast(message: string, type = 'info') { export function showToast(message: string, type = 'info') {
if (_toastTimer) clearTimeout(_toastTimer);
const toast = document.getElementById('toast')!; const toast = document.getElementById('toast')!;
toast.textContent = message; toast.textContent = message;
toast.className = `toast ${type} show`; toast.className = `toast ${type} show`;
setTimeout(() => { _toastTimer = setTimeout(() => {
toast.className = 'toast'; toast.className = 'toast';
_toastTimer = null;
}, 3000); }, 3000);
} }
@@ -1765,6 +1765,7 @@
"stream.error.clone_pp_failed": "Failed to clone postprocessing template", "stream.error.clone_pp_failed": "Failed to clone postprocessing template",
"theme.switched.dark": "Switched to dark theme", "theme.switched.dark": "Switched to dark theme",
"theme.switched.light": "Switched to light theme", "theme.switched.light": "Switched to light theme",
"theme.switched.system": "Switched to system theme",
"accent.color.updated": "Accent color updated", "accent.color.updated": "Accent color updated",
"search.footer": "↑↓ navigate · Enter select · Esc close", "search.footer": "↑↓ navigate · Enter select · Esc close",
"sync_clock.group.title": "Sync Clocks", "sync_clock.group.title": "Sync Clocks",
@@ -743,71 +743,68 @@
"automations.name.placeholder": "Моя автоматизация", "automations.name.placeholder": "Моя автоматизация",
"automations.enabled": "Включена:", "automations.enabled": "Включена:",
"automations.enabled.hint": "Отключённые автоматизации не активируются даже при выполнении условий", "automations.enabled.hint": "Отключённые автоматизации не активируются даже при выполнении условий",
"automations.condition_logic": "Логика условий:", "automations.rule_logic": "Логика условий:",
"automations.condition_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)", "automations.rule_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)",
"automations.condition_logic.or": "Любое условие (ИЛИ)", "automations.rule_logic.or": "Любое условие (ИЛИ)",
"automations.condition_logic.and": "Все условия (И)", "automations.rule_logic.and": "Все условия (И)",
"automations.condition_logic.or.desc": "Срабатывает при любом совпадении", "automations.rule_logic.or.desc": "Срабатывает при любом совпадении",
"automations.condition_logic.and.desc": "Срабатывает только при всех", "automations.rule_logic.and.desc": "Срабатывает только при всех",
"automations.conditions": "Условия:", "automations.rules": "Условия:",
"automations.conditions.hint": "Правила, определяющие когда автоматизация активируется", "automations.rules.hint": "Правила, определяющие когда автоматизация активируется",
"automations.conditions.add": "Добавить условие", "automations.rules.add": "Добавить условие",
"automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена", "automations.rules.empty": "Нет условий — автоматизация всегда активна когда включена",
"automations.condition.always": "Всегда", "automations.rule.startup": "Автозапуск",
"automations.condition.always.desc": "Всегда активно", "automations.rule.startup.desc": "При запуске сервера",
"automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной.", "automations.rule.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.",
"automations.condition.startup": "Автозапуск", "automations.rule.application": "Приложение",
"automations.condition.startup.desc": "При запуске сервера", "automations.rule.application.desc": "Приложение запущено",
"automations.condition.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.", "automations.rule.application.apps": "Приложения:",
"automations.condition.application": "Приложение", "automations.rule.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
"automations.condition.application.desc": "Приложение запущено", "automations.rule.application.browse": "Обзор",
"automations.condition.application.apps": "Приложения:", "automations.rule.application.search": "Фильтр процессов...",
"automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)", "automations.rule.application.no_processes": "Процессы не найдены",
"automations.condition.application.browse": "Обзор", "automations.rule.application.match_type": "Тип соответствия:",
"automations.condition.application.search": "Фильтр процессов...", "automations.rule.application.match_type.hint": "Как определять наличие приложения",
"automations.condition.application.no_processes": "Процессы не найдены", "automations.rule.application.match_type.running": "Запущено",
"automations.condition.application.match_type": "Тип соответствия:", "automations.rule.application.match_type.running.desc": "Процесс активен",
"automations.condition.application.match_type.hint": "Как определять наличие приложения", "automations.rule.application.match_type.topmost": "На переднем плане",
"automations.condition.application.match_type.running": "Запущено", "automations.rule.application.match_type.topmost.desc": "Окно в фокусе",
"automations.condition.application.match_type.running.desc": "Процесс активен", "automations.rule.application.match_type.topmost_fullscreen": "Передний план + ПЭ",
"automations.condition.application.match_type.topmost": "На переднем плане", "automations.rule.application.match_type.topmost_fullscreen.desc": "В фокусе + полный экран",
"automations.condition.application.match_type.topmost.desc": "Окно в фокусе", "automations.rule.application.match_type.fullscreen": "Полный экран",
"automations.condition.application.match_type.topmost_fullscreen": "Передний план + ПЭ", "automations.rule.application.match_type.fullscreen.desc": "Любое полноэкранное",
"automations.condition.application.match_type.topmost_fullscreen.desc": "В фокусе + полный экран", "automations.rule.time_of_day": "Время суток",
"automations.condition.application.match_type.fullscreen": "Полный экран", "automations.rule.time_of_day.desc": "Диапазон времени",
"automations.condition.application.match_type.fullscreen.desc": "Любое полноэкранное", "automations.rule.time_of_day.start_time": "Время начала:",
"automations.condition.time_of_day": "Время суток", "automations.rule.time_of_day.end_time": "Время окончания:",
"automations.condition.time_of_day.desc": "Диапазон времени", "automations.rule.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:00–06:00) укажите время начала позже времени окончания.",
"automations.condition.time_of_day.start_time": "Время начала:", "automations.rule.system_idle": "Бездействие системы",
"automations.condition.time_of_day.end_time": "Время окончания:", "automations.rule.system_idle.desc": "Бездействие/активность",
"automations.condition.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:00–06:00) укажите время начала позже времени окончания.", "automations.rule.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):",
"automations.condition.system_idle": "Бездействие системы", "automations.rule.system_idle.mode": "Режим срабатывания:",
"automations.condition.system_idle.desc": "Бездействие/активность", "automations.rule.system_idle.when_idle": "При бездействии",
"automations.condition.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):", "automations.rule.system_idle.when_active": "При активности",
"automations.condition.system_idle.mode": "Режим срабатывания:", "automations.rule.display_state": "Состояние дисплея",
"automations.condition.system_idle.when_idle": "При бездействии", "automations.rule.display_state.desc": "Монитор вкл/выкл",
"automations.condition.system_idle.when_active": "При активности", "automations.rule.display_state.state": "Состояние монитора:",
"automations.condition.display_state": "Состояние дисплея", "automations.rule.display_state.on": "Включён",
"automations.condition.display_state.desc": "Монитор вкл/выкл", "automations.rule.display_state.off": "Выключен (спящий режим)",
"automations.condition.display_state.state": "Состояние монитора:", "automations.rule.mqtt": "MQTT",
"automations.condition.display_state.on": "Включён", "automations.rule.mqtt.desc": "MQTT сообщение",
"automations.condition.display_state.off": "Выключен (спящий режим)", "automations.rule.mqtt.topic": "Топик:",
"automations.condition.mqtt": "MQTT", "automations.rule.mqtt.payload": "Значение:",
"automations.condition.mqtt.desc": "MQTT сообщение", "automations.rule.mqtt.match_mode": "Режим сравнения:",
"automations.condition.mqtt.topic": "Топик:", "automations.rule.mqtt.match_mode.exact": "Точное совпадение",
"automations.condition.mqtt.payload": "Значение:", "automations.rule.mqtt.match_mode.contains": "Содержит",
"automations.condition.mqtt.match_mode": "Режим сравнения:", "automations.rule.mqtt.match_mode.regex": "Регулярное выражение",
"automations.condition.mqtt.match_mode.exact": "Точное совпадение", "automations.rule.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику",
"automations.condition.mqtt.match_mode.contains": "Содержит", "automations.rule.webhook": "Вебхук",
"automations.condition.mqtt.match_mode.regex": "Регулярное выражение", "automations.rule.webhook.desc": "HTTP вызов",
"automations.condition.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику", "automations.rule.webhook.hint": "Активировать через HTTP-запрос от внешних сервисов (Home Assistant, IFTTT, curl и т.д.)",
"automations.condition.webhook": "Вебхук", "automations.rule.webhook.url": "URL вебхука:",
"automations.condition.webhook.desc": "HTTP вызов", "automations.rule.webhook.copy": "Скопировать",
"automations.condition.webhook.hint": "Активировать через HTTP-запрос от внешних сервисов (Home Assistant, IFTTT, curl и т.д.)", "automations.rule.webhook.copied": "Скопировано!",
"automations.condition.webhook.url": "URL вебхука:", "automations.rule.webhook.save_first": "Сначала сохраните автоматизацию для генерации URL вебхука",
"automations.condition.webhook.copy": "Скопировать",
"automations.condition.webhook.copied": "Скопировано!",
"automations.condition.webhook.save_first": "Сначала сохраните автоматизацию для генерации URL вебхука",
"automations.scene": "Сцена:", "automations.scene": "Сцена:",
"automations.scene.hint": "Пресет сцены для активации при выполнении условий", "automations.scene.hint": "Пресет сцены для активации при выполнении условий",
"automations.scene.search_placeholder": "Поиск сцен...", "automations.scene.search_placeholder": "Поиск сцен...",
@@ -1626,6 +1623,7 @@
"stream.error.clone_pp_failed": "Не удалось клонировать шаблон постобработки", "stream.error.clone_pp_failed": "Не удалось клонировать шаблон постобработки",
"theme.switched.dark": "Переключено на тёмную тему", "theme.switched.dark": "Переключено на тёмную тему",
"theme.switched.light": "Переключено на светлую тему", "theme.switched.light": "Переключено на светлую тему",
"theme.switched.system": "Переключено на системную тему",
"accent.color.updated": "Цвет акцента обновлён", "accent.color.updated": "Цвет акцента обновлён",
"search.footer": "↑↓ навигация · Enter выбор · Esc закрыть", "search.footer": "↑↓ навигация · Enter выбор · Esc закрыть",
"sync_clock.group.title": "Часы синхронизации", "sync_clock.group.title": "Часы синхронизации",
@@ -743,71 +743,68 @@
"automations.name.placeholder": "我的自动化", "automations.name.placeholder": "我的自动化",
"automations.enabled": "启用:", "automations.enabled": "启用:",
"automations.enabled.hint": "禁用的自动化即使满足条件也不会激活", "automations.enabled.hint": "禁用的自动化即使满足条件也不会激活",
"automations.condition_logic": "条件逻辑:", "automations.rule_logic": "条件逻辑:",
"automations.condition_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)", "automations.rule_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)",
"automations.condition_logic.or": "任一条件(或)", "automations.rule_logic.or": "任一条件(或)",
"automations.condition_logic.and": "全部条件(与)", "automations.rule_logic.and": "全部条件(与)",
"automations.condition_logic.or.desc": "任一条件匹配时触发", "automations.rule_logic.or.desc": "任一条件匹配时触发",
"automations.condition_logic.and.desc": "全部匹配时才触发", "automations.rule_logic.and.desc": "全部匹配时才触发",
"automations.conditions": "条件:", "automations.rules": "条件:",
"automations.conditions.hint": "决定此自动化何时激活的规则", "automations.rules.hint": "决定此自动化何时激活的规则",
"automations.conditions.add": "添加条件", "automations.rules.add": "添加条件",
"automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态", "automations.rules.empty": "无条件 — 启用后自动化始终处于活动状态",
"automations.condition.always": "始终", "automations.rule.startup": "启动",
"automations.condition.always.desc": "始终活跃", "automations.rule.startup.desc": "服务器启动时",
"automations.condition.always.hint": "自动化启用后立即激活并保持活动。", "automations.rule.startup.hint": "服务器启动时激活,启用期间保持活动。",
"automations.condition.startup": "启动", "automations.rule.application": "应用程序",
"automations.condition.startup.desc": "服务器启动时", "automations.rule.application.desc": "应用运行/聚焦",
"automations.condition.startup.hint": "服务器启动时激活,启用期间保持活动。", "automations.rule.application.apps": "应用程序:",
"automations.condition.application": "应用程序", "automations.rule.application.apps.hint": "进程名,每行一个(例如 firefox.exe",
"automations.condition.application.desc": "应用运行/聚焦", "automations.rule.application.browse": "浏览",
"automations.condition.application.apps": "应用程序:", "automations.rule.application.search": "筛选进程...",
"automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe", "automations.rule.application.no_processes": "未找到进程",
"automations.condition.application.browse": "浏览", "automations.rule.application.match_type": "匹配类型:",
"automations.condition.application.search": "筛选进程...", "automations.rule.application.match_type.hint": "如何检测应用程序",
"automations.condition.application.no_processes": "未找到进程", "automations.rule.application.match_type.running": "运行中",
"automations.condition.application.match_type": "匹配类型:", "automations.rule.application.match_type.running.desc": "进程活跃",
"automations.condition.application.match_type.hint": "如何检测应用程序", "automations.rule.application.match_type.topmost": "最前",
"automations.condition.application.match_type.running": "运行中", "automations.rule.application.match_type.topmost.desc": "前台窗口",
"automations.condition.application.match_type.running.desc": "进程活跃", "automations.rule.application.match_type.topmost_fullscreen": "最前 + 全屏",
"automations.condition.application.match_type.topmost": "前", "automations.rule.application.match_type.topmost_fullscreen.desc": "前台 + 全屏",
"automations.condition.application.match_type.topmost.desc": "前台窗口", "automations.rule.application.match_type.fullscreen": "全屏",
"automations.condition.application.match_type.topmost_fullscreen": "最前 + 全屏", "automations.rule.application.match_type.fullscreen.desc": "任意全屏应用",
"automations.condition.application.match_type.topmost_fullscreen.desc": "前台 + 全屏", "automations.rule.time_of_day": "时段",
"automations.condition.application.match_type.fullscreen": "全屏", "automations.rule.time_of_day.desc": "时间范围",
"automations.condition.application.match_type.fullscreen.desc": "任意全屏应用", "automations.rule.time_of_day.start_time": "开始时间:",
"automations.condition.time_of_day": "时段", "automations.rule.time_of_day.end_time": "结束时间:",
"automations.condition.time_of_day.desc": "时间范围", "automations.rule.time_of_day.overnight_hint": "跨夜时段(如 22:00–06:00),请将开始时间设为晚于结束时间。",
"automations.condition.time_of_day.start_time": "开始时间:", "automations.rule.system_idle": "系统空闲",
"automations.condition.time_of_day.end_time": "结束时间:", "automations.rule.system_idle.desc": "空闲/活跃",
"automations.condition.time_of_day.overnight_hint": "跨夜时段(如 22:00–06:00),请将开始时间设为晚于结束时间。", "automations.rule.system_idle.idle_minutes": "空闲超时(分钟):",
"automations.condition.system_idle": "系统空闲", "automations.rule.system_idle.mode": "触发模式:",
"automations.condition.system_idle.desc": "空闲/活跃", "automations.rule.system_idle.when_idle": "空闲",
"automations.condition.system_idle.idle_minutes": "空闲超时(分钟):", "automations.rule.system_idle.when_active": "活跃时",
"automations.condition.system_idle.mode": "触发模式:", "automations.rule.display_state": "显示器状态",
"automations.condition.system_idle.when_idle": "空闲时", "automations.rule.display_state.desc": "显示器开/关",
"automations.condition.system_idle.when_active": "活跃时", "automations.rule.display_state.state": "显示器状态:",
"automations.condition.display_state": "显示器状态", "automations.rule.display_state.on": "开启",
"automations.condition.display_state.desc": "显示器开/关", "automations.rule.display_state.off": "关闭(休眠)",
"automations.condition.display_state.state": "显示器状态:", "automations.rule.mqtt": "MQTT",
"automations.condition.display_state.on": "开启", "automations.rule.mqtt.desc": "MQTT 消息",
"automations.condition.display_state.off": "关闭(休眠)", "automations.rule.mqtt.topic": "主题:",
"automations.condition.mqtt": "MQTT", "automations.rule.mqtt.payload": "消息内容:",
"automations.condition.mqtt.desc": "MQTT 消息", "automations.rule.mqtt.match_mode": "匹配模式:",
"automations.condition.mqtt.topic": "主题:", "automations.rule.mqtt.match_mode.exact": "精确匹配",
"automations.condition.mqtt.payload": "消息内容:", "automations.rule.mqtt.match_mode.contains": "包含",
"automations.condition.mqtt.match_mode": "匹配模式:", "automations.rule.mqtt.match_mode.regex": "正则表达式",
"automations.condition.mqtt.match_mode.exact": "精确匹配", "automations.rule.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活",
"automations.condition.mqtt.match_mode.contains": "包含", "automations.rule.webhook": "Webhook",
"automations.condition.mqtt.match_mode.regex": "正则表达式", "automations.rule.webhook.desc": "HTTP 回调",
"automations.condition.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活", "automations.rule.webhook.hint": "通过外部服务的 HTTP 请求激活(Home Assistant、IFTTT、curl 等)",
"automations.condition.webhook": "Webhook", "automations.rule.webhook.url": "Webhook URL",
"automations.condition.webhook.desc": "HTTP 回调", "automations.rule.webhook.copy": "复制",
"automations.condition.webhook.hint": "通过外部服务的 HTTP 请求激活(Home Assistant、IFTTT、curl 等)", "automations.rule.webhook.copied": "已复制!",
"automations.condition.webhook.url": "Webhook URL", "automations.rule.webhook.save_first": "请先保存自动化以生成 Webhook URL",
"automations.condition.webhook.copy": "复制",
"automations.condition.webhook.copied": "已复制!",
"automations.condition.webhook.save_first": "请先保存自动化以生成 Webhook URL",
"automations.scene": "场景:", "automations.scene": "场景:",
"automations.scene.hint": "条件满足时激活的场景预设", "automations.scene.hint": "条件满足时激活的场景预设",
"automations.scene.search_placeholder": "搜索场景...", "automations.scene.search_placeholder": "搜索场景...",
@@ -1626,6 +1623,7 @@
"stream.error.clone_pp_failed": "克隆后处理模板失败", "stream.error.clone_pp_failed": "克隆后处理模板失败",
"theme.switched.dark": "已切换到深色主题", "theme.switched.dark": "已切换到深色主题",
"theme.switched.light": "已切换到浅色主题", "theme.switched.light": "已切换到浅色主题",
"theme.switched.system": "已切换到系统主题",
"accent.color.updated": "强调色已更新", "accent.color.updated": "强调色已更新",
"search.footer": "↑↓ 导航 · Enter 选择 · Esc 关闭", "search.footer": "↑↓ 导航 · Enter 选择 · Esc 关闭",
"sync_clock.group.title": "同步时钟", "sync_clock.group.title": "同步时钟",
+42 -18
View File
@@ -279,30 +279,54 @@
if (btn) btn.style.opacity = state === 'on' ? '1' : '0.5'; if (btn) btn.style.opacity = state === 'on' ? '1' : '0.5';
} }
// Initialize theme // Initialize theme (preference can be 'dark', 'light', or 'system')
const savedTheme = localStorage.getItem('theme') || 'dark'; const _systemDarkMq = window.matchMedia('(prefers-color-scheme: dark)');
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
function updateThemeIcon(theme) { function _resolveTheme(pref) {
if (pref === 'system') return _systemDarkMq.matches ? 'dark' : 'light';
return pref;
}
function _applyTheme(resolved) {
document.documentElement.setAttribute('data-theme', resolved);
if (window._updateBgAnimTheme) window._updateBgAnimTheme(resolved === 'dark');
const accent = localStorage.getItem('accentColor');
if (accent) applyAccentColor(accent, true);
}
const _themePref = localStorage.getItem('theme') || 'dark';
_applyTheme(_resolveTheme(_themePref));
updateThemeIcon(_themePref);
// Listen for OS preference changes when in system mode
_systemDarkMq.addEventListener('change', function() {
if (localStorage.getItem('theme') === 'system') {
_applyTheme(_resolveTheme('system'));
}
});
function updateThemeIcon(pref) {
const icon = document.getElementById('theme-icon'); const icon = document.getElementById('theme-icon');
icon.innerHTML = theme === 'dark' if (pref === 'system') {
? '<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>' icon.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>';
: '<svg class="icon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>'; } else if (pref === 'dark') {
icon.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>';
} else {
icon.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>';
}
} }
function toggleTheme() { function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme'); const current = localStorage.getItem('theme') || 'dark';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; const order = ['dark', 'light', 'system'];
const next = order[(order.indexOf(current) + 1) % order.length];
const resolved = _resolveTheme(next);
document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', next);
localStorage.setItem('theme', newTheme); _applyTheme(resolved);
updateThemeIcon(newTheme); updateThemeIcon(next);
if (window._updateBgAnimTheme) window._updateBgAnimTheme(newTheme === 'dark'); const toastKeys = { dark: 'theme.switched.dark', light: 'theme.switched.light', system: 'theme.switched.system' };
// Re-derive accent text variant for the new theme showToast(window.t ? t(toastKeys[next]) : `Switched to ${next} theme`, 'info');
const accent = localStorage.getItem('accentColor');
if (accent) applyAccentColor(accent, true);
showToast(window.t ? t(newTheme === 'dark' ? 'theme.switched.dark' : 'theme.switched.light') : `Switched to ${newTheme} theme`, 'info');
} }
// Initialize accent color // Initialize accent color