diff --git a/server/src/wled_controller/static/css/base.css b/server/src/wled_controller/static/css/base.css index bceb6df..99e107c 100644 --- a/server/src/wled_controller/static/css/base.css +++ b/server/src/wled_controller/static/css/base.css @@ -6,9 +6,25 @@ :root { --primary-color: #4CAF50; + --primary-hover: #5cb860; --danger-color: #f44336; --warning-color: #ff9800; --info-color: #2196F3; + --font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'SF Mono', 'Consolas', 'Liberation Mono', monospace; +} + +/* ── SVG icon base ── */ +.icon { + display: inline-block; + width: 1em; + height: 1em; + vertical-align: -0.125em; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + flex-shrink: 0; } /* Dark theme (default) */ diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css index b58058e..e1b61b8 100644 --- a/server/src/wled_controller/static/css/cards.css +++ b/server/src/wled_controller/static/css/cards.css @@ -312,6 +312,10 @@ body.cs-drag-active .card-drag-handle { font-size: 0.6rem; } +.device-url-badge .icon { + color: var(--primary-text-color); +} + .card-subtitle { display: flex; align-items: center; @@ -328,6 +332,10 @@ body.cs-drag-active .card-drag-handle { gap: 4px; } +.card-meta .icon { + color: var(--primary-text-color); +} + .device-type-badge { font-size: 10px; font-weight: 700; @@ -647,7 +655,7 @@ ul.section-tip li { font-weight: 400; opacity: 0.45; line-height: 1.1; - color: #4CAF50; + color: var(--primary-color); } .fps-unreachable { diff --git a/server/src/wled_controller/static/css/dashboard.css b/server/src/wled_controller/static/css/dashboard.css index 75737e7..edc9f71 100644 --- a/server/src/wled_controller/static/css/dashboard.css +++ b/server/src/wled_controller/static/css/dashboard.css @@ -192,7 +192,7 @@ font-weight: 400; opacity: 0.45; line-height: 1.1; - color: #4CAF50; + color: var(--primary-color); } .dashboard-target-actions { @@ -258,7 +258,7 @@ border-radius: 10px; font-size: 0.7rem; font-weight: 600; - background: var(--success-color); + background: var(--primary-color); color: #fff; flex-shrink: 0; } diff --git a/server/src/wled_controller/static/css/layout.css b/server/src/wled_controller/static/css/layout.css index fc44a24..dd5afff 100644 --- a/server/src/wled_controller/static/css/layout.css +++ b/server/src/wled_controller/static/css/layout.css @@ -216,6 +216,81 @@ h2 { transform: scale(1.1); } +/* Accent color picker */ +.accent-wrapper { + position: relative; +} +.accent-swatch { + display: inline-block; + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid var(--border-color); + transition: border-color 0.2s, box-shadow 0.2s; +} +.search-toggle:hover .accent-swatch { + box-shadow: 0 0 6px var(--primary-color); +} +.accent-popover { + position: absolute; + top: calc(100% + 8px); + right: 0; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 10px; + box-shadow: 0 8px 24px var(--shadow-color); + z-index: 200; + animation: accent-pop-in 0.15s ease-out; +} +@keyframes accent-pop-in { + from { opacity: 0; transform: translateY(-4px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +.accent-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} +.accent-dot { + width: 32px; + height: 32px; + border-radius: 50%; + border: 3px solid transparent; + cursor: pointer; + transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s; + padding: 0; +} +.accent-dot:hover { + transform: scale(1.15); + box-shadow: 0 0 8px rgba(255,255,255,0.2); +} +.accent-dot.active { + border-color: var(--text-color); + box-shadow: 0 0 0 2px var(--card-bg), 0 0 0 4px var(--text-color); +} +.accent-custom { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-color); + font-size: 0.78rem; + color: var(--text-secondary); + cursor: pointer; +} +.accent-custom input[type="color"] { + width: 28px; + height: 28px; + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 2px; + cursor: pointer; + background: transparent; + flex-shrink: 0; +} + /* Footer */ .app-footer { margin-top: 12px; diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css index 9308666..4e8a694 100644 --- a/server/src/wled_controller/static/css/modal.css +++ b/server/src/wled_controller/static/css/modal.css @@ -477,8 +477,13 @@ .form-collapse > summary::-webkit-details-marker { display: none; } .form-collapse > summary::before { - content: '▶'; - font-size: 0.6rem; + content: ''; + display: inline-block; + width: 0; + height: 0; + border-left: 0.35em solid currentColor; + border-top: 0.25em solid transparent; + border-bottom: 0.25em solid transparent; opacity: 0.6; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); flex-shrink: 0; diff --git a/server/src/wled_controller/static/css/patterns.css b/server/src/wled_controller/static/css/patterns.css index 8784e92..2c64fae 100644 --- a/server/src/wled_controller/static/css/patterns.css +++ b/server/src/wled_controller/static/css/patterns.css @@ -54,6 +54,10 @@ vertical-align: middle; } +.stream-card-prop .icon { + color: var(--primary-text-color); +} + .stream-card-prop-full { max-width: 100%; word-break: break-all; @@ -72,6 +76,10 @@ color: #fff; } +.stream-card-link:hover .icon { + color: #fff; +} + @keyframes cardHighlight { 0%, 100% { box-shadow: none; } 25%, 75% { box-shadow: 0 0 0 3px var(--primary-color), 0 0 20px rgba(var(--primary-rgb, 59, 130, 246), 0.3); } diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index 9e025a1..8b3bc8c 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -538,7 +538,6 @@ display: flex; align-items: center; gap: 4px; - border-bottom: 2px solid var(--border-color); margin-bottom: 16px; } @@ -551,7 +550,6 @@ color: var(--text-secondary); cursor: pointer; border-bottom: 2px solid transparent; - margin-bottom: -2px; transition: color 0.2s ease, border-color 0.25s ease; } diff --git a/server/src/wled_controller/static/js/core/icon-paths.js b/server/src/wled_controller/static/js/core/icon-paths.js new file mode 100644 index 0000000..92dff65 --- /dev/null +++ b/server/src/wled_controller/static/js/core/icon-paths.js @@ -0,0 +1,71 @@ +/** + * Lucide icon SVG path data (https://lucide.dev) — MIT license. + * + * Each export is the inner SVG markup (paths, circles, rects, lines) + * for a 24×24 viewBox icon. These are consumed by icons.js via the + * _svg() wrapper which adds the outer tag with consistent attributes. + * + * To add a new icon: copy the inner elements from the Lucide source SVG + * and export as a single string constant here. + */ + +export const lightbulb = ''; +export const zap = ''; +export const palette = ''; +export const monitor = ''; +export const layoutDashboard = ''; +export const clipboardList = ''; +export const copy = ''; +export const tv = ''; +export const film = ''; +export const fileText = ''; +export const flaskConical = ''; +export const pencil = ''; +export const play = ''; +export const square = ''; +export const pause = ''; +export const settings = ''; +export const ruler = ''; +export const volume2 = ''; +export const mic = ''; +export const clock = ''; +export const triangleAlert = ''; +export const circleCheck = ''; +export const globe = ''; +export const eye = ''; +export const eyeOff = ''; +export const star = ''; +export const hash = ''; +export const camera = ''; +export const wrench = ''; +export const music = ''; +export const search = ''; +export const moon = ''; +export const sun = ''; +export const keyRound = ''; +export const logOut = ''; +export const rainbow = ''; +export const refreshCw = ''; +export const link = ''; +export const mapPin = ''; +export const plug = ''; +export const smartphone = ''; +export const rocket = ''; +export const image = ''; +export const target = ''; +export const trendingUp = ''; +export const activity = ''; +export const timer = ''; +export const moveVertical = ''; +export const cloudSun = ''; +export const sunDim = ''; +export const slidersHorizontal = ''; +export const circleHelp = ''; +export const radio = ''; +export const send = ''; +export const sparkles = ''; +export const fastForward = ''; +export const rotateCw = ''; +export const rotateCcw = ''; +export const download = ''; +export const undo2 = ''; diff --git a/server/src/wled_controller/static/js/core/icons.js b/server/src/wled_controller/static/js/core/icons.js index 21d866d..3e664f8 100644 --- a/server/src/wled_controller/static/js/core/icons.js +++ b/server/src/wled_controller/static/js/core/icons.js @@ -1,97 +1,146 @@ /** - * Centralized emoji icon maps and getter functions. + * Centralized SVG icon constants and getter functions. + * + * Uses Lucide icons (https://lucide.dev) — MIT-licensed, 24×24 stroke icons. + * SVG path data lives in icon-paths.js; this module wraps them with the + * tag and exports named constants for use across the app. * * Import icons from this module instead of using inline emoji literals. */ -// ── Type-resolution maps (private) ────────────────────────── +import * as P from './icon-paths.js'; -const _targetTypeIcons = { led: '\uD83D\uDCA1', wled: '\uD83D\uDCA1', key_colors: '\uD83C\uDFA8' }; -const _pictureSourceTypeIcons = { raw: '\uD83D\uDDA5\uFE0F', processed: '\uD83C\uDFA8', static_image: '\uD83D\uDDBC\uFE0F' }; -const _colorStripTypeIcons = { - static: '\uD83C\uDFA8', color_cycle: '\uD83D\uDD04', gradient: '\uD83C\uDF08', - effect: '\u26A1', composite: '\uD83D\uDD17', - mapped: '\uD83D\uDCCD', mapped_zones: '\uD83D\uDCCD', - audio: '\uD83C\uDFB5', audio_visualization: '\uD83C\uDFB5', - api_input: '\uD83D\uDCE1', +// ── SVG wrapper ──────────────────────────────────────────── +const _svg = (d) => `${d}`; + +// ── Type-resolution maps (private) ────────────────────────── +const _targetTypeIcons = { led: _svg(P.lightbulb), wled: _svg(P.lightbulb), key_colors: _svg(P.palette) }; +const _pictureSourceTypeIcons = { raw: _svg(P.monitor), processed: _svg(P.palette), static_image: _svg(P.image) }; +const _colorStripTypeIcons = { + static: _svg(P.palette), color_cycle: _svg(P.refreshCw), gradient: _svg(P.rainbow), + effect: _svg(P.zap), composite: _svg(P.link), + mapped: _svg(P.mapPin), mapped_zones: _svg(P.mapPin), + audio: _svg(P.music), audio_visualization: _svg(P.music), + api_input: _svg(P.send), }; -const _valueSourceTypeIcons = { - static: '\uD83D\uDCCA', animated: '\uD83D\uDD04', audio: '\uD83C\uDFB5', - adaptive_time: '\uD83D\uDD50', adaptive_scene: '\uD83C\uDF24\uFE0F', +const _valueSourceTypeIcons = { + static: _svg(P.layoutDashboard), animated: _svg(P.refreshCw), audio: _svg(P.music), + adaptive_time: _svg(P.clock), adaptive_scene: _svg(P.cloudSun), }; -const _audioSourceTypeIcons = { mono: '\uD83C\uDFA4', multichannel: '\uD83D\uDD0A' }; -const _engineTypeIcons = { scrcpy: '\uD83D\uDCF1' }; +const _audioSourceTypeIcons = { mono: _svg(P.mic), multichannel: _svg(P.volume2) }; +const _engineTypeIcons = { scrcpy: _svg(P.smartphone) }; // ── Type-resolution getters ───────────────────────────────── -/** Target type → emoji (fallback: ⚡) */ +/** Target type → icon (fallback: zap) */ export function getTargetTypeIcon(targetType) { - return _targetTypeIcons[targetType] || '\u26A1'; + return _targetTypeIcons[targetType] || _svg(P.zap); } -/** Picture source / stream type → emoji (fallback: 📺) */ +/** Picture source / stream type → icon (fallback: tv) */ export function getPictureSourceIcon(streamType) { - return _pictureSourceTypeIcons[streamType] || '\uD83D\uDCFA'; + return _pictureSourceTypeIcons[streamType] || _svg(P.tv); } -/** Color strip source type → emoji (fallback: 🎞️) */ +/** Color strip source type → icon (fallback: film) */ export function getColorStripIcon(sourceType) { - return _colorStripTypeIcons[sourceType] || '\uD83C\uDF9E\uFE0F'; + return _colorStripTypeIcons[sourceType] || _svg(P.film); } -/** Value source type → emoji (fallback: 🎚️) */ +/** Value source type → icon (fallback: sliders) */ export function getValueSourceIcon(sourceType) { - return _valueSourceTypeIcons[sourceType] || '\uD83C\uDF9A\uFE0F'; + return _valueSourceTypeIcons[sourceType] || _svg(P.slidersHorizontal); } -/** Audio source type → emoji (fallback: 🎵) */ +/** Audio source type → icon (fallback: music) */ export function getAudioSourceIcon(sourceType) { - return _audioSourceTypeIcons[sourceType] || '\uD83C\uDFB5'; + return _audioSourceTypeIcons[sourceType] || _svg(P.music); } -/** Capture engine type → emoji (fallback: 🚀) */ +/** Capture engine type → icon (fallback: rocket) */ export function getEngineIcon(engineType) { - return _engineTypeIcons[engineType] || '\uD83D\uDE80'; + return _engineTypeIcons[engineType] || _svg(P.rocket); } // ── Entity-kind constants ─────────────────────────────────── -export const ICON_PROFILE = '\uD83D\uDCCB'; // 📋 -export const ICON_DEVICE = '\uD83D\uDDA5\uFE0F'; // 🖥️ -export const ICON_TARGET = '\u26A1'; // ⚡ -export const ICON_VALUE_SOURCE = '\uD83D\uDD22'; // 🔢 +export const ICON_PROFILE = _svg(P.clipboardList); +export const ICON_DEVICE = _svg(P.monitor); +export const ICON_TARGET = _svg(P.zap); +export const ICON_VALUE_SOURCE = _svg(P.hash); // ── Template-kind constants ───────────────────────────────── -export const ICON_TEMPLATE = '\uD83D\uDCCB'; // 📋 (generic card header) -export const ICON_CAPTURE_TEMPLATE = '\uD83D\uDCF7'; // 📷 -export const ICON_PP_TEMPLATE = '\uD83D\uDD27'; // 🔧 -export const ICON_PATTERN_TEMPLATE = '\uD83D\uDCC4'; // 📄 -export const ICON_AUDIO_TEMPLATE = '\uD83C\uDFB5'; // 🎵 +export const ICON_TEMPLATE = _svg(P.clipboardList); +export const ICON_CAPTURE_TEMPLATE = _svg(P.camera); +export const ICON_PP_TEMPLATE = _svg(P.wrench); +export const ICON_PATTERN_TEMPLATE = _svg(P.fileText); +export const ICON_AUDIO_TEMPLATE = _svg(P.music); // ── Action constants ──────────────────────────────────────── -export const ICON_CLONE = '\uD83D\uDCCB'; // 📋 -export const ICON_EDIT = '\u270F\uFE0F'; // ✏️ -export const ICON_TEST = '\uD83E\uDDEA'; // 🧪 -export const ICON_START = '\u25B6\uFE0F'; // ▶️ -export const ICON_STOP = '\u23F9\uFE0F'; // ⏹️ -export const ICON_STOP_PLAIN = '\u23F9'; // ⏹ -export const ICON_PAUSE = '\u23F8'; // ⏸ -export const ICON_SETTINGS = '\u2699\uFE0F'; // ⚙️ -export const ICON_CALIBRATION = '\uD83D\uDCD0'; // 📐 +export const ICON_CLONE = _svg(P.copy); +export const ICON_EDIT = _svg(P.pencil); +export const ICON_TEST = _svg(P.flaskConical); +export const ICON_START = _svg(P.play); +export const ICON_STOP = _svg(P.square); +export const ICON_STOP_PLAIN = _svg(P.square); +export const ICON_PAUSE = _svg(P.pause); +export const ICON_SETTINGS = _svg(P.settings); +export const ICON_CALIBRATION = _svg(P.ruler); // ── Misc badge constants ──────────────────────────────────── -export const ICON_AUDIO_LOOPBACK = '\uD83D\uDD0A'; // 🔊 -export const ICON_AUDIO_INPUT = '\uD83C\uDFA4'; // 🎤 -export const ICON_CLOCK = '\uD83D\uDD50'; // 🕐 -export const ICON_WARNING = '\u26A0\uFE0F'; // ⚠️ -export const ICON_OK = '\u2705'; // ✅ -export const ICON_LINK_SOURCE = '\uD83D\uDCFA'; // 📺 -export const ICON_LED = '\uD83D\uDCA1'; // 💡 -export const ICON_FPS = '\u26A1'; // ⚡ -export const ICON_WEB = '\uD83C\uDF10'; // 🌐 -export const ICON_OVERLAY = '\uD83D\uDC41\uFE0F'; // 👁️ -export const ICON_LED_PREVIEW = '\uD83D\uDCCA'; // 📊 -export const ICON_AUTOSTART = '\u2B50'; // ⭐ +export const ICON_AUDIO_LOOPBACK = _svg(P.volume2); +export const ICON_AUDIO_INPUT = _svg(P.mic); +export const ICON_CLOCK = _svg(P.clock); +export const ICON_WARNING = _svg(P.triangleAlert); +export const ICON_OK = _svg(P.circleCheck); +export const ICON_LINK_SOURCE = _svg(P.tv); +export const ICON_LED = _svg(P.lightbulb); +export const ICON_FPS = _svg(P.zap); +export const ICON_WEB = _svg(P.globe); +export const ICON_OVERLAY = _svg(P.eye); +export const ICON_LED_PREVIEW = _svg(P.layoutDashboard); +export const ICON_AUTOSTART = _svg(P.star); + +// ── UI / header / modal icons ─────────────────────────────── + +export const ICON_SEARCH = _svg(P.search); +export const ICON_MOON = _svg(P.moon); +export const ICON_SUN = _svg(P.sun); +export const ICON_KEY = _svg(P.keyRound); +export const ICON_LOGOUT = _svg(P.logOut); +export const ICON_EYE = _svg(P.eye); +export const ICON_EYE_OFF = _svg(P.eyeOff); +export const ICON_HELP = _svg(P.circleHelp); +export const ICON_DASHBOARD = _svg(P.layoutDashboard); + +// ── Card badge icons ──────────────────────────────────────── + +export const ICON_PALETTE = _svg(P.palette); +export const ICON_RAINBOW = _svg(P.rainbow); +export const ICON_REFRESH = _svg(P.refreshCw); +export const ICON_LINK = _svg(P.link); +export const ICON_MAP_PIN = _svg(P.mapPin); +export const ICON_MUSIC = _svg(P.music); +export const ICON_TIMER = _svg(P.timer); +export const ICON_MONITOR = _svg(P.monitor); +export const ICON_GLOBE = _svg(P.globe); +export const ICON_RADIO = _svg(P.radio); +export const ICON_PLUG = _svg(P.plug); +export const ICON_FILM = _svg(P.film); +export const ICON_FILE_TEXT = _svg(P.fileText); +export const ICON_TARGET_ICON = _svg(P.target); +export const ICON_TRENDING_UP = _svg(P.trendingUp); +export const ICON_ACTIVITY = _svg(P.activity); +export const ICON_MOVE_VERTICAL = _svg(P.moveVertical); +export const ICON_SUN_DIM = _svg(P.sunDim); +export const ICON_CAMERA = _svg(P.camera); +export const ICON_WRENCH = _svg(P.wrench); +export const ICON_SPARKLES = _svg(P.sparkles); +export const ICON_FAST_FORWARD = _svg(P.fastForward); +export const ICON_ROTATE_CW = _svg(P.rotateCw); +export const ICON_ROTATE_CCW = _svg(P.rotateCcw); +export const ICON_DOWNLOAD = _svg(P.download); +export const ICON_UNDO = _svg(P.undo2); diff --git a/server/src/wled_controller/static/js/core/ui.js b/server/src/wled_controller/static/js/core/ui.js index 81431a7..20e8822 100644 --- a/server/src/wled_controller/static/js/core/ui.js +++ b/server/src/wled_controller/static/js/core/ui.js @@ -4,6 +4,7 @@ import { kcTestAutoRefresh, setKcTestAutoRefresh, setKcTestTargetId, confirmResolve, setConfirmResolve } from './state.js'; import { t } from './i18n.js'; +import { ICON_PAUSE, ICON_START } from './icons.js'; export function toggleHint(btn) { const hint = btn.closest('.label-row').nextElementSibling; @@ -125,10 +126,10 @@ export function updateAutoRefreshButton(active) { if (!btn) return; if (active) { btn.classList.add('active'); - btn.innerHTML = '⏸'; + btn.innerHTML = ICON_PAUSE; } else { btn.classList.remove('active'); - btn.innerHTML = '▶'; + btn.innerHTML = ICON_START; } } diff --git a/server/src/wled_controller/static/js/features/audio-sources.js b/server/src/wled_controller/static/js/features/audio-sources.js index 3de8ca6..54ea845 100644 --- a/server/src/wled_controller/static/js/features/audio-sources.js +++ b/server/src/wled_controller/static/js/features/audio-sources.js @@ -15,6 +15,7 @@ import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; import { showToast, showConfirm, lockBody, unlockBody } from '../core/ui.js'; import { Modal } from '../core/modal.js'; +import { ICON_MUSIC } from '../core/icons.js'; import { loadPictureSources } from './streams.js'; class AudioSourceModal extends Modal { @@ -43,7 +44,7 @@ export async function showAudioSourceModal(sourceType, editData) { ? (editData.source_type === 'mono' ? 'audio_source.edit.mono' : 'audio_source.edit.multichannel') : (sourceType === 'mono' ? 'audio_source.add.mono' : 'audio_source.add.multichannel'); - document.getElementById('audio-source-modal-title').textContent = t(titleKey); + document.getElementById('audio-source-modal-title').innerHTML = `${ICON_MUSIC} ${t(titleKey)}`; document.getElementById('audio-source-id').value = isEdit ? editData.id : ''; document.getElementById('audio-source-error').style.display = 'none'; @@ -199,7 +200,7 @@ async function _loadAudioDevices() { const data = await resp.json(); const devices = data.devices || []; select.innerHTML = devices.map(d => { - const label = d.is_loopback ? `🔊 ${d.name}` : `🎤 ${d.name}`; + const label = d.name; const val = `${d.index}:${d.is_loopback ? '1' : '0'}`; return ``; }).join(''); diff --git a/server/src/wled_controller/static/js/features/calibration.js b/server/src/wled_controller/static/js/features/calibration.js index d2880f2..1936333 100644 --- a/server/src/wled_controller/static/js/features/calibration.js +++ b/server/src/wled_controller/static/js/features/calibration.js @@ -10,6 +10,7 @@ import { showToast } from '../core/ui.js'; import { Modal } from '../core/modal.js'; import { closeTutorial, startCalibrationTutorial } from './tutorials.js'; import { startCSSOverlay, stopCSSOverlay } from './color-strips.js'; +import { ICON_WARNING, ICON_ROTATE_CW, ICON_ROTATE_CCW } from '../core/icons.js'; /* ── CalibrationModal subclass ────────────────────────────────── */ @@ -371,7 +372,7 @@ export function updateCalibrationPreview() { const mismatch = inCSS ? (declaredCount > 0 && total > declaredCount) : (total !== declaredCount); - document.getElementById('cal-total-leds-inline').textContent = (mismatch ? '\u26A0 ' : '') + total; + document.getElementById('cal-total-leds-inline').innerHTML = (mismatch ? ICON_WARNING + ' ' : '') + total; if (totalEl) totalEl.classList.toggle('mismatch', mismatch); const startPos = document.getElementById('cal-start-position').value; @@ -386,7 +387,7 @@ export function updateCalibrationPreview() { const direction = document.getElementById('cal-layout').value; const dirIcon = document.getElementById('direction-icon'); const dirLabel = document.getElementById('direction-label'); - if (dirIcon) dirIcon.textContent = direction === 'clockwise' ? '↻' : '↺'; + if (dirIcon) dirIcon.innerHTML = direction === 'clockwise' ? ICON_ROTATE_CW : ICON_ROTATE_CCW; if (dirLabel) dirLabel.textContent = direction === 'clockwise' ? 'CW' : 'CCW'; const deviceId = document.getElementById('calibration-device-id').value; diff --git a/server/src/wled_controller/static/js/features/color-strips.js b/server/src/wled_controller/static/js/features/color-strips.js index 1a429b6..8bf69de 100644 --- a/server/src/wled_controller/static/js/features/color-strips.js +++ b/server/src/wled_controller/static/js/features/color-strips.js @@ -9,6 +9,9 @@ import { Modal } from '../core/modal.js'; import { getColorStripIcon, getPictureSourceIcon, ICON_CLONE, ICON_EDIT, ICON_CALIBRATION, + ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC, + ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM, + ICON_LINK, ICON_SPARKLES, ICON_FAST_FORWARD, ICON_ACTIVITY, } from '../core/icons.js'; class CSSEditorModal extends Modal { @@ -587,8 +590,8 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { const anim = (isStatic || isGradient) && source.animation && source.animation.enabled ? source.animation : null; const animBadge = anim - ? `✨ ${t('color_strip.animation.type.' + anim.type) || anim.type}` - + `⏩ ${(anim.speed || 1.0).toFixed(1)}×` + ? `${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}` + + `${ICON_FAST_FORWARD} ${(anim.speed || 1.0).toFixed(1)}×` : ''; let propsHtml; @@ -598,7 +601,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { ${hexColor.toUpperCase()} - ${source.led_count ? `💡 ${source.led_count}` : ''} + ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} ${animBadge} `; } else if (isColorCycle) { @@ -608,8 +611,8 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { ).join(''); propsHtml = ` ${swatches} - ⏩ ${(source.cycle_speed || 1.0).toFixed(1)}× - ${source.led_count ? `💡 ${source.led_count}` : ''} + ${ICON_FAST_FORWARD} ${(source.cycle_speed || 1.0).toFixed(1)}× + ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} `; } else if (isGradient) { const stops = source.stops || []; @@ -629,31 +632,31 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { } propsHtml = ` ${cssGradient ? `` : ''} - 🎨 ${stops.length} ${t('color_strip.gradient.stops_count')} - ${source.led_count ? `💡 ${source.led_count}` : ''} + ${ICON_PALETTE} ${stops.length} ${t('color_strip.gradient.stops_count')} + ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} ${animBadge} `; } else if (isEffect) { const effectLabel = t('color_strip.effect.' + (source.effect_type || 'fire')) || source.effect_type || 'fire'; const paletteLabel = source.palette ? (t('color_strip.palette.' + source.palette) || source.palette) : ''; propsHtml = ` - ⚡ ${escapeHtml(effectLabel)} - ${paletteLabel ? `🎨 ${escapeHtml(paletteLabel)}` : ''} - ⏩ ${(source.speed || 1.0).toFixed(1)}× - ${source.led_count ? `💡 ${source.led_count}` : ''} + ${ICON_FPS} ${escapeHtml(effectLabel)} + ${paletteLabel ? `${ICON_PALETTE} ${escapeHtml(paletteLabel)}` : ''} + ${ICON_FAST_FORWARD} ${(source.speed || 1.0).toFixed(1)}× + ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} `; } else if (isComposite) { const layerCount = (source.layers || []).length; const enabledCount = (source.layers || []).filter(l => l.enabled !== false).length; propsHtml = ` - 🔗 ${enabledCount}/${layerCount} ${t('color_strip.composite.layers_count')} - ${source.led_count ? `💡 ${source.led_count}` : ''} + ${ICON_LINK} ${enabledCount}/${layerCount} ${t('color_strip.composite.layers_count')} + ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} `; } else if (isMapped) { const zoneCount = (source.zones || []).length; propsHtml = ` - 📍 ${zoneCount} ${t('color_strip.mapped.zones_count')} - ${source.led_count ? `💡 ${source.led_count}` : ''} + ${ICON_MAP_PIN} ${zoneCount} ${t('color_strip.mapped.zones_count')} + ${source.led_count ? `${ICON_LED} ${source.led_count}` : ''} `; } else if (isAudio) { const vizLabel = t('color_strip.audio.viz.' + (source.visualization_mode || 'spectrum')) || source.visualization_mode || 'spectrum'; @@ -662,14 +665,14 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { const showPalette = (vizMode === 'spectrum' || vizMode === 'beat_pulse') && source.palette; const audioPaletteLabel = showPalette ? (t('color_strip.palette.' + source.palette) || source.palette) : ''; propsHtml = ` - 🎵 ${escapeHtml(vizLabel)} - ${audioPaletteLabel ? `🎨 ${escapeHtml(audioPaletteLabel)}` : ''} - 📶 ${sensitivityVal} + ${ICON_MUSIC} ${escapeHtml(vizLabel)} + ${audioPaletteLabel ? `${ICON_PALETTE} ${escapeHtml(audioPaletteLabel)}` : ''} + ${ICON_ACTIVITY} ${sensitivityVal} ${source.audio_source_id ? (() => { const as = audioSourceMap && audioSourceMap[source.audio_source_id]; const asName = as ? as.name : source.audio_source_id; const asSection = as && as.source_type === 'mono' ? 'audio-mono' : 'audio-multi'; - return `🔊 ${escapeHtml(asName)}`; + return `${ICON_AUDIO_LOOPBACK} ${escapeHtml(asName)}`; })() : ''} ${source.mirror ? `🪞` : ''} `; @@ -680,7 +683,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { ${fbColor.toUpperCase()} - ⏱️ ${timeoutVal}s + ${ICON_TIMER} ${timeoutVal}s `; } else { const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id]; @@ -694,8 +697,8 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) { else if (ps.stream_type === 'processed') { psSubTab = 'processed'; psSection = 'proc-streams'; } } propsHtml = ` - 📺 ${escapeHtml(srcName)} - ${ledCount ? `💡 ${ledCount}` : ''} + ${ICON_LINK_SOURCE} ${escapeHtml(srcName)} + ${ledCount ? `${ICON_LED} ${ledCount}` : ''} `; } @@ -845,12 +848,12 @@ export async function showCSSEditor(cssId = null, cloneData = null) { } await _populateFromCSS(css); - document.getElementById('css-editor-title').textContent = t('color_strip.edit'); + document.getElementById('css-editor-title').innerHTML = `${ICON_FILM} ${t('color_strip.edit')}`; } else if (cloneData) { document.getElementById('css-editor-id').value = ''; document.getElementById('css-editor-name').value = (cloneData.name || '') + ' (Copy)'; await _populateFromCSS(cloneData); - document.getElementById('css-editor-title').textContent = t('color_strip.add'); + document.getElementById('css-editor-title').innerHTML = `${ICON_FILM} ${t('color_strip.add')}`; } else { document.getElementById('css-editor-id').value = ''; document.getElementById('css-editor-name').value = ''; @@ -887,7 +890,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) { document.getElementById('css-editor-api-input-timeout').value = 5.0; document.getElementById('css-editor-api-input-timeout-val').textContent = '5.0'; _showApiInputEndpoints(null); - document.getElementById('css-editor-title').textContent = t('color_strip.add'); + document.getElementById('css-editor-title').innerHTML = `${ICON_FILM} ${t('color_strip.add')}`; document.getElementById('css-editor-gradient-preset').value = ''; gradientInit([ { position: 0.0, color: [255, 0, 0] }, diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index f8fdfef..3f2aad6 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -11,7 +11,7 @@ import { startAutoRefresh, updateTabBadge } from './tabs.js'; import { getTargetTypeIcon, ICON_TARGET, ICON_PROFILE, ICON_CLOCK, ICON_WARNING, ICON_OK, - ICON_STOP, ICON_STOP_PLAIN, ICON_AUTOSTART, + ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_AUTOSTART, ICON_HELP, } from '../core/icons.js'; const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed'; @@ -64,7 +64,7 @@ function _startUptimeTimer() { if (!el) continue; const seconds = _getInterpolatedUptime(id); if (seconds != null) { - el.textContent = `${ICON_CLOCK} ${formatUptime(seconds)}`; + el.innerHTML = `${ICON_CLOCK} ${formatUptime(seconds)}`; } } }, 1000); @@ -218,7 +218,7 @@ function _updateRunningMetrics(enrichedRunning) { } const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`); - if (errorsEl) errorsEl.textContent = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`; + if (errorsEl) errorsEl.innerHTML = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`; // Update health dot — prefer streaming reachability when processing const isLed = target.target_type === 'led' || target.target_type === 'wled'; @@ -267,7 +267,7 @@ function _updateProfilesInPlace(profiles) { if (btn) { btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`; btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`); - btn.textContent = p.enabled ? ICON_STOP_PLAIN : '▶'; + btn.innerHTML = p.enabled ? ICON_STOP_PLAIN : ICON_START; } } } @@ -460,7 +460,7 @@ export async function loadDashboard(forceFullRender = false) {
`; @@ -518,7 +518,7 @@ export async function loadDashboard(forceFullRender = false) { // First load: build everything in one innerHTML to avoid flicker const isFirstLoad = !container.querySelector('.dashboard-perf-persistent'); const pollSelect = _renderPollIntervalSelect(); - const toolbar = `
${pollSelect}
`; + const toolbar = `
${pollSelect}
`; if (isFirstLoad) { container.innerHTML = `${toolbar}
${_sectionHeader('perf', t('dashboard.section.performance'), '')} @@ -639,7 +639,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
- +
`; } @@ -690,7 +690,7 @@ function renderDashboardProfile(profile) {
`; diff --git a/server/src/wled_controller/static/js/features/devices.js b/server/src/wled_controller/static/js/features/devices.js index e5864c5..35f316c 100644 --- a/server/src/wled_controller/static/js/features/devices.js +++ b/server/src/wled_controller/static/js/features/devices.js @@ -9,7 +9,7 @@ import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice, isMock import { t } from '../core/i18n.js'; import { showToast, showConfirm } from '../core/ui.js'; import { Modal } from '../core/modal.js'; -import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB } from '../core/icons.js'; +import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_PLUG } from '../core/icons.js'; class DeviceSettingsModal extends Modal { constructor() { super('device-settings-modal'); } @@ -90,7 +90,7 @@ export function createDeviceCard(device) {
${(device.device_type || 'wled').toUpperCase()} ${ledCount ? `${ICON_LED} ${ledCount}` : ''} - ${state.device_led_type ? `🔌 ${state.device_led_type.replace(/ RGBW$/, '')}` : ''} + ${state.device_led_type ? `${ICON_PLUG} ${state.device_led_type.replace(/ RGBW$/, '')}` : ''} ${state.device_rgbw ? '' : ''}
${(device.capabilities || []).includes('brightness_control') ? ` diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js index ec80106..c537a28 100644 --- a/server/src/wled_controller/static/js/features/kc-targets.js +++ b/server/src/wled_controller/static/js/features/kc-targets.js @@ -16,7 +16,8 @@ import { lockBody, showToast, showConfirm, formatUptime } from '../core/ui.js'; import { Modal } from '../core/modal.js'; import { getValueSourceIcon, getPictureSourceIcon, - ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, + ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, ICON_PAUSE, + ICON_LINK_SOURCE, ICON_PATTERN_TEMPLATE, ICON_FPS, ICON_PALETTE, } from '../core/icons.js'; class KCEditorModal extends Modal { @@ -126,14 +127,13 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
${escapeHtml(target.name)} - ${isProcessing ? `${t('targets.status.processing')}` : ''}
- 📺 ${escapeHtml(sourceName)} - 📄 ${escapeHtml(patternName)} + ${ICON_LINK_SOURCE} ${escapeHtml(sourceName)} + ${ICON_PATTERN_TEMPLATE} ${escapeHtml(patternName)} ▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''} - ⚡ ${kcSettings.fps ?? 10} + ${ICON_FPS} ${kcSettings.fps ?? 10} ${bvs ? `${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}` : ''}
@@ -289,10 +289,10 @@ export function updateAutoRefreshButton(active) { if (!btn) return; if (active) { btn.classList.add('active'); - btn.innerHTML = '⏸'; // pause symbol + btn.innerHTML = ICON_PAUSE; } else { btn.classList.remove('active'); - btn.innerHTML = '▶'; // play symbol + btn.innerHTML = ICON_START; } } @@ -404,7 +404,7 @@ function _populateKCBrightnessVsDropdown(selectedId = '') { const icon = getValueSourceIcon(vs.source_type); const opt = document.createElement('option'); opt.value = vs.id; - opt.textContent = `${icon} ${vs.name}`; + opt.textContent = vs.name; sel.appendChild(opt); }); sel.value = selectedId || ''; @@ -462,7 +462,7 @@ export async function showKCEditor(targetId = null, cloneData = null) { document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3; patSelect.value = kcSettings.pattern_template_id || ''; _populateKCBrightnessVsDropdown(kcSettings.brightness_value_source_id || ''); - document.getElementById('kc-editor-title').textContent = t('kc.edit'); + document.getElementById('kc-editor-title').innerHTML = `${ICON_PALETTE} ${t('kc.edit')}`; } else if (cloneData) { const kcSettings = cloneData.key_colors_settings || {}; document.getElementById('kc-editor-id').value = ''; @@ -475,7 +475,7 @@ export async function showKCEditor(targetId = null, cloneData = null) { document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3; patSelect.value = kcSettings.pattern_template_id || ''; _populateKCBrightnessVsDropdown(kcSettings.brightness_value_source_id || ''); - document.getElementById('kc-editor-title').textContent = t('kc.add'); + document.getElementById('kc-editor-title').innerHTML = `${ICON_PALETTE} ${t('kc.add')}`; } else { document.getElementById('kc-editor-id').value = ''; document.getElementById('kc-editor-name').value = ''; @@ -487,7 +487,7 @@ export async function showKCEditor(targetId = null, cloneData = null) { document.getElementById('kc-editor-smoothing-value').textContent = '0.3'; if (patTemplates.length > 0) patSelect.value = patTemplates[0].id; _populateKCBrightnessVsDropdown(''); - document.getElementById('kc-editor-title').textContent = t('kc.add'); + document.getElementById('kc-editor-title').innerHTML = `${ICON_PALETTE} ${t('kc.add')}`; } // Auto-name diff --git a/server/src/wled_controller/static/js/features/pattern-templates.js b/server/src/wled_controller/static/js/features/pattern-templates.js index e1e2c6a..e224805 100644 --- a/server/src/wled_controller/static/js/features/pattern-templates.js +++ b/server/src/wled_controller/static/js/features/pattern-templates.js @@ -97,19 +97,19 @@ export async function showPatternTemplateEditor(templateId = null, cloneData = n document.getElementById('pattern-template-id').value = tmpl.id; document.getElementById('pattern-template-name').value = tmpl.name; document.getElementById('pattern-template-description').value = tmpl.description || ''; - document.getElementById('pattern-template-modal-title').textContent = t('pattern.edit'); + document.getElementById('pattern-template-modal-title').innerHTML = `${ICON_PATTERN_TEMPLATE} ${t('pattern.edit')}`; setPatternEditorRects((tmpl.rectangles || []).map(r => ({ ...r }))); } else if (cloneData) { document.getElementById('pattern-template-id').value = ''; document.getElementById('pattern-template-name').value = (cloneData.name || '') + ' (Copy)'; document.getElementById('pattern-template-description').value = cloneData.description || ''; - document.getElementById('pattern-template-modal-title').textContent = t('pattern.add'); + document.getElementById('pattern-template-modal-title').innerHTML = `${ICON_PATTERN_TEMPLATE} ${t('pattern.add')}`; setPatternEditorRects((cloneData.rectangles || []).map(r => ({ ...r }))); } else { document.getElementById('pattern-template-id').value = ''; document.getElementById('pattern-template-name').value = ''; document.getElementById('pattern-template-description').value = ''; - document.getElementById('pattern-template-modal-title').textContent = t('pattern.add'); + document.getElementById('pattern-template-modal-title').innerHTML = `${ICON_PATTERN_TEMPLATE} ${t('pattern.add')}`; setPatternEditorRects([]); } diff --git a/server/src/wled_controller/static/js/features/profiles.js b/server/src/wled_controller/static/js/features/profiles.js index 9f16b58..fa642cd 100644 --- a/server/src/wled_controller/static/js/features/profiles.js +++ b/server/src/wled_controller/static/js/features/profiles.js @@ -9,7 +9,7 @@ import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js'; import { Modal } from '../core/modal.js'; import { CardSection } from '../core/card-sections.js'; import { updateTabBadge } from './tabs.js'; -import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE } from '../core/icons.js'; +import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_TARGET, ICON_PROFILE, ICON_HELP, ICON_OK } from '../core/icons.js'; class ProfileEditorModal extends Modal { constructor() { super('profile-editor-modal'); } @@ -88,7 +88,7 @@ function renderProfiles(profiles, runningTargetIds = new Set()) { const container = document.getElementById('profiles-content'); const items = csProfiles.applySortOrder(profiles.map(p => ({ key: p.id, html: createProfileCard(p, runningTargetIds) }))); - const toolbar = `
`; + const toolbar = `
`; container.innerHTML = toolbar + csProfiles.render(items); csProfiles.bind(); @@ -109,7 +109,7 @@ function createProfileCard(profile, runningTargetIds = new Set()) { } else { const parts = profile.conditions.map(c => { if (c.condition_type === 'always') { - return `✅ ${t('profiles.condition.always')}`; + return `${ICON_OK} ${t('profiles.condition.always')}`; } if (c.condition_type === 'application') { const apps = (c.apps || []).join(', '); @@ -127,7 +127,7 @@ function createProfileCard(profile, runningTargetIds = new Set()) { let lastActivityMeta = ''; if (profile.last_activated_at) { const ts = new Date(profile.last_activated_at); - lastActivityMeta = `🕐 ${ts.toLocaleString()}`; + lastActivityMeta = `${ICON_CLOCK} ${ts.toLocaleString()}`; } return ` @@ -143,7 +143,7 @@ function createProfileCard(profile, runningTargetIds = new Set()) {
${profile.condition_logic === 'and' ? t('profiles.logic.all') : t('profiles.logic.any')} - ⚡ ${targetCountText} + ${ICON_TARGET} ${targetCountText} ${lastActivityMeta}
${condPills}
@@ -158,7 +158,7 @@ function createProfileCard(profile, runningTargetIds = new Set()) { `; })() : ''} `; @@ -180,7 +180,7 @@ export async function openProfileEditor(profileId) { await loadProfileTargetChecklist([]); if (profileId) { - titleEl.textContent = t('profiles.edit'); + titleEl.innerHTML = `${ICON_PROFILE} ${t('profiles.edit')}`; try { const resp = await fetchWithAuth(`/profiles/${profileId}`); if (!resp.ok) throw new Error('Failed to load profile'); @@ -201,7 +201,7 @@ export async function openProfileEditor(profileId) { return; } } else { - titleEl.textContent = t('profiles.add'); + titleEl.innerHTML = `${ICON_PROFILE} ${t('profiles.add')}`; idInput.value = ''; nameInput.value = ''; enabledInput.checked = true; diff --git a/server/src/wled_controller/static/js/features/settings.js b/server/src/wled_controller/static/js/features/settings.js index 161b5eb..3e4b2fa 100644 --- a/server/src/wled_controller/static/js/features/settings.js +++ b/server/src/wled_controller/static/js/features/settings.js @@ -7,6 +7,7 @@ import { API_BASE, fetchWithAuth } from '../core/api.js'; import { Modal } from '../core/modal.js'; import { showToast, showConfirm } from '../core/ui.js'; import { t } from '../core/i18n.js'; +import { ICON_UNDO, ICON_DOWNLOAD } from '../core/icons.js'; // Simple modal (no form / no dirty check needed) const settingsModal = new Modal('settings-modal'); @@ -207,8 +208,8 @@ export async function loadBackupList() { ${date} ${sizeKB} KB - - + + `; }).join(''); diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index 341b2e2..4cfd10a 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -39,7 +39,8 @@ import { getEngineIcon, getPictureSourceIcon, getAudioSourceIcon, ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE, ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT, - ICON_AUDIO_TEMPLATE, + ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO, + ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_HELP, } from '../core/icons.js'; // ── Card section instances ── @@ -166,7 +167,7 @@ async function loadCaptureTemplates() { export async function showAddTemplateModal(cloneData = null) { setCurrentEditingTemplateId(null); - document.getElementById('template-modal-title').textContent = t('templates.add'); + document.getElementById('template-modal-title').innerHTML = `${ICON_CAPTURE_TEMPLATE} ${t('templates.add')}`; document.getElementById('template-form').reset(); document.getElementById('template-id').value = ''; document.getElementById('engine-config-section').style.display = 'none'; @@ -197,7 +198,7 @@ export async function editTemplate(templateId) { const template = await response.json(); setCurrentEditingTemplateId(templateId); - document.getElementById('template-modal-title').textContent = t('templates.edit'); + document.getElementById('template-modal-title').innerHTML = `${ICON_CAPTURE_TEMPLATE} ${t('templates.edit')}`; document.getElementById('template-id').value = templateId; document.getElementById('template-name').value = template.name; document.getElementById('template-description').value = template.description || ''; @@ -751,7 +752,7 @@ async function loadAudioTemplates() { export async function showAddAudioTemplateModal(cloneData = null) { setCurrentEditingAudioTemplateId(null); - document.getElementById('audio-template-modal-title').textContent = t('audio_template.add'); + document.getElementById('audio-template-modal-title').innerHTML = `${ICON_AUDIO_TEMPLATE} ${t('audio_template.add')}`; document.getElementById('audio-template-form').reset(); document.getElementById('audio-template-id').value = ''; document.getElementById('audio-engine-config-section').style.display = 'none'; @@ -781,7 +782,7 @@ export async function editAudioTemplate(templateId) { const template = await response.json(); setCurrentEditingAudioTemplateId(templateId); - document.getElementById('audio-template-modal-title').textContent = t('audio_template.edit'); + document.getElementById('audio-template-modal-title').innerHTML = `${ICON_AUDIO_TEMPLATE} ${t('audio_template.edit')}`; document.getElementById('audio-template-id').value = templateId; document.getElementById('audio-template-name').value = template.name; document.getElementById('audio-template-description').value = template.description || ''; @@ -900,7 +901,7 @@ export async function showTestAudioTemplateModal(templateId) { const data = await resp.json(); const devices = data.devices || []; deviceSelect.innerHTML = devices.map(d => { - const label = d.is_loopback ? `🔊 ${d.name}` : `🎤 ${d.name}`; + const label = d.name; const val = `${d.index}:${d.is_loopback ? '1' : '0'}`; return ``; }).join(''); @@ -1182,7 +1183,7 @@ function renderPictureSourcesList(streams) { if (capTmpl) capTmplName = escapeHtml(capTmpl.name); } detailsHtml = `
- 🖥️ ${stream.display_index ?? 0} + ${ICON_MONITOR} ${stream.display_index ?? 0} ${ICON_FPS} ${stream.target_fps ?? 30} ${capTmplName ? `${ICON_TEMPLATE} ${capTmplName}` : ''}
`; @@ -1236,7 +1237,7 @@ function renderPictureSourcesList(streams) { ${template.description ? `
${escapeHtml(template.description)}
` : ''}
${getEngineIcon(template.engine_type)} ${template.engine_type.toUpperCase()} - ${configEntries.length > 0 ? `🔧 ${configEntries.length}` : ''} + ${configEntries.length > 0 ? `${ICON_WRENCH} ${configEntries.length}` : ''}
${configEntries.length > 0 ? `
@@ -1301,7 +1302,7 @@ function renderPictureSourcesList(streams) { const tabBar = `
${tabs.map(tab => `` - ).join('')}
`; + ).join('')}`; const renderAudioSourceCard = (src) => { const isMono = src.source_type === 'mono'; @@ -1317,7 +1318,7 @@ function renderPictureSourcesList(streams) { : `${ICON_AUDIO_LOOPBACK} ${escapeHtml(parentName)}`; propsHtml = ` ${parentBadge} - 📻 ${chLabel} + ${ICON_RADIO} ${chLabel} `; } else { const devIdx = src.device_index ?? -1; @@ -1356,7 +1357,7 @@ function renderPictureSourcesList(streams) { ${template.description ? `
${escapeHtml(template.description)}
` : ''}
${ICON_AUDIO_TEMPLATE} ${template.engine_type.toUpperCase()} - ${configEntries.length > 0 ? `🔧 ${configEntries.length}` : ''} + ${configEntries.length > 0 ? `${ICON_WRENCH} ${configEntries.length}` : ''}
${configEntries.length > 0 ? `
@@ -1456,7 +1457,7 @@ function _autoGenerateStreamName() { export async function showAddStreamModal(presetType, cloneData = null) { const streamType = (cloneData && cloneData.stream_type) || presetType || 'raw'; const titleKeys = { raw: 'streams.add.raw', processed: 'streams.add.processed', static_image: 'streams.add.static_image' }; - document.getElementById('stream-modal-title').textContent = t(titleKeys[streamType] || 'streams.add'); + document.getElementById('stream-modal-title').innerHTML = `${getPictureSourceIcon(streamType)} ${t(titleKeys[streamType] || 'streams.add')}`; document.getElementById('stream-form').reset(); document.getElementById('stream-id').value = ''; document.getElementById('stream-display-index').value = ''; @@ -1513,7 +1514,7 @@ export async function editStream(streamId) { const stream = await response.json(); const editTitleKeys = { raw: 'streams.edit.raw', processed: 'streams.edit.processed', static_image: 'streams.edit.static_image' }; - document.getElementById('stream-modal-title').textContent = t(editTitleKeys[stream.stream_type] || 'streams.edit'); + document.getElementById('stream-modal-title').innerHTML = `${getPictureSourceIcon(stream.stream_type)} ${t(editTitleKeys[stream.stream_type] || 'streams.edit')}`; document.getElementById('stream-id').value = streamId; document.getElementById('stream-name').value = stream.name; document.getElementById('stream-description').value = stream.description || ''; @@ -2108,7 +2109,7 @@ function _autoGeneratePPTemplateName() { export async function showAddPPTemplateModal(cloneData = null) { if (_availableFilters.length === 0) await loadAvailableFilters(); - document.getElementById('pp-template-modal-title').textContent = t('postprocessing.add'); + document.getElementById('pp-template-modal-title').innerHTML = `${ICON_PP_TEMPLATE} ${t('postprocessing.add')}`; document.getElementById('pp-template-form').reset(); document.getElementById('pp-template-id').value = ''; document.getElementById('pp-template-error').style.display = 'none'; @@ -2146,7 +2147,7 @@ export async function editPPTemplate(templateId) { if (!response.ok) throw new Error(`Failed to load template: ${response.status}`); const tmpl = await response.json(); - document.getElementById('pp-template-modal-title').textContent = t('postprocessing.edit'); + document.getElementById('pp-template-modal-title').innerHTML = `${ICON_PP_TEMPLATE} ${t('postprocessing.edit')}`; document.getElementById('pp-template-id').value = templateId; document.getElementById('pp-template-name').value = tmpl.name; document.getElementById('pp-template-description').value = tmpl.description || ''; diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index 1d38dd0..5634e01 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -21,6 +21,7 @@ import { getValueSourceIcon, getTargetTypeIcon, ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP, ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW, + ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP, } from '../core/icons.js'; import { CardSection } from '../core/card-sections.js'; import { updateSubTabHash, updateTabBadge } from './tabs.js'; @@ -276,7 +277,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) { document.getElementById('target-editor-fps-value').textContent = fps; document.getElementById('target-editor-keepalive-interval').value = target.keepalive_interval ?? 1.0; document.getElementById('target-editor-keepalive-interval-value').textContent = target.keepalive_interval ?? 1.0; - document.getElementById('target-editor-title').textContent = t('targets.edit'); + document.getElementById('target-editor-title').innerHTML = `${ICON_TARGET_ICON} ${t('targets.edit')}`; const thresh = target.min_brightness_threshold ?? 0; document.getElementById('target-editor-brightness-threshold').value = thresh; @@ -297,7 +298,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) { document.getElementById('target-editor-fps-value').textContent = fps; document.getElementById('target-editor-keepalive-interval').value = cloneData.keepalive_interval ?? 1.0; document.getElementById('target-editor-keepalive-interval-value').textContent = cloneData.keepalive_interval ?? 1.0; - document.getElementById('target-editor-title').textContent = t('targets.add'); + document.getElementById('target-editor-title').innerHTML = `${ICON_TARGET_ICON} ${t('targets.add')}`; const cloneThresh = cloneData.min_brightness_threshold ?? 0; document.getElementById('target-editor-brightness-threshold').value = cloneThresh; @@ -316,7 +317,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) { document.getElementById('target-editor-fps-value').textContent = '30'; document.getElementById('target-editor-keepalive-interval').value = 1.0; document.getElementById('target-editor-keepalive-interval-value').textContent = '1.0'; - document.getElementById('target-editor-title').textContent = t('targets.add'); + document.getElementById('target-editor-title').innerHTML = `${ICON_TARGET_ICON} ${t('targets.add')}`; document.getElementById('target-editor-brightness-threshold').value = 0; document.getElementById('target-editor-brightness-threshold-value').textContent = '0'; @@ -576,7 +577,7 @@ export async function loadTargetsTab() { const tabBar = `
${subTabs.map(tab => `` - ).join('')}
`; + ).join('')}`; // Use window.createPatternTemplateCard to avoid circular import const createPatternTemplateCard = window.createPatternTemplateCard || (() => ''); @@ -867,16 +868,15 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
${escapeHtml(target.name)} - ${isProcessing ? `${t('device.status.processing')}` : ''}
${ICON_LED} ${escapeHtml(deviceName)} ${ICON_FPS} ${target.fps || 30} - ${device?.device_type === 'wled' || !device ? `${target.protocol === 'http' ? '🌐' : '📡'} ${(target.protocol || 'ddp').toUpperCase()}` : `🔌 ${t('targets.protocol.serial')}`} - 🎞️ ${cssSummary} + ${device?.device_type === 'wled' || !device ? `${target.protocol === 'http' ? ICON_GLOBE : ICON_RADIO} ${(target.protocol || 'ddp').toUpperCase()}` : `${ICON_PLUG} ${t('targets.protocol.serial')}`} + ${ICON_FILM} ${cssSummary} ${bvs ? `${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}` : ''} - ${target.min_brightness_threshold > 0 ? `🔅 <${target.min_brightness_threshold} → off` : ''} + ${target.min_brightness_threshold > 0 ? `${ICON_SUN_DIM} <${target.min_brightness_threshold} → off` : ''}
${isProcessing ? ` @@ -1123,7 +1123,7 @@ function connectLedPreviewWS(targetId) { if (bLabel) { const pct = Math.round(brightness / 255 * 100); if (pct < 100 || bLabel.dataset.hasBvs) { - bLabel.textContent = `☀ ${pct}%`; + bLabel.innerHTML = `${ICON_SUN_DIM} ${pct}%`; bLabel.style.display = ''; } else { bLabel.style.display = 'none'; diff --git a/server/src/wled_controller/static/js/features/value-sources.js b/server/src/wled_controller/static/js/features/value-sources.js index 674d4ec..f829336 100644 --- a/server/src/wled_controller/static/js/features/value-sources.js +++ b/server/src/wled_controller/static/js/features/value-sources.js @@ -15,7 +15,12 @@ import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; import { showToast, showConfirm } from '../core/ui.js'; import { Modal } from '../core/modal.js'; -import { getValueSourceIcon, ICON_CLONE, ICON_EDIT, ICON_TEST } from '../core/icons.js'; +import { + getValueSourceIcon, + ICON_CLONE, ICON_EDIT, ICON_TEST, + ICON_LED_PREVIEW, ICON_ACTIVITY, ICON_TIMER, ICON_MOVE_VERTICAL, + ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH, +} from '../core/icons.js'; import { loadPictureSources } from './streams.js'; export { getValueSourceIcon }; @@ -58,7 +63,8 @@ export async function showValueSourceModal(editData) { const isEdit = !!editData; const titleKey = isEdit ? 'value_source.edit' : 'value_source.add'; - document.getElementById('value-source-modal-title').textContent = t(titleKey); + const titleIcon = isEdit ? getValueSourceIcon(editData.source_type) : getValueSourceIcon('static'); + document.getElementById('value-source-modal-title').innerHTML = `${titleIcon} ${t(titleKey)}`; document.getElementById('value-source-id').value = isEdit ? editData.id : ''; document.getElementById('value-source-error').style.display = 'none'; @@ -471,13 +477,13 @@ export function createValueSourceCard(src) { let propsHtml = ''; if (src.source_type === 'static') { - propsHtml = `📊 ${t('value_source.type.static')}: ${src.value ?? 1.0}`; + propsHtml = `${ICON_LED_PREVIEW} ${t('value_source.type.static')}: ${src.value ?? 1.0}`; } else if (src.source_type === 'animated') { const waveLabel = src.waveform || 'sine'; propsHtml = ` - 〰️ ${escapeHtml(waveLabel)} - ⏱️ ${src.speed ?? 10} cpm - ↕️ ${src.min_value ?? 0}–${src.max_value ?? 1} + ${ICON_ACTIVITY} ${escapeHtml(waveLabel)} + ${ICON_TIMER} ${src.speed ?? 10} cpm + ${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}–${src.max_value ?? 1} `; } else if (src.source_type === 'audio') { const audioSrc = _cachedAudioSources.find(a => a.id === src.audio_source_id); @@ -485,18 +491,18 @@ export function createValueSourceCard(src) { const audioSection = audioSrc ? (audioSrc.source_type === 'mono' ? 'audio-mono' : 'audio-multi') : 'audio-multi'; const modeLabel = src.mode || 'rms'; const audioBadge = audioSrc - ? `🎵 ${escapeHtml(audioName)}` - : `🎵 ${escapeHtml(audioName)}`; + ? `${ICON_MUSIC} ${escapeHtml(audioName)}` + : `${ICON_MUSIC} ${escapeHtml(audioName)}`; propsHtml = ` ${audioBadge} - 📈 ${modeLabel.toUpperCase()} - ↕️ ${src.min_value ?? 0}–${src.max_value ?? 1} + ${ICON_TRENDING_UP} ${modeLabel.toUpperCase()} + ${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}–${src.max_value ?? 1} `; } else if (src.source_type === 'adaptive_time') { const pts = (src.schedule || []).length; propsHtml = ` - 📍 ${pts} ${t('value_source.schedule.points')} - ↕️ ${src.min_value ?? 0}–${src.max_value ?? 1} + ${ICON_MAP_PIN} ${pts} ${t('value_source.schedule.points')} + ${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}–${src.max_value ?? 1} `; } else if (src.source_type === 'adaptive_scene') { const ps = _cachedStreams.find(s => s.id === src.picture_source_id); @@ -507,11 +513,11 @@ export function createValueSourceCard(src) { else if (ps.stream_type === 'processed') { psSubTab = 'processed'; psSection = 'proc-streams'; } } const psBadge = ps - ? `🖥️ ${escapeHtml(psName)}` - : `🖥️ ${escapeHtml(psName)}`; + ? `${ICON_MONITOR} ${escapeHtml(psName)}` + : `${ICON_MONITOR} ${escapeHtml(psName)}`; propsHtml = ` ${psBadge} - 🔄 ${src.scene_behavior || 'complement'} + ${ICON_REFRESH} ${src.scene_behavior || 'complement'} `; } diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 9c65afc..99c7a11 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -3,6 +3,8 @@ "app.version": "Version:", "app.api_docs": "API Documentation", "theme.toggle": "Toggle theme", + "accent.title": "Accent color", + "accent.custom": "Custom", "locale.change": "Change language", "auth.login": "Login", "auth.logout": "Logout", @@ -21,7 +23,7 @@ "auth.please_login": "Please login to view", "auth.session_expired": "Your session has expired or the API key is invalid. Please login again.", "displays.title": "Available Displays", - "displays.layout": "\uD83D\uDDA5\uFE0F Displays", + "displays.layout": "Displays", "displays.information": "Display Information", "displays.legend.primary": "Primary Display", "displays.legend.secondary": "Secondary Display", @@ -44,7 +46,7 @@ "displays.picker.adb_connect.error": "Failed to connect device", "displays.picker.adb_disconnect": "Disconnect", "displays.picker.no_android": "No Android devices found. Connect via USB or enter IP above.", - "templates.title": "\uD83D\uDCC4 Engine Templates", + "templates.title": "Engine Templates", "templates.description": "Capture templates define how the screen is captured. Each template uses a specific capture engine (MSS, DXcam, WGC) with custom settings. Assign templates to devices for optimal performance.", "templates.loading": "Loading templates...", "templates.empty": "No capture templates configured", @@ -77,7 +79,7 @@ "templates.test.display.select": "Select display...", "templates.test.duration": "Capture Duration (s):", "templates.test.border_width": "Border Width (px):", - "templates.test.run": "\uD83E\uDDEA Run", + "templates.test.run": "Run", "templates.test.running": "Running test...", "templates.test.results.preview": "Full Capture Preview", "templates.test.results.borders": "Border Extraction", @@ -97,7 +99,7 @@ "templates.test.error.no_engine": "Please select a capture engine", "templates.test.error.no_display": "Please select a display", "templates.test.error.failed": "Test failed", - "devices.title": "\uD83D\uDCA1 Devices", + "devices.title": "Devices", "devices.add": "Add New Device", "devices.loading": "Loading devices...", "devices.none": "No devices configured", @@ -180,7 +182,7 @@ "device.tip.calibrate": "Calibrate LED positions, direction, and coverage", "device.tip.webui": "Open the device's built-in web interface for advanced configuration", "device.tip.add": "Click here to add a new LED device", - "settings.title": "Device Settings", + "settings.title": "Settings", "settings.general.title": "General Settings", "settings.capture.title": "Capture Settings", "settings.capture.saved": "Capture settings updated", @@ -285,12 +287,12 @@ "section.filter.reset": "Clear filter", "section.expand_all": "Expand all sections", "section.collapse_all": "Collapse all sections", - "streams.title": "\uD83D\uDCFA Sources", + "streams.title": "Sources", "streams.description": "Sources define the capture pipeline. A raw source captures from a display using a capture template. A processed source applies postprocessing to another source. Assign sources to devices.", "streams.group.raw": "Screen Capture", "streams.group.processed": "Processed", "streams.group.audio": "Audio", - "streams.section.streams": "\uD83D\uDCFA Sources", + "streams.section.streams": "Sources", "streams.add": "Add Source", "streams.add.raw": "Add Screen Capture", "streams.add.processed": "Add Processed Source", @@ -322,11 +324,11 @@ "streams.error.required": "Please fill in all required fields", "streams.error.delete": "Failed to delete source", "streams.test.title": "Test Source", - "streams.test.run": "🧪 Run", + "streams.test.run": "Run", "streams.test.running": "Testing source...", "streams.test.duration": "Capture Duration (s):", "streams.test.error.failed": "Source test failed", - "postprocessing.title": "\uD83D\uDCC4 Filter Templates", + "postprocessing.title": "Filter Templates", "postprocessing.description": "Processing templates define image filters and color correction. Assign them to processed picture sources for consistent postprocessing across devices.", "postprocessing.add": "Add Filter Template", "postprocessing.edit": "Edit Filter Template", @@ -363,7 +365,7 @@ "postprocessing.test.error.no_stream": "Please select a source", "postprocessing.test.error.failed": "Processing template test failed", "device.button.stream_selector": "Source Settings", - "device.stream_settings.title": "📺 Source Settings", + "device.stream_settings.title": "Source Settings", "device.stream_selector.label": "Source:", "device.stream_selector.hint": "Select a source that defines what this device captures and processes", "device.stream_selector.none": "-- No source assigned --", @@ -388,13 +390,13 @@ "streams.validate_image.validating": "Validating...", "streams.validate_image.valid": "Image accessible", "streams.validate_image.invalid": "Image not accessible", - "targets.title": "⚡ Targets", + "targets.title": "Targets", "targets.description": "Targets bridge color strip sources to output devices. Each target references a device and a color strip source.", "targets.subtab.wled": "LED", "targets.subtab.led": "LED", - "targets.section.devices": "💡 Devices", - "targets.section.color_strips": "🎞️ Color Strip Sources", - "targets.section.targets": "⚡ Targets", + "targets.section.devices": "Devices", + "targets.section.color_strips": "Color Strip Sources", + "targets.section.targets": "Targets", "targets.section.specific_settings": "Specific Settings", "targets.add": "Add Target", "targets.edit": "Edit Target", @@ -444,7 +446,7 @@ "targets.metrics.frames": "Frames", "targets.metrics.errors": "Errors", "targets.subtab.key_colors": "Key Colors", - "targets.section.key_colors": "🎨 Key Colors Targets", + "targets.section.key_colors": "Key Colors Targets", "kc.add": "Add Key Colors Target", "kc.edit": "Edit Key Colors Target", "kc.name": "Target Name:", @@ -476,9 +478,9 @@ "kc.colors.none": "No colors extracted yet", "kc.test": "Test", "kc.test.error": "Test failed", - "targets.section.pattern_templates": "📄 Pattern Templates", - "pattern.add": "📄 Add Pattern Template", - "pattern.edit": "📄 Edit Pattern Template", + "targets.section.pattern_templates": "Pattern Templates", + "pattern.add": "Add Pattern Template", + "pattern.edit": "Edit Pattern Template", "pattern.name": "Template Name:", "pattern.name.placeholder": "My Pattern Template", "pattern.description_label": "Description (optional):", @@ -513,7 +515,7 @@ "overlay.stopped": "Overlay visualization stopped", "overlay.error.start": "Failed to start overlay", "overlay.error.stop": "Failed to stop overlay", - "dashboard.title": "📊 Dashboard", + "dashboard.title": "Dashboard", "dashboard.section.targets": "Targets", "dashboard.section.running": "Running", "dashboard.section.stopped": "Stopped", @@ -532,10 +534,9 @@ "dashboard.perf.gpu": "GPU", "dashboard.perf.unavailable": "unavailable", "dashboard.poll_interval": "Refresh interval", - - "profiles.title": "\uD83D\uDCCB Profiles", + "profiles.title": "Profiles", "profiles.empty": "No profiles configured. Create one to automate target activation.", - "profiles.add": "\uD83D\uDCCB Add Profile", + "profiles.add": "Add Profile", "profiles.edit": "Edit Profile", "profiles.delete.confirm": "Delete profile \"{name}\"?", "profiles.name": "Name:", @@ -549,7 +550,7 @@ "profiles.conditions": "Conditions:", "profiles.conditions.hint": "Rules that determine when this profile activates", "profiles.conditions.add": "Add Condition", - "profiles.conditions.empty": "No conditions \u2014 profile is always active when enabled", + "profiles.conditions.empty": "No conditions — profile is always active when enabled", "profiles.condition.always": "Always", "profiles.condition.always.hint": "Profile activates immediately when enabled and stays active. Use this to auto-start targets on server startup.", "profiles.condition.application": "Application", @@ -597,9 +598,8 @@ "aria.previous": "Previous", "aria.next": "Next", "aria.hint": "Show hint", - - "color_strip.add": "🎞️ Add Color Strip Source", - "color_strip.edit": "🎞️ Edit Color Strip Source", + "color_strip.add": "Add Color Strip Source", + "color_strip.edit": "Edit Color Strip Source", "color_strip.name": "Name:", "color_strip.name.placeholder": "Wall Strip", "color_strip.picture_source": "Picture Source:", @@ -786,7 +786,6 @@ "color_strip.palette.aurora": "Aurora", "color_strip.palette.sunset": "Sunset", "color_strip.palette.ice": "Ice", - "audio_source.title": "Audio Sources", "audio_source.group.multichannel": "Multichannel", "audio_source.group.mono": "Mono", @@ -829,14 +828,12 @@ "audio_source.test.beat": "Beat", "audio_source.test.connecting": "Connecting...", "audio_source.test.error": "Audio test failed", - "audio_template.test": "Test", "audio_template.test.title": "Test Audio Template", "audio_template.test.device": "Audio Device:", "audio_template.test.device.hint": "Select which audio device to capture from during the test", - "audio_template.test.run": "🧪 Run", - - "audio_template.title": "🎵 Audio Templates", + "audio_template.test.run": "Run", + "audio_template.title": "Audio Templates", "audio_template.add": "Add Audio Template", "audio_template.edit": "Edit Audio Template", "audio_template.name": "Template Name:", @@ -857,9 +854,8 @@ "audio_template.error.engines": "Failed to load audio engines", "audio_template.error.required": "Please fill in all required fields", "audio_template.error.delete": "Failed to delete audio template", - "streams.group.value": "Value Sources", - "value_source.group.title": "🔢 Value Sources", + "value_source.group.title": "Value Sources", "value_source.add": "Add Value Source", "value_source.edit": "Edit Value Source", "value_source.name": "Name:", @@ -947,7 +943,6 @@ "targets.protocol": "Protocol:", "targets.protocol.hint": "DDP sends pixels via fast UDP (recommended for most setups). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.", "targets.protocol.serial": "Serial", - "search.open": "Search (Ctrl+K)", "search.placeholder": "Search entities... (Ctrl+K)", "search.loading": "Loading...", @@ -963,8 +958,6 @@ "search.group.pattern_templates": "Pattern Templates", "search.group.audio": "Audio Sources", "search.group.value": "Value Sources", - - "settings.title": "Settings", "settings.backup.label": "Backup Configuration", "settings.backup.hint": "Download all configuration (devices, targets, streams, templates, profiles) as a single JSON file.", "settings.backup.button": "Download Backup", @@ -979,7 +972,6 @@ "settings.restore.restarting": "Server is restarting...", "settings.restore.restart_timeout": "Server did not respond. Please refresh the page manually.", "settings.button.close": "Close", - "settings.auto_backup.label": "Auto-Backup", "settings.auto_backup.hint": "Automatically create periodic backups of all configuration. Old backups are pruned when the maximum count is reached.", "settings.auto_backup.enable": "Enable auto-backup", @@ -990,7 +982,6 @@ "settings.auto_backup.save_error": "Failed to save auto-backup settings", "settings.auto_backup.last_backup": "Last backup", "settings.auto_backup.never": "Never", - "settings.saved_backups.label": "Saved Backups", "settings.saved_backups.hint": "Auto-backup files stored on the server. Download to save locally, or delete to free space.", "settings.saved_backups.empty": "No saved backups", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index d4232c6..fed38ca 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -3,6 +3,8 @@ "app.version": "Версия:", "app.api_docs": "Документация API", "theme.toggle": "Переключить тему", + "accent.title": "Цвет акцента", + "accent.custom": "Свой", "locale.change": "Изменить язык", "auth.login": "Войти", "auth.logout": "Выйти", @@ -21,7 +23,7 @@ "auth.please_login": "Пожалуйста, войдите для просмотра", "auth.session_expired": "Ваша сессия истекла или API ключ недействителен. Пожалуйста, войдите снова.", "displays.title": "Доступные Дисплеи", - "displays.layout": "\uD83D\uDDA5\uFE0F Дисплеи", + "displays.layout": "Дисплеи", "displays.information": "Информация о Дисплеях", "displays.legend.primary": "Основной Дисплей", "displays.legend.secondary": "Вторичный Дисплей", @@ -44,7 +46,7 @@ "displays.picker.adb_connect.error": "Не удалось подключить устройство", "displays.picker.adb_disconnect": "Отключить", "displays.picker.no_android": "Android устройства не найдены. Подключите по USB или введите IP выше.", - "templates.title": "\uD83D\uDCC4 Шаблоны Движков", + "templates.title": "Шаблоны Движков", "templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.", "templates.loading": "Загрузка шаблонов...", "templates.empty": "Шаблоны захвата не настроены", @@ -77,7 +79,7 @@ "templates.test.display.select": "Выберите дисплей...", "templates.test.duration": "Длительность Захвата (с):", "templates.test.border_width": "Ширина Границы (px):", - "templates.test.run": "\uD83E\uDDEA Запустить", + "templates.test.run": "Запустить", "templates.test.running": "Выполняется тест...", "templates.test.results.preview": "Полный Предпросмотр Захвата", "templates.test.results.borders": "Извлечение Границ", @@ -97,7 +99,7 @@ "templates.test.error.no_engine": "Пожалуйста, выберите движок захвата", "templates.test.error.no_display": "Пожалуйста, выберите дисплей", "templates.test.error.failed": "Тест не удался", - "devices.title": "\uD83D\uDCA1 Устройства", + "devices.title": "Устройства", "devices.add": "Добавить Новое Устройство", "devices.loading": "Загрузка устройств...", "devices.none": "Устройства не настроены", @@ -180,7 +182,7 @@ "device.tip.calibrate": "Калибровка позиций LED, направления и зоны покрытия", "device.tip.webui": "Открыть встроенный веб-интерфейс устройства для расширенной настройки", "device.tip.add": "Нажмите, чтобы добавить новое LED устройство", - "settings.title": "Настройки Устройства", + "settings.title": "Настройки", "settings.general.title": "Основные Настройки", "settings.capture.title": "Настройки Захвата", "settings.capture.saved": "Настройки захвата обновлены", @@ -285,12 +287,12 @@ "section.filter.reset": "Очистить фильтр", "section.expand_all": "Развернуть все секции", "section.collapse_all": "Свернуть все секции", - "streams.title": "\uD83D\uDCFA Источники", + "streams.title": "Источники", "streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.", "streams.group.raw": "Захват Экрана", "streams.group.processed": "Обработанные", "streams.group.audio": "Аудио", - "streams.section.streams": "\uD83D\uDCFA Источники", + "streams.section.streams": "Источники", "streams.add": "Добавить Источник", "streams.add.raw": "Добавить Захват Экрана", "streams.add.processed": "Добавить Обработанный", @@ -322,11 +324,11 @@ "streams.error.required": "Пожалуйста, заполните все обязательные поля", "streams.error.delete": "Не удалось удалить источник", "streams.test.title": "Тест Источника", - "streams.test.run": "🧪 Запустить", + "streams.test.run": "Запустить", "streams.test.running": "Тестирование источника...", "streams.test.duration": "Длительность Захвата (с):", "streams.test.error.failed": "Тест источника не удался", - "postprocessing.title": "\uD83D\uDCC4 Шаблоны Фильтров", + "postprocessing.title": "Шаблоны Фильтров", "postprocessing.description": "Шаблоны обработки определяют фильтры изображений и цветокоррекцию. Назначайте их обработанным источникам для единообразной постобработки на всех устройствах.", "postprocessing.add": "Добавить Шаблон Фильтра", "postprocessing.edit": "Редактировать Шаблон Фильтра", @@ -363,7 +365,7 @@ "postprocessing.test.error.no_stream": "Пожалуйста, выберите источник", "postprocessing.test.error.failed": "Тест шаблона фильтра не удался", "device.button.stream_selector": "Настройки источника", - "device.stream_settings.title": "📺 Настройки источника", + "device.stream_settings.title": "Настройки источника", "device.stream_selector.label": "Источник:", "device.stream_selector.hint": "Выберите источник, определяющий что это устройство захватывает и обрабатывает", "device.stream_selector.none": "-- Источник не назначен --", @@ -388,13 +390,13 @@ "streams.validate_image.validating": "Проверка...", "streams.validate_image.valid": "Изображение доступно", "streams.validate_image.invalid": "Изображение недоступно", - "targets.title": "⚡ Цели", + "targets.title": "Цели", "targets.description": "Цели связывают источники цветовых полос с устройствами вывода. Каждая цель ссылается на устройство и источник цветовой полосы.", "targets.subtab.wled": "LED", "targets.subtab.led": "LED", - "targets.section.devices": "💡 Устройства", - "targets.section.color_strips": "🎞️ Источники цветовых полос", - "targets.section.targets": "⚡ Цели", + "targets.section.devices": "Устройства", + "targets.section.color_strips": "Источники цветовых полос", + "targets.section.targets": "Цели", "targets.section.specific_settings": "Специальные настройки", "targets.add": "Добавить Цель", "targets.edit": "Редактировать Цель", @@ -444,7 +446,7 @@ "targets.metrics.frames": "Кадры", "targets.metrics.errors": "Ошибки", "targets.subtab.key_colors": "Ключевые Цвета", - "targets.section.key_colors": "🎨 Цели Ключевых Цветов", + "targets.section.key_colors": "Цели Ключевых Цветов", "kc.add": "Добавить Цель Ключевых Цветов", "kc.edit": "Редактировать Цель Ключевых Цветов", "kc.name": "Имя Цели:", @@ -476,9 +478,9 @@ "kc.colors.none": "Цвета пока не извлечены", "kc.test": "Тест", "kc.test.error": "Ошибка теста", - "targets.section.pattern_templates": "📄 Шаблоны Паттернов", - "pattern.add": "📄 Добавить Шаблон Паттерна", - "pattern.edit": "📄 Редактировать Шаблон Паттерна", + "targets.section.pattern_templates": "Шаблоны Паттернов", + "pattern.add": "Добавить Шаблон Паттерна", + "pattern.edit": "Редактировать Шаблон Паттерна", "pattern.name": "Имя Шаблона:", "pattern.name.placeholder": "Мой Шаблон Паттерна", "pattern.description_label": "Описание (необязательно):", @@ -513,7 +515,7 @@ "overlay.stopped": "Визуализация наложения остановлена", "overlay.error.start": "Не удалось запустить наложение", "overlay.error.stop": "Не удалось остановить наложение", - "dashboard.title": "📊 Обзор", + "dashboard.title": "Обзор", "dashboard.section.targets": "Цели", "dashboard.section.running": "Запущенные", "dashboard.section.stopped": "Остановленные", @@ -532,10 +534,9 @@ "dashboard.perf.gpu": "ГП", "dashboard.perf.unavailable": "недоступно", "dashboard.poll_interval": "Интервал обновления", - - "profiles.title": "\uD83D\uDCCB Профили", + "profiles.title": "Профили", "profiles.empty": "Профили не настроены. Создайте профиль для автоматизации целей.", - "profiles.add": "\uD83D\uDCCB Добавить профиль", + "profiles.add": "Добавить профиль", "profiles.edit": "Редактировать профиль", "profiles.delete.confirm": "Удалить профиль \"{name}\"?", "profiles.name": "Название:", @@ -549,7 +550,7 @@ "profiles.conditions": "Условия:", "profiles.conditions.hint": "Правила, определяющие когда профиль активируется", "profiles.conditions.add": "Добавить условие", - "profiles.conditions.empty": "Нет условий \u2014 профиль всегда активен когда включён", + "profiles.conditions.empty": "Нет условий — профиль всегда активен когда включён", "profiles.condition.always": "Всегда", "profiles.condition.always.hint": "Профиль активируется сразу при включении и остаётся активным. Используйте для автозапуска целей при старте сервера.", "profiles.condition.application": "Приложение", @@ -597,9 +598,8 @@ "aria.previous": "Назад", "aria.next": "Вперёд", "aria.hint": "Показать подсказку", - - "color_strip.add": "🎞️ Добавить источник цветовой полосы", - "color_strip.edit": "🎞️ Редактировать источник цветовой полосы", + "color_strip.add": "Добавить источник цветовой полосы", + "color_strip.edit": "Редактировать источник цветовой полосы", "color_strip.name": "Название:", "color_strip.name.placeholder": "Настенная полоса", "color_strip.picture_source": "Источник изображения:", @@ -786,7 +786,6 @@ "color_strip.palette.aurora": "Аврора", "color_strip.palette.sunset": "Закат", "color_strip.palette.ice": "Лёд", - "audio_source.title": "Аудиоисточники", "audio_source.group.multichannel": "Многоканальные", "audio_source.group.mono": "Моно", @@ -829,14 +828,12 @@ "audio_source.test.beat": "Бит", "audio_source.test.connecting": "Подключение...", "audio_source.test.error": "Ошибка теста аудио", - "audio_template.test": "Тест", "audio_template.test.title": "Тест аудиошаблона", "audio_template.test.device": "Аудиоустройство:", "audio_template.test.device.hint": "Выберите устройство для захвата звука во время теста", - "audio_template.test.run": "🧪 Запуск", - - "audio_template.title": "🎵 Аудиошаблоны", + "audio_template.test.run": "Запуск", + "audio_template.title": "Аудиошаблоны", "audio_template.add": "Добавить аудиошаблон", "audio_template.edit": "Редактировать аудиошаблон", "audio_template.name": "Название шаблона:", @@ -857,9 +854,8 @@ "audio_template.error.engines": "Не удалось загрузить аудиодвижки", "audio_template.error.required": "Пожалуйста, заполните все обязательные поля", "audio_template.error.delete": "Не удалось удалить аудиошаблон", - "streams.group.value": "Источники значений", - "value_source.group.title": "🔢 Источники значений", + "value_source.group.title": "Источники значений", "value_source.add": "Добавить источник значений", "value_source.edit": "Редактировать источник значений", "value_source.name": "Название:", @@ -947,7 +943,6 @@ "targets.protocol": "Протокол:", "targets.protocol.hint": "DDP отправляет пиксели по быстрому UDP (рекомендуется). HTTP использует JSON API — медленнее, но надёжнее, ограничение ~500 LED.", "targets.protocol.serial": "Serial", - "search.open": "Поиск (Ctrl+K)", "search.placeholder": "Поиск... (Ctrl+K)", "search.loading": "Загрузка...", @@ -963,8 +958,6 @@ "search.group.pattern_templates": "Шаблоны паттернов", "search.group.audio": "Аудиоисточники", "search.group.value": "Источники значений", - - "settings.title": "Настройки", "settings.backup.label": "Резервное копирование", "settings.backup.hint": "Скачать всю конфигурацию (устройства, цели, потоки, шаблоны, профили) в виде одного JSON-файла.", "settings.backup.button": "Скачать резервную копию", @@ -979,7 +972,6 @@ "settings.restore.restarting": "Сервер перезапускается...", "settings.restore.restart_timeout": "Сервер не отвечает. Обновите страницу вручную.", "settings.button.close": "Закрыть", - "settings.auto_backup.label": "Авто-бэкап", "settings.auto_backup.hint": "Автоматическое создание периодических резервных копий конфигурации. Старые копии удаляются при превышении максимального количества.", "settings.auto_backup.enable": "Включить авто-бэкап", @@ -990,7 +982,6 @@ "settings.auto_backup.save_error": "Не удалось сохранить настройки авто-бэкапа", "settings.auto_backup.last_backup": "Последний бэкап", "settings.auto_backup.never": "Никогда", - "settings.saved_backups.label": "Сохранённые копии", "settings.saved_backups.hint": "Файлы авто-бэкапа на сервере. Скачайте для локального хранения или удалите для освобождения места.", "settings.saved_backups.empty": "Нет сохранённых копий", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 50caaa8..ce51ada 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -3,6 +3,8 @@ "app.version": "版本:", "app.api_docs": "API 文档", "theme.toggle": "切换主题", + "accent.title": "主题色", + "accent.custom": "自定义", "locale.change": "切换语言", "auth.login": "登录", "auth.logout": "退出", @@ -21,7 +23,7 @@ "auth.please_login": "请先登录", "auth.session_expired": "会话已过期或 API 密钥无效,请重新登录。", "displays.title": "可用显示器", - "displays.layout": "\uD83D\uDDA5\uFE0F 显示器", + "displays.layout": "显示器", "displays.information": "显示器信息", "displays.legend.primary": "主显示器", "displays.legend.secondary": "副显示器", @@ -44,7 +46,7 @@ "displays.picker.adb_connect.error": "连接设备失败", "displays.picker.adb_disconnect": "断开连接", "displays.picker.no_android": "未找到 Android 设备。请通过 USB 连接或在上方输入 IP 地址。", - "templates.title": "\uD83D\uDCC4 引擎模板", + "templates.title": "引擎模板", "templates.description": "采集模板定义屏幕的采集方式。每个模板使用特定的采集引擎(MSS、DXcam、WGC)及自定义设置。将模板分配给设备以获得最佳性能。", "templates.loading": "正在加载模板...", "templates.empty": "尚未配置采集模板", @@ -77,7 +79,7 @@ "templates.test.display.select": "选择显示器...", "templates.test.duration": "采集时长(秒):", "templates.test.border_width": "边框宽度(像素):", - "templates.test.run": "\uD83E\uDDEA 运行", + "templates.test.run": "运行", "templates.test.running": "正在运行测试...", "templates.test.results.preview": "全幅采集预览", "templates.test.results.borders": "边框提取", @@ -97,7 +99,7 @@ "templates.test.error.no_engine": "请选择采集引擎", "templates.test.error.no_display": "请选择显示器", "templates.test.error.failed": "测试失败", - "devices.title": "\uD83D\uDCA1 设备", + "devices.title": "设备", "devices.add": "添加新设备", "devices.loading": "正在加载设备...", "devices.none": "尚未配置设备", @@ -180,7 +182,7 @@ "device.tip.calibrate": "校准 LED 位置、方向和覆盖范围", "device.tip.webui": "打开设备内置的 Web 界面进行高级配置", "device.tip.add": "点击此处添加新的 LED 设备", - "settings.title": "设备设置", + "settings.title": "设置", "settings.general.title": "常规设置", "settings.capture.title": "采集设置", "settings.capture.saved": "采集设置已更新", @@ -285,12 +287,12 @@ "section.filter.reset": "清除筛选", "section.expand_all": "全部展开", "section.collapse_all": "全部折叠", - "streams.title": "\uD83D\uDCFA 源", + "streams.title": "源", "streams.description": "源定义采集管线。原始源使用采集模板从显示器采集。处理源对另一个源应用后处理。将源分配给设备。", "streams.group.raw": "屏幕采集", "streams.group.processed": "已处理", "streams.group.audio": "音频", - "streams.section.streams": "\uD83D\uDCFA 源", + "streams.section.streams": "源", "streams.add": "添加源", "streams.add.raw": "添加屏幕采集", "streams.add.processed": "添加处理源", @@ -322,11 +324,11 @@ "streams.error.required": "请填写所有必填项", "streams.error.delete": "删除源失败", "streams.test.title": "测试源", - "streams.test.run": "🧪 运行", + "streams.test.run": "运行", "streams.test.running": "正在测试源...", "streams.test.duration": "采集时长(秒):", "streams.test.error.failed": "源测试失败", - "postprocessing.title": "\uD83D\uDCC4 滤镜模板", + "postprocessing.title": "滤镜模板", "postprocessing.description": "处理模板定义图像滤镜和色彩校正。将它们分配给处理图片源以实现跨设备的一致后处理。", "postprocessing.add": "添加滤镜模板", "postprocessing.edit": "编辑滤镜模板", @@ -363,7 +365,7 @@ "postprocessing.test.error.no_stream": "请选择一个源", "postprocessing.test.error.failed": "处理模板测试失败", "device.button.stream_selector": "源设置", - "device.stream_settings.title": "📺 源设置", + "device.stream_settings.title": "源设置", "device.stream_selector.label": "源:", "device.stream_selector.hint": "选择一个源来定义此设备采集和处理的内容", "device.stream_selector.none": "-- 未分配源 --", @@ -388,13 +390,13 @@ "streams.validate_image.validating": "正在验证...", "streams.validate_image.valid": "图片可访问", "streams.validate_image.invalid": "图片不可访问", - "targets.title": "⚡ 目标", + "targets.title": "目标", "targets.description": "目标将色带源桥接到输出设备。每个目标引用一个设备和一个色带源。", "targets.subtab.wled": "LED", "targets.subtab.led": "LED", - "targets.section.devices": "💡 设备", - "targets.section.color_strips": "🎞️ 色带源", - "targets.section.targets": "⚡ 目标", + "targets.section.devices": "设备", + "targets.section.color_strips": "色带源", + "targets.section.targets": "目标", "targets.section.specific_settings": "特定设置", "targets.add": "添加目标", "targets.edit": "编辑目标", @@ -444,7 +446,7 @@ "targets.metrics.frames": "帧数", "targets.metrics.errors": "错误", "targets.subtab.key_colors": "关键颜色", - "targets.section.key_colors": "🎨 关键颜色目标", + "targets.section.key_colors": "关键颜色目标", "kc.add": "添加关键颜色目标", "kc.edit": "编辑关键颜色目标", "kc.name": "目标名称:", @@ -476,9 +478,9 @@ "kc.colors.none": "尚未提取颜色", "kc.test": "测试", "kc.test.error": "测试失败", - "targets.section.pattern_templates": "📄 图案模板", - "pattern.add": "📄 添加图案模板", - "pattern.edit": "📄 编辑图案模板", + "targets.section.pattern_templates": "图案模板", + "pattern.add": "添加图案模板", + "pattern.edit": "编辑图案模板", "pattern.name": "模板名称:", "pattern.name.placeholder": "我的图案模板", "pattern.description_label": "描述(可选):", @@ -513,7 +515,7 @@ "overlay.stopped": "叠加层可视化已停止", "overlay.error.start": "启动叠加层失败", "overlay.error.stop": "停止叠加层失败", - "dashboard.title": "📊 仪表盘", + "dashboard.title": "仪表盘", "dashboard.section.targets": "目标", "dashboard.section.running": "运行中", "dashboard.section.stopped": "已停止", @@ -532,10 +534,9 @@ "dashboard.perf.gpu": "GPU", "dashboard.perf.unavailable": "不可用", "dashboard.poll_interval": "刷新间隔", - - "profiles.title": "\uD83D\uDCCB 配置文件", + "profiles.title": "配置文件", "profiles.empty": "尚未配置配置文件。创建一个以自动化目标激活。", - "profiles.add": "\uD83D\uDCCB 添加配置文件", + "profiles.add": "添加配置文件", "profiles.edit": "编辑配置文件", "profiles.delete.confirm": "删除配置文件 \"{name}\"?", "profiles.name": "名称:", @@ -597,9 +598,8 @@ "aria.previous": "上一个", "aria.next": "下一个", "aria.hint": "显示提示", - - "color_strip.add": "🎞️ 添加色带源", - "color_strip.edit": "🎞️ 编辑色带源", + "color_strip.add": "添加色带源", + "color_strip.edit": "编辑色带源", "color_strip.name": "名称:", "color_strip.name.placeholder": "墙壁灯带", "color_strip.picture_source": "图片源:", @@ -786,7 +786,6 @@ "color_strip.palette.aurora": "极光", "color_strip.palette.sunset": "日落", "color_strip.palette.ice": "冰", - "audio_source.title": "音频源", "audio_source.group.multichannel": "多声道", "audio_source.group.mono": "单声道", @@ -829,14 +828,12 @@ "audio_source.test.beat": "节拍", "audio_source.test.connecting": "连接中...", "audio_source.test.error": "音频测试失败", - "audio_template.test": "测试", "audio_template.test.title": "测试音频模板", "audio_template.test.device": "音频设备:", "audio_template.test.device.hint": "选择测试期间要采集的音频设备", - "audio_template.test.run": "🧪 运行", - - "audio_template.title": "🎵 音频模板", + "audio_template.test.run": "运行", + "audio_template.title": "音频模板", "audio_template.add": "添加音频模板", "audio_template.edit": "编辑音频模板", "audio_template.name": "模板名称:", @@ -857,9 +854,8 @@ "audio_template.error.engines": "加载音频引擎失败", "audio_template.error.required": "请填写所有必填项", "audio_template.error.delete": "删除音频模板失败", - "streams.group.value": "值源", - "value_source.group.title": "🔢 值源", + "value_source.group.title": "值源", "value_source.add": "添加值源", "value_source.edit": "编辑值源", "value_source.name": "名称:", @@ -947,7 +943,6 @@ "targets.protocol": "协议:", "targets.protocol.hint": "DDP通过快速UDP发送像素(推荐)。HTTP使用JSON API——较慢但可靠,限制约500个LED。", "targets.protocol.serial": "串口", - "search.open": "搜索 (Ctrl+K)", "search.placeholder": "搜索实体... (Ctrl+K)", "search.loading": "加载中...", @@ -963,8 +958,6 @@ "search.group.pattern_templates": "图案模板", "search.group.audio": "音频源", "search.group.value": "值源", - - "settings.title": "设置", "settings.backup.label": "备份配置", "settings.backup.hint": "将所有配置(设备、目标、流、模板、配置文件)下载为单个 JSON 文件。", "settings.backup.button": "下载备份", @@ -979,7 +972,6 @@ "settings.restore.restarting": "服务器正在重启...", "settings.restore.restart_timeout": "服务器未响应。请手动刷新页面。", "settings.button.close": "关闭", - "settings.auto_backup.label": "自动备份", "settings.auto_backup.hint": "自动定期创建所有配置的备份。当达到最大数量时,旧备份会被自动清理。", "settings.auto_backup.enable": "启用自动备份", @@ -990,7 +982,6 @@ "settings.auto_backup.save_error": "保存自动备份设置失败", "settings.auto_backup.last_backup": "上次备份", "settings.auto_backup.never": "从未", - "settings.saved_backups.label": "已保存的备份", "settings.saved_backups.hint": "存储在服务器上的自动备份文件。下载到本地保存,或删除以释放空间。", "settings.saved_backups.empty": "没有已保存的备份", diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html index 57436bf..b4e982c 100644 --- a/server/src/wled_controller/templates/index.html +++ b/server/src/wled_controller/templates/index.html @@ -29,16 +29,39 @@
API +
+ + +
- - - - + + + +
@@ -158,7 +181,9 @@ function updateThemeIcon(theme) { const icon = document.getElementById('theme-icon'); - icon.textContent = theme === 'dark' ? '☀️' : '🌙'; + icon.innerHTML = theme === 'dark' + ? '' + : ''; } function toggleTheme() { @@ -168,9 +193,90 @@ document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeIcon(newTheme); + // Re-derive accent text variant for the new theme + const accent = localStorage.getItem('accentColor'); + if (accent) applyAccentColor(accent, true); showToast(`Switched to ${newTheme} theme`, 'info'); } + // Initialize accent color + function adjustLightness(hex, amount) { + const r = parseInt(hex.slice(1,3),16)/255; + const g = parseInt(hex.slice(3,5),16)/255; + const b = parseInt(hex.slice(5,7),16)/255; + const max = Math.max(r,g,b), min = Math.min(r,g,b); + let h, s, l = (max+min)/2; + if (max===min) { h=s=0; } else { + const d = max-min; + s = l>0.5 ? d/(2-max-min) : d/(max+min); + if (max===r) h=((g-b)/d+(g { if(t<0)t+=1; if(t>1)t-=1; if(t<1/6)return p+(q-p)*6*t; if(t<1/2)return q; if(t<2/3)return p+(q-p)*(2/3-t)*6; return p; }; + let rr,gg,bb; + if (s===0) { rr=gg=bb=l; } else { + const q = l<0.5 ? l*(1+s) : l+s-l*s, p = 2*l-q; + rr=hue2rgb(p,q,h+1/3); gg=hue2rgb(p,q,h); bb=hue2rgb(p,q,h-1/3); + } + return '#'+[rr,gg,bb].map(x=>Math.round(x*255).toString(16).padStart(2,'0')).join(''); + } + + function applyAccentColor(hex, silent) { + const root = document.documentElement; + root.style.setProperty('--primary-color', hex); + const theme = root.getAttribute('data-theme'); + root.style.setProperty('--primary-text-color', adjustLightness(hex, theme === 'dark' ? 15 : -15)); + root.style.setProperty('--primary-hover', adjustLightness(hex, 8)); + document.getElementById('accent-swatch').style.background = hex; + document.getElementById('accent-picker').value = hex; + // Mark the active preset dot + document.querySelectorAll('.accent-dot').forEach(d => { + d.classList.toggle('active', d.style.background === hex || d.style.backgroundColor === hex + || d.style.background.toLowerCase() === hex.toLowerCase()); + }); + localStorage.setItem('accentColor', hex); + if (!silent) showToast('Accent color updated', 'info'); + } + + function toggleAccentPicker() { + const pop = document.getElementById('accent-popover'); + const show = pop.style.display === 'none'; + pop.style.display = show ? '' : 'none'; + if (show) { + // Mark active dot on open + const cur = localStorage.getItem('accentColor') || '#4CAF50'; + document.querySelectorAll('.accent-dot').forEach(d => { + const dColor = d.style.backgroundColor || d.style.background; + d.classList.toggle('active', rgbToHex(dColor) === cur.toUpperCase()); + }); + } + } + + function rgbToHex(rgb) { + if (rgb.startsWith('#')) return rgb.toUpperCase(); + const m = rgb.match(/\d+/g); + if (!m) return rgb; + return '#' + m.slice(0,3).map(n => parseInt(n).toString(16).padStart(2,'0')).join('').toUpperCase(); + } + + function pickAccent(hex) { + applyAccentColor(hex); + document.getElementById('accent-popover').style.display = 'none'; + } + + // Close popover on outside click + document.addEventListener('click', function(e) { + const wrapper = document.querySelector('.accent-wrapper'); + if (wrapper && !wrapper.contains(e.target)) { + document.getElementById('accent-popover').style.display = 'none'; + } + }); + + const savedAccent = localStorage.getItem('accentColor'); + if (savedAccent) applyAccentColor(savedAccent, true); + // Initialize auth state function updateAuthUI() { const apiKey = localStorage.getItem('wled_api_key'); @@ -228,10 +334,10 @@ const button = document.querySelector('.password-toggle'); if (input.type === 'password') { input.type = 'text'; - button.textContent = '🙈'; + button.innerHTML = ''; } else { input.type = 'password'; - button.textContent = '👁️'; + button.innerHTML = ''; } } diff --git a/server/src/wled_controller/templates/modals/add-device.html b/server/src/wled_controller/templates/modals/add-device.html index c0b5c42..7f27a64 100644 --- a/server/src/wled_controller/templates/modals/add-device.html +++ b/server/src/wled_controller/templates/modals/add-device.html @@ -4,7 +4,7 @@ diff --git a/server/src/wled_controller/templates/modals/api-key.html b/server/src/wled_controller/templates/modals/api-key.html index 92bb79f..dd21563 100644 --- a/server/src/wled_controller/templates/modals/api-key.html +++ b/server/src/wled_controller/templates/modals/api-key.html @@ -2,7 +2,7 @@ diff --git a/server/src/wled_controller/templates/modals/calibration.html b/server/src/wled_controller/templates/modals/calibration.html index e64f523..9647bcc 100644 --- a/server/src/wled_controller/templates/modals/calibration.html +++ b/server/src/wled_controller/templates/modals/calibration.html @@ -2,7 +2,7 @@