diff --git a/server/src/wled_controller/static/js/core/icon-paths.js b/server/src/wled_controller/static/js/core/icon-paths.js index 8879511..b96348c 100644 --- a/server/src/wled_controller/static/js/core/icon-paths.js +++ b/server/src/wled_controller/static/js/core/icon-paths.js @@ -71,3 +71,5 @@ export const rotateCcw = ''; export const undo2 = ''; export const power = ''; +export const wifi = ''; +export const usb = ''; diff --git a/server/src/wled_controller/static/js/core/icons.js b/server/src/wled_controller/static/js/core/icons.js index a4d766f..d698b36 100644 --- a/server/src/wled_controller/static/js/core/icons.js +++ b/server/src/wled_controller/static/js/core/icons.js @@ -29,7 +29,13 @@ const _valueSourceTypeIcons = { adaptive_time: _svg(P.clock), adaptive_scene: _svg(P.cloudSun), }; const _audioSourceTypeIcons = { mono: _svg(P.mic), multichannel: _svg(P.volume2) }; +const _deviceTypeIcons = { + wled: _svg(P.wifi), adalight: _svg(P.usb), ambiled: _svg(P.usb), + mqtt: _svg(P.send), ws: _svg(P.globe), openrgb: _svg(P.palette), + mock: _svg(P.wrench), +}; const _engineTypeIcons = { scrcpy: _svg(P.smartphone) }; +const _audioEngineTypeIcons = { wasapi: _svg(P.volume2), sounddevice: _svg(P.mic) }; // ── Type-resolution getters ───────────────────────────────── @@ -58,11 +64,21 @@ export function getAudioSourceIcon(sourceType) { return _audioSourceTypeIcons[sourceType] || _svg(P.music); } +/** Device type → icon (fallback: lightbulb) */ +export function getDeviceTypeIcon(deviceType) { + return _deviceTypeIcons[deviceType] || _svg(P.lightbulb); +} + /** Capture engine type → icon (fallback: rocket) */ export function getEngineIcon(engineType) { return _engineTypeIcons[engineType] || _svg(P.rocket); } +/** Audio engine type → icon (fallback: music) */ +export function getAudioEngineIcon(engineType) { + return _audioEngineTypeIcons[engineType] || _svg(P.music); +} + // ── Entity-kind constants ─────────────────────────────────── export const ICON_AUTOMATION = _svg(P.clipboardList); diff --git a/server/src/wled_controller/static/js/features/device-discovery.js b/server/src/wled_controller/static/js/features/device-discovery.js index 0634318..cb56c3f 100644 --- a/server/src/wled_controller/static/js/features/device-discovery.js +++ b/server/src/wled_controller/static/js/features/device-discovery.js @@ -11,6 +11,8 @@ import { t } from '../core/i18n.js'; import { showToast } from '../core/ui.js'; import { Modal } from '../core/modal.js'; import { _computeMaxFps, _renderFpsHint } from './devices.js'; +import { getDeviceTypeIcon } from '../core/icons.js'; +import { IconSelect } from '../core/icon-select.js'; class AddDeviceModal extends Modal { constructor() { super('add-device-modal'); } @@ -33,8 +35,31 @@ class AddDeviceModal extends Modal { const addDeviceModal = new AddDeviceModal(); +/* ── Icon-grid type selector ──────────────────────────────────── */ + +const DEVICE_TYPE_KEYS = ['wled', 'adalight', 'ambiled', 'mqtt', 'ws', 'openrgb', 'mock']; + +function _buildDeviceTypeItems() { + return DEVICE_TYPE_KEYS.map(key => ({ + value: key, + icon: getDeviceTypeIcon(key), + label: t(`device.type.${key}`), + desc: t(`device.type.${key}.desc`), + })); +} + +let _deviceTypeIconSelect = null; + +function _ensureDeviceTypeIconSelect() { + const sel = document.getElementById('device-type'); + if (!sel) return; + if (_deviceTypeIconSelect) { _deviceTypeIconSelect.updateItems(_buildDeviceTypeItems()); return; } + _deviceTypeIconSelect = new IconSelect({ target: sel, items: _buildDeviceTypeItems(), columns: 3 }); +} + export function onDeviceTypeChanged() { const deviceType = document.getElementById('device-type').value; + if (_deviceTypeIconSelect) _deviceTypeIconSelect.setValue(deviceType); const urlGroup = document.getElementById('device-url-group'); const urlInput = document.getElementById('device-url'); const serialGroup = document.getElementById('device-serial-port-group'); @@ -272,6 +297,7 @@ export function showAddDevice() { document.getElementById('device-serial-port').innerHTML = ''; const scanBtn = document.getElementById('scan-network-btn'); if (scanBtn) scanBtn.disabled = false; + _ensureDeviceTypeIconSelect(); addDeviceModal.open(); onDeviceTypeChanged(); setTimeout(() => { diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index 20eddff..08c7ea0 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -41,13 +41,14 @@ import { updateSubTabHash } from './tabs.js'; import { createValueSourceCard } from './value-sources.js'; import { createSyncClockCard } from './sync-clocks.js'; import { - getEngineIcon, getPictureSourceIcon, getAudioSourceIcon, + getEngineIcon, getAudioEngineIcon, getPictureSourceIcon, getAudioSourceIcon, ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE, ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_CLOCK, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT, ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO, ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_HELP, } from '../core/icons.js'; import { wrapCard } from '../core/card-colors.js'; +import { IconSelect } from '../core/icon-select.js'; // ── Card section instances ── const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')", keyAttr: 'data-stream-id' }); @@ -296,14 +297,25 @@ async function loadAvailableEngines() { const firstAvailable = availableEngines.find(e => e.available); if (firstAvailable) select.value = firstAvailable.type; } + + // Update icon-grid selector with dynamic engine list + const items = availableEngines + .filter(e => e.available) + .map(e => ({ value: e.type, icon: getEngineIcon(e.type), label: e.name, desc: '' })); + if (_engineIconSelect) { _engineIconSelect.updateItems(items); } + else { _engineIconSelect = new IconSelect({ target: select, items, columns: 2 }); } + _engineIconSelect.setValue(select.value); } catch (error) { console.error('Error loading engines:', error); showToast(t('templates.error.engines') + ': ' + error.message, 'error'); } } +let _engineIconSelect = null; + export async function onEngineChange() { const engineType = document.getElementById('template-engine').value; + if (_engineIconSelect) _engineIconSelect.setValue(engineType); const configSection = document.getElementById('engine-config-section'); const configFields = document.getElementById('engine-config-fields'); @@ -667,14 +679,25 @@ async function loadAvailableAudioEngines() { const firstAvailable = availableAudioEngines.find(e => e.available); if (firstAvailable) select.value = firstAvailable.type; } + + // Update icon-grid selector with dynamic engine list + const items = availableAudioEngines + .filter(e => e.available) + .map(e => ({ value: e.type, icon: getAudioEngineIcon(e.type), label: e.type.toUpperCase(), desc: '' })); + if (_audioEngineIconSelect) { _audioEngineIconSelect.updateItems(items); } + else { _audioEngineIconSelect = new IconSelect({ target: select, items, columns: 2 }); } + _audioEngineIconSelect.setValue(select.value); } catch (error) { console.error('Error loading audio engines:', error); showToast(t('audio_template.error.engines') + ': ' + error.message, 'error'); } } +let _audioEngineIconSelect = null; + export async function onAudioEngineChange() { const engineType = document.getElementById('audio-template-engine').value; + if (_audioEngineIconSelect) _audioEngineIconSelect.setValue(engineType); const configSection = document.getElementById('audio-engine-config-section'); const configFields = document.getElementById('audio-engine-config-fields'); 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 3fd395b..e75349e 100644 --- a/server/src/wled_controller/static/js/features/value-sources.js +++ b/server/src/wled_controller/static/js/features/value-sources.js @@ -22,6 +22,7 @@ import { ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH, } from '../core/icons.js'; import { wrapCard } from '../core/card-colors.js'; +import { IconSelect } from '../core/icon-select.js'; import { loadPictureSources } from './streams.js'; export { getValueSourceIcon }; @@ -58,6 +59,28 @@ class ValueSourceModal extends Modal { const valueSourceModal = new ValueSourceModal(); +/* ── Icon-grid type selector ──────────────────────────────────── */ + +const VS_TYPE_KEYS = ['static', 'animated', 'audio', 'adaptive_time', 'adaptive_scene']; + +function _buildVSTypeItems() { + return VS_TYPE_KEYS.map(key => ({ + value: key, + icon: getValueSourceIcon(key), + label: t(`value_source.type.${key}`), + desc: t(`value_source.type.${key}.desc`), + })); +} + +let _vsTypeIconSelect = null; + +function _ensureVSTypeIconSelect() { + const sel = document.getElementById('value-source-type'); + if (!sel) return; + if (_vsTypeIconSelect) { _vsTypeIconSelect.updateItems(_buildVSTypeItems()); return; } + _vsTypeIconSelect = new IconSelect({ target: sel, items: _buildVSTypeItems(), columns: 2 }); +} + // ── Modal ───────────────────────────────────────────────────── export async function showValueSourceModal(editData) { @@ -69,6 +92,7 @@ export async function showValueSourceModal(editData) { document.getElementById('value-source-id').value = isEdit ? editData.id : ''; document.getElementById('value-source-error').style.display = 'none'; + _ensureVSTypeIconSelect(); const typeSelect = document.getElementById('value-source-type'); document.getElementById('value-source-type-group').style.display = isEdit ? 'none' : ''; @@ -142,6 +166,7 @@ export async function closeValueSourceModal() { export function onValueSourceTypeChange() { const type = document.getElementById('value-source-type').value; + if (_vsTypeIconSelect) _vsTypeIconSelect.setValue(type); document.getElementById('value-source-static-section').style.display = type === 'static' ? '' : 'none'; document.getElementById('value-source-animated-section').style.display = type === 'animated' ? '' : 'none'; document.getElementById('value-source-audio-section').style.display = type === 'audio' ? '' : 'none'; diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 46a3afc..8dfa3ac 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -120,6 +120,20 @@ "device.scan.selected": "Device selected", "device.type": "Device Type:", "device.type.hint": "Select the type of LED controller", + "device.type.wled": "WLED", + "device.type.wled.desc": "WiFi LED controller over HTTP/UDP", + "device.type.adalight": "Adalight", + "device.type.adalight.desc": "Serial LED protocol for Arduino", + "device.type.ambiled": "AmbiLED", + "device.type.ambiled.desc": "Serial protocol for AmbiLED devices", + "device.type.mqtt": "MQTT", + "device.type.mqtt.desc": "Publish LED data via MQTT broker", + "device.type.ws": "WebSocket", + "device.type.ws.desc": "Stream LED data to WebSocket clients", + "device.type.openrgb": "OpenRGB", + "device.type.openrgb.desc": "Control RGB peripherals via OpenRGB", + "device.type.mock": "Mock", + "device.type.mock.desc": "Virtual device for testing", "device.serial_port": "Serial Port:", "device.serial_port.hint": "Select the COM port of the Adalight device", "device.serial_port.none": "No serial ports found", @@ -999,10 +1013,15 @@ "value_source.type": "Type:", "value_source.type.hint": "Static outputs a constant value. Animated cycles through a waveform. Audio reacts to sound input. Adaptive types adjust brightness automatically based on time of day or scene content.", "value_source.type.static": "Static", + "value_source.type.static.desc": "Constant output value", "value_source.type.animated": "Animated", + "value_source.type.animated.desc": "Cycles through a waveform", "value_source.type.audio": "Audio", - "value_source.type.adaptive_time": "Adaptive (Time of Day)", + "value_source.type.audio.desc": "Reacts to sound input", + "value_source.type.adaptive_time": "Adaptive (Time)", + "value_source.type.adaptive_time.desc": "Adjusts by time of day", "value_source.type.adaptive_scene": "Adaptive (Scene)", + "value_source.type.adaptive_scene.desc": "Adjusts by scene content", "value_source.value": "Value:", "value_source.value.hint": "Constant output value (0.0 = off, 1.0 = full brightness)", "value_source.waveform": "Waveform:", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index ec087fd..f2aed31 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -120,6 +120,20 @@ "device.scan.selected": "Устройство выбрано", "device.type": "Тип устройства:", "device.type.hint": "Выберите тип LED контроллера", + "device.type.wled": "WLED", + "device.type.wled.desc": "WiFi LED контроллер по HTTP/UDP", + "device.type.adalight": "Adalight", + "device.type.adalight.desc": "Серийный протокол для Arduino", + "device.type.ambiled": "AmbiLED", + "device.type.ambiled.desc": "Серийный протокол AmbiLED", + "device.type.mqtt": "MQTT", + "device.type.mqtt.desc": "Отправка LED данных через MQTT брокер", + "device.type.ws": "WebSocket", + "device.type.ws.desc": "Стриминг LED данных через WebSocket", + "device.type.openrgb": "OpenRGB", + "device.type.openrgb.desc": "Управление RGB через OpenRGB", + "device.type.mock": "Mock", + "device.type.mock.desc": "Виртуальное устройство для тестов", "device.serial_port": "Серийный порт:", "device.serial_port.hint": "Выберите COM порт устройства Adalight", "device.serial_port.none": "Серийные порты не найдены", @@ -999,10 +1013,15 @@ "value_source.type": "Тип:", "value_source.type.hint": "Статический выдаёт постоянное значение. Анимированный циклически меняет форму волны. Аудио реагирует на звук. Адаптивные типы автоматически подстраивают яркость по времени суток или содержимому сцены.", "value_source.type.static": "Статический", + "value_source.type.static.desc": "Постоянное выходное значение", "value_source.type.animated": "Анимированный", + "value_source.type.animated.desc": "Циклическая смена по форме волны", "value_source.type.audio": "Аудио", - "value_source.type.adaptive_time": "Адаптивный (Время суток)", + "value_source.type.audio.desc": "Реагирует на звуковой сигнал", + "value_source.type.adaptive_time": "Адаптивный (Время)", + "value_source.type.adaptive_time.desc": "Подстройка по времени суток", "value_source.type.adaptive_scene": "Адаптивный (Сцена)", + "value_source.type.adaptive_scene.desc": "Подстройка по содержимому сцены", "value_source.value": "Значение:", "value_source.value.hint": "Постоянное выходное значение (0.0 = выкл, 1.0 = полная яркость)", "value_source.waveform": "Форма волны:", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index e1a8637..a50e920 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -120,6 +120,20 @@ "device.scan.selected": "设备已选择", "device.type": "设备类型:", "device.type.hint": "选择 LED 控制器的类型", + "device.type.wled": "WLED", + "device.type.wled.desc": "通过HTTP/UDP控制的WiFi LED", + "device.type.adalight": "Adalight", + "device.type.adalight.desc": "Arduino串口LED协议", + "device.type.ambiled": "AmbiLED", + "device.type.ambiled.desc": "AmbiLED串口协议", + "device.type.mqtt": "MQTT", + "device.type.mqtt.desc": "通过MQTT代理发布LED数据", + "device.type.ws": "WebSocket", + "device.type.ws.desc": "通过WebSocket流式传输LED数据", + "device.type.openrgb": "OpenRGB", + "device.type.openrgb.desc": "通过OpenRGB控制RGB外设", + "device.type.mock": "Mock", + "device.type.mock.desc": "用于测试的虚拟设备", "device.serial_port": "串口:", "device.serial_port.hint": "选择 Adalight 设备的 COM 端口", "device.serial_port.none": "未找到串口", @@ -999,10 +1013,15 @@ "value_source.type": "类型:", "value_source.type.hint": "静态输出固定值。动画循环波形。音频响应声音输入。自适应类型根据时间或场景内容自动调节亮度。", "value_source.type.static": "静态", + "value_source.type.static.desc": "固定输出值", "value_source.type.animated": "动画", + "value_source.type.animated.desc": "循环波形变化", "value_source.type.audio": "音频", + "value_source.type.audio.desc": "响应声音输入", "value_source.type.adaptive_time": "自适应(时间)", + "value_source.type.adaptive_time.desc": "按时间自动调节", "value_source.type.adaptive_scene": "自适应(场景)", + "value_source.type.adaptive_scene.desc": "按场景内容调节", "value_source.value": "值:", "value_source.value.hint": "固定输出值(0.0 = 关闭,1.0 = 最大亮度)", "value_source.waveform": "波形:", diff --git a/server/src/wled_controller/static/sw.js b/server/src/wled_controller/static/sw.js index 7d51b1b..188f51f 100644 --- a/server/src/wled_controller/static/sw.js +++ b/server/src/wled_controller/static/sw.js @@ -7,7 +7,7 @@ * - Navigation: network-first with offline fallback */ -const CACHE_NAME = 'ledgrab-v15'; +const CACHE_NAME = 'ledgrab-v16'; // Only pre-cache static assets (no auth required). // Do NOT pre-cache '/' — it requires API key auth and would cache an error page.