diff --git a/server/src/wled_controller/static/js/core/ui.ts b/server/src/wled_controller/static/js/core/ui.ts index 216d52e..32dd740 100644 --- a/server/src/wled_controller/static/js/core/ui.ts +++ b/server/src/wled_controller/static/js/core/ui.ts @@ -115,12 +115,16 @@ export function closeLightbox(event?: Event) { unlockBody(); } +let _toastTimer: ReturnType | null = null; + export function showToast(message: string, type = 'info') { + if (_toastTimer) clearTimeout(_toastTimer); const toast = document.getElementById('toast')!; toast.textContent = message; toast.className = `toast ${type} show`; - setTimeout(() => { + _toastTimer = setTimeout(() => { toast.className = 'toast'; + _toastTimer = null; }, 3000); } diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 340aa23..405e077 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -1765,6 +1765,7 @@ "stream.error.clone_pp_failed": "Failed to clone postprocessing template", "theme.switched.dark": "Switched to dark theme", "theme.switched.light": "Switched to light theme", + "theme.switched.system": "Switched to system theme", "accent.color.updated": "Accent color updated", "search.footer": "↑↓ navigate · Enter select · Esc close", "sync_clock.group.title": "Sync Clocks", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 462f7be..254ddbf 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -743,71 +743,68 @@ "automations.name.placeholder": "Моя автоматизация", "automations.enabled": "Включена:", "automations.enabled.hint": "Отключённые автоматизации не активируются даже при выполнении условий", - "automations.condition_logic": "Логика условий:", - "automations.condition_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)", - "automations.condition_logic.or": "Любое условие (ИЛИ)", - "automations.condition_logic.and": "Все условия (И)", - "automations.condition_logic.or.desc": "Срабатывает при любом совпадении", - "automations.condition_logic.and.desc": "Срабатывает только при всех", - "automations.conditions": "Условия:", - "automations.conditions.hint": "Правила, определяющие когда автоматизация активируется", - "automations.conditions.add": "Добавить условие", - "automations.conditions.empty": "Нет условий — автоматизация всегда активна когда включена", - "automations.condition.always": "Всегда", - "automations.condition.always.desc": "Всегда активно", - "automations.condition.always.hint": "Автоматизация активируется сразу при включении и остаётся активной.", - "automations.condition.startup": "Автозапуск", - "automations.condition.startup.desc": "При запуске сервера", - "automations.condition.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.", - "automations.condition.application": "Приложение", - "automations.condition.application.desc": "Приложение запущено", - "automations.condition.application.apps": "Приложения:", - "automations.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)", - "automations.condition.application.browse": "Обзор", - "automations.condition.application.search": "Фильтр процессов...", - "automations.condition.application.no_processes": "Процессы не найдены", - "automations.condition.application.match_type": "Тип соответствия:", - "automations.condition.application.match_type.hint": "Как определять наличие приложения", - "automations.condition.application.match_type.running": "Запущено", - "automations.condition.application.match_type.running.desc": "Процесс активен", - "automations.condition.application.match_type.topmost": "На переднем плане", - "automations.condition.application.match_type.topmost.desc": "Окно в фокусе", - "automations.condition.application.match_type.topmost_fullscreen": "Передний план + ПЭ", - "automations.condition.application.match_type.topmost_fullscreen.desc": "В фокусе + полный экран", - "automations.condition.application.match_type.fullscreen": "Полный экран", - "automations.condition.application.match_type.fullscreen.desc": "Любое полноэкранное", - "automations.condition.time_of_day": "Время суток", - "automations.condition.time_of_day.desc": "Диапазон времени", - "automations.condition.time_of_day.start_time": "Время начала:", - "automations.condition.time_of_day.end_time": "Время окончания:", - "automations.condition.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:00–06:00) укажите время начала позже времени окончания.", - "automations.condition.system_idle": "Бездействие системы", - "automations.condition.system_idle.desc": "Бездействие/активность", - "automations.condition.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):", - "automations.condition.system_idle.mode": "Режим срабатывания:", - "automations.condition.system_idle.when_idle": "При бездействии", - "automations.condition.system_idle.when_active": "При активности", - "automations.condition.display_state": "Состояние дисплея", - "automations.condition.display_state.desc": "Монитор вкл/выкл", - "automations.condition.display_state.state": "Состояние монитора:", - "automations.condition.display_state.on": "Включён", - "automations.condition.display_state.off": "Выключен (спящий режим)", - "automations.condition.mqtt": "MQTT", - "automations.condition.mqtt.desc": "MQTT сообщение", - "automations.condition.mqtt.topic": "Топик:", - "automations.condition.mqtt.payload": "Значение:", - "automations.condition.mqtt.match_mode": "Режим сравнения:", - "automations.condition.mqtt.match_mode.exact": "Точное совпадение", - "automations.condition.mqtt.match_mode.contains": "Содержит", - "automations.condition.mqtt.match_mode.regex": "Регулярное выражение", - "automations.condition.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику", - "automations.condition.webhook": "Вебхук", - "automations.condition.webhook.desc": "HTTP вызов", - "automations.condition.webhook.hint": "Активировать через HTTP-запрос от внешних сервисов (Home Assistant, IFTTT, curl и т.д.)", - "automations.condition.webhook.url": "URL вебхука:", - "automations.condition.webhook.copy": "Скопировать", - "automations.condition.webhook.copied": "Скопировано!", - "automations.condition.webhook.save_first": "Сначала сохраните автоматизацию для генерации URL вебхука", + "automations.rule_logic": "Логика условий:", + "automations.rule_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)", + "automations.rule_logic.or": "Любое условие (ИЛИ)", + "automations.rule_logic.and": "Все условия (И)", + "automations.rule_logic.or.desc": "Срабатывает при любом совпадении", + "automations.rule_logic.and.desc": "Срабатывает только при всех", + "automations.rules": "Условия:", + "automations.rules.hint": "Правила, определяющие когда автоматизация активируется", + "automations.rules.add": "Добавить условие", + "automations.rules.empty": "Нет условий — автоматизация всегда активна когда включена", + "automations.rule.startup": "Автозапуск", + "automations.rule.startup.desc": "При запуске сервера", + "automations.rule.startup.hint": "Активируется при запуске сервера и остаётся активной пока включена.", + "automations.rule.application": "Приложение", + "automations.rule.application.desc": "Приложение запущено", + "automations.rule.application.apps": "Приложения:", + "automations.rule.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)", + "automations.rule.application.browse": "Обзор", + "automations.rule.application.search": "Фильтр процессов...", + "automations.rule.application.no_processes": "Процессы не найдены", + "automations.rule.application.match_type": "Тип соответствия:", + "automations.rule.application.match_type.hint": "Как определять наличие приложения", + "automations.rule.application.match_type.running": "Запущено", + "automations.rule.application.match_type.running.desc": "Процесс активен", + "automations.rule.application.match_type.topmost": "На переднем плане", + "automations.rule.application.match_type.topmost.desc": "Окно в фокусе", + "automations.rule.application.match_type.topmost_fullscreen": "Передний план + ПЭ", + "automations.rule.application.match_type.topmost_fullscreen.desc": "В фокусе + полный экран", + "automations.rule.application.match_type.fullscreen": "Полный экран", + "automations.rule.application.match_type.fullscreen.desc": "Любое полноэкранное", + "automations.rule.time_of_day": "Время суток", + "automations.rule.time_of_day.desc": "Диапазон времени", + "automations.rule.time_of_day.start_time": "Время начала:", + "automations.rule.time_of_day.end_time": "Время окончания:", + "automations.rule.time_of_day.overnight_hint": "Для ночных диапазонов (например 22:00–06:00) укажите время начала позже времени окончания.", + "automations.rule.system_idle": "Бездействие системы", + "automations.rule.system_idle.desc": "Бездействие/активность", + "automations.rule.system_idle.idle_minutes": "Тайм-аут бездействия (минуты):", + "automations.rule.system_idle.mode": "Режим срабатывания:", + "automations.rule.system_idle.when_idle": "При бездействии", + "automations.rule.system_idle.when_active": "При активности", + "automations.rule.display_state": "Состояние дисплея", + "automations.rule.display_state.desc": "Монитор вкл/выкл", + "automations.rule.display_state.state": "Состояние монитора:", + "automations.rule.display_state.on": "Включён", + "automations.rule.display_state.off": "Выключен (спящий режим)", + "automations.rule.mqtt": "MQTT", + "automations.rule.mqtt.desc": "MQTT сообщение", + "automations.rule.mqtt.topic": "Топик:", + "automations.rule.mqtt.payload": "Значение:", + "automations.rule.mqtt.match_mode": "Режим сравнения:", + "automations.rule.mqtt.match_mode.exact": "Точное совпадение", + "automations.rule.mqtt.match_mode.contains": "Содержит", + "automations.rule.mqtt.match_mode.regex": "Регулярное выражение", + "automations.rule.mqtt.hint": "Активировать при получении совпадающего значения по MQTT топику", + "automations.rule.webhook": "Вебхук", + "automations.rule.webhook.desc": "HTTP вызов", + "automations.rule.webhook.hint": "Активировать через HTTP-запрос от внешних сервисов (Home Assistant, IFTTT, curl и т.д.)", + "automations.rule.webhook.url": "URL вебхука:", + "automations.rule.webhook.copy": "Скопировать", + "automations.rule.webhook.copied": "Скопировано!", + "automations.rule.webhook.save_first": "Сначала сохраните автоматизацию для генерации URL вебхука", "automations.scene": "Сцена:", "automations.scene.hint": "Пресет сцены для активации при выполнении условий", "automations.scene.search_placeholder": "Поиск сцен...", @@ -1626,6 +1623,7 @@ "stream.error.clone_pp_failed": "Не удалось клонировать шаблон постобработки", "theme.switched.dark": "Переключено на тёмную тему", "theme.switched.light": "Переключено на светлую тему", + "theme.switched.system": "Переключено на системную тему", "accent.color.updated": "Цвет акцента обновлён", "search.footer": "↑↓ навигация · Enter выбор · Esc закрыть", "sync_clock.group.title": "Часы синхронизации", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 46229b1..35c7034 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -743,71 +743,68 @@ "automations.name.placeholder": "我的自动化", "automations.enabled": "启用:", "automations.enabled.hint": "禁用的自动化即使满足条件也不会激活", - "automations.condition_logic": "条件逻辑:", - "automations.condition_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)", - "automations.condition_logic.or": "任一条件(或)", - "automations.condition_logic.and": "全部条件(与)", - "automations.condition_logic.or.desc": "任一条件匹配时触发", - "automations.condition_logic.and.desc": "全部匹配时才触发", - "automations.conditions": "条件:", - "automations.conditions.hint": "决定此自动化何时激活的规则", - "automations.conditions.add": "添加条件", - "automations.conditions.empty": "无条件 — 启用后自动化始终处于活动状态", - "automations.condition.always": "始终", - "automations.condition.always.desc": "始终活跃", - "automations.condition.always.hint": "自动化启用后立即激活并保持活动。", - "automations.condition.startup": "启动", - "automations.condition.startup.desc": "服务器启动时", - "automations.condition.startup.hint": "服务器启动时激活,启用期间保持活动。", - "automations.condition.application": "应用程序", - "automations.condition.application.desc": "应用运行/聚焦", - "automations.condition.application.apps": "应用程序:", - "automations.condition.application.apps.hint": "进程名,每行一个(例如 firefox.exe)", - "automations.condition.application.browse": "浏览", - "automations.condition.application.search": "筛选进程...", - "automations.condition.application.no_processes": "未找到进程", - "automations.condition.application.match_type": "匹配类型:", - "automations.condition.application.match_type.hint": "如何检测应用程序", - "automations.condition.application.match_type.running": "运行中", - "automations.condition.application.match_type.running.desc": "进程活跃", - "automations.condition.application.match_type.topmost": "最前", - "automations.condition.application.match_type.topmost.desc": "前台窗口", - "automations.condition.application.match_type.topmost_fullscreen": "最前 + 全屏", - "automations.condition.application.match_type.topmost_fullscreen.desc": "前台 + 全屏", - "automations.condition.application.match_type.fullscreen": "全屏", - "automations.condition.application.match_type.fullscreen.desc": "任意全屏应用", - "automations.condition.time_of_day": "时段", - "automations.condition.time_of_day.desc": "时间范围", - "automations.condition.time_of_day.start_time": "开始时间:", - "automations.condition.time_of_day.end_time": "结束时间:", - "automations.condition.time_of_day.overnight_hint": "跨夜时段(如 22:00–06:00),请将开始时间设为晚于结束时间。", - "automations.condition.system_idle": "系统空闲", - "automations.condition.system_idle.desc": "空闲/活跃", - "automations.condition.system_idle.idle_minutes": "空闲超时(分钟):", - "automations.condition.system_idle.mode": "触发模式:", - "automations.condition.system_idle.when_idle": "空闲时", - "automations.condition.system_idle.when_active": "活跃时", - "automations.condition.display_state": "显示器状态", - "automations.condition.display_state.desc": "显示器开/关", - "automations.condition.display_state.state": "显示器状态:", - "automations.condition.display_state.on": "开启", - "automations.condition.display_state.off": "关闭(休眠)", - "automations.condition.mqtt": "MQTT", - "automations.condition.mqtt.desc": "MQTT 消息", - "automations.condition.mqtt.topic": "主题:", - "automations.condition.mqtt.payload": "消息内容:", - "automations.condition.mqtt.match_mode": "匹配模式:", - "automations.condition.mqtt.match_mode.exact": "精确匹配", - "automations.condition.mqtt.match_mode.contains": "包含", - "automations.condition.mqtt.match_mode.regex": "正则表达式", - "automations.condition.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活", - "automations.condition.webhook": "Webhook", - "automations.condition.webhook.desc": "HTTP 回调", - "automations.condition.webhook.hint": "通过外部服务的 HTTP 请求激活(Home Assistant、IFTTT、curl 等)", - "automations.condition.webhook.url": "Webhook URL:", - "automations.condition.webhook.copy": "复制", - "automations.condition.webhook.copied": "已复制!", - "automations.condition.webhook.save_first": "请先保存自动化以生成 Webhook URL", + "automations.rule_logic": "条件逻辑:", + "automations.rule_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)", + "automations.rule_logic.or": "任一条件(或)", + "automations.rule_logic.and": "全部条件(与)", + "automations.rule_logic.or.desc": "任一条件匹配时触发", + "automations.rule_logic.and.desc": "全部匹配时才触发", + "automations.rules": "条件:", + "automations.rules.hint": "决定此自动化何时激活的规则", + "automations.rules.add": "添加条件", + "automations.rules.empty": "无条件 — 启用后自动化始终处于活动状态", + "automations.rule.startup": "启动", + "automations.rule.startup.desc": "服务器启动时", + "automations.rule.startup.hint": "服务器启动时激活,启用期间保持活动。", + "automations.rule.application": "应用程序", + "automations.rule.application.desc": "应用运行/聚焦", + "automations.rule.application.apps": "应用程序:", + "automations.rule.application.apps.hint": "进程名,每行一个(例如 firefox.exe)", + "automations.rule.application.browse": "浏览", + "automations.rule.application.search": "筛选进程...", + "automations.rule.application.no_processes": "未找到进程", + "automations.rule.application.match_type": "匹配类型:", + "automations.rule.application.match_type.hint": "如何检测应用程序", + "automations.rule.application.match_type.running": "运行中", + "automations.rule.application.match_type.running.desc": "进程活跃", + "automations.rule.application.match_type.topmost": "最前", + "automations.rule.application.match_type.topmost.desc": "前台窗口", + "automations.rule.application.match_type.topmost_fullscreen": "最前 + 全屏", + "automations.rule.application.match_type.topmost_fullscreen.desc": "前台 + 全屏", + "automations.rule.application.match_type.fullscreen": "全屏", + "automations.rule.application.match_type.fullscreen.desc": "任意全屏应用", + "automations.rule.time_of_day": "时段", + "automations.rule.time_of_day.desc": "时间范围", + "automations.rule.time_of_day.start_time": "开始时间:", + "automations.rule.time_of_day.end_time": "结束时间:", + "automations.rule.time_of_day.overnight_hint": "跨夜时段(如 22:00–06:00),请将开始时间设为晚于结束时间。", + "automations.rule.system_idle": "系统空闲", + "automations.rule.system_idle.desc": "空闲/活跃", + "automations.rule.system_idle.idle_minutes": "空闲超时(分钟):", + "automations.rule.system_idle.mode": "触发模式:", + "automations.rule.system_idle.when_idle": "空闲时", + "automations.rule.system_idle.when_active": "活跃时", + "automations.rule.display_state": "显示器状态", + "automations.rule.display_state.desc": "显示器开/关", + "automations.rule.display_state.state": "显示器状态:", + "automations.rule.display_state.on": "开启", + "automations.rule.display_state.off": "关闭(休眠)", + "automations.rule.mqtt": "MQTT", + "automations.rule.mqtt.desc": "MQTT 消息", + "automations.rule.mqtt.topic": "主题:", + "automations.rule.mqtt.payload": "消息内容:", + "automations.rule.mqtt.match_mode": "匹配模式:", + "automations.rule.mqtt.match_mode.exact": "精确匹配", + "automations.rule.mqtt.match_mode.contains": "包含", + "automations.rule.mqtt.match_mode.regex": "正则表达式", + "automations.rule.mqtt.hint": "当 MQTT 主题收到匹配的消息时激活", + "automations.rule.webhook": "Webhook", + "automations.rule.webhook.desc": "HTTP 回调", + "automations.rule.webhook.hint": "通过外部服务的 HTTP 请求激活(Home Assistant、IFTTT、curl 等)", + "automations.rule.webhook.url": "Webhook URL:", + "automations.rule.webhook.copy": "复制", + "automations.rule.webhook.copied": "已复制!", + "automations.rule.webhook.save_first": "请先保存自动化以生成 Webhook URL", "automations.scene": "场景:", "automations.scene.hint": "条件满足时激活的场景预设", "automations.scene.search_placeholder": "搜索场景...", @@ -1626,6 +1623,7 @@ "stream.error.clone_pp_failed": "克隆后处理模板失败", "theme.switched.dark": "已切换到深色主题", "theme.switched.light": "已切换到浅色主题", + "theme.switched.system": "已切换到系统主题", "accent.color.updated": "强调色已更新", "search.footer": "↑↓ 导航 · Enter 选择 · Esc 关闭", "sync_clock.group.title": "同步时钟", diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html index 92d05bc..ed9be66 100644 --- a/server/src/wled_controller/templates/index.html +++ b/server/src/wled_controller/templates/index.html @@ -279,30 +279,54 @@ if (btn) btn.style.opacity = state === 'on' ? '1' : '0.5'; } - // Initialize theme - const savedTheme = localStorage.getItem('theme') || 'dark'; - document.documentElement.setAttribute('data-theme', savedTheme); - updateThemeIcon(savedTheme); + // Initialize theme (preference can be 'dark', 'light', or 'system') + const _systemDarkMq = window.matchMedia('(prefers-color-scheme: dark)'); - 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'); - icon.innerHTML = theme === 'dark' - ? '' - : ''; + if (pref === 'system') { + icon.innerHTML = ''; + } else if (pref === 'dark') { + icon.innerHTML = ''; + } else { + icon.innerHTML = ''; + } } function toggleTheme() { - const currentTheme = document.documentElement.getAttribute('data-theme'); - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + const current = localStorage.getItem('theme') || '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', newTheme); - updateThemeIcon(newTheme); - if (window._updateBgAnimTheme) window._updateBgAnimTheme(newTheme === 'dark'); - // Re-derive accent text variant for the new theme - 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'); + localStorage.setItem('theme', next); + _applyTheme(resolved); + updateThemeIcon(next); + const toastKeys = { dark: 'theme.switched.dark', light: 'theme.switched.light', system: 'theme.switched.system' }; + showToast(window.t ? t(toastKeys[next]) : `Switched to ${next} theme`, 'info'); } // Initialize accent color