diff --git a/server/src/wled_controller/api/routes/audio.py b/server/src/wled_controller/api/routes/audio.py index a723bb2..5c66a6a 100644 --- a/server/src/wled_controller/api/routes/audio.py +++ b/server/src/wled_controller/api/routes/audio.py @@ -10,9 +10,19 @@ router = APIRouter() @router.get("/api/v1/audio-devices", tags=["Audio"]) async def list_audio_devices(_auth: AuthRequired): - """List available audio input/output devices for audio-reactive sources.""" + """List available audio input/output devices for audio-reactive sources. + + Returns a deduped flat list (backward compat) plus a ``by_engine`` dict + with per-engine device lists (no cross-engine dedup) so the frontend can + filter by the selected audio template's engine type. + """ try: devices = AudioCaptureManager.enumerate_devices() - return {"devices": devices, "count": len(devices)} + by_engine = AudioCaptureManager.enumerate_devices_by_engine() + return { + "devices": devices, + "count": len(devices), + "by_engine": by_engine, + } except Exception as e: - return {"devices": [], "count": 0, "error": str(e)} + return {"devices": [], "count": 0, "by_engine": {}, "error": str(e)} diff --git a/server/src/wled_controller/core/audio/audio_capture.py b/server/src/wled_controller/core/audio/audio_capture.py index fb278b5..ab3e75a 100644 --- a/server/src/wled_controller/core/audio/audio_capture.py +++ b/server/src/wled_controller/core/audio/audio_capture.py @@ -292,3 +292,31 @@ class AudioCaptureManager: except Exception as e: logger.error(f"Error enumerating devices for engine '{engine_type}': {e}") return result + + @staticmethod + def enumerate_devices_by_engine() -> Dict[str, List[dict]]: + """List available audio devices grouped by engine type. + + Unlike enumerate_devices(), does NOT deduplicate across engines. + Each engine's devices are returned with their engine-specific indices. + """ + result: Dict[str, List[dict]] = {} + for engine_type, engine_class in AudioEngineRegistry.get_all_engines().items(): + try: + if not engine_class.is_available(): + continue + devices = [] + for dev in engine_class.enumerate_devices(): + devices.append({ + "index": dev.index, + "name": dev.name, + "is_input": dev.is_input, + "is_loopback": dev.is_loopback, + "channels": dev.channels, + "default_samplerate": dev.default_samplerate, + "engine_type": engine_type, + }) + result[engine_type] = devices + except Exception as e: + logger.error(f"Error enumerating devices for engine '{engine_type}': {e}") + return result diff --git a/server/src/wled_controller/static/css/tutorials.css b/server/src/wled_controller/static/css/tutorials.css index 2e27671..80ebaae 100644 --- a/server/src/wled_controller/static/css/tutorials.css +++ b/server/src/wled_controller/static/css/tutorials.css @@ -63,8 +63,8 @@ } @keyframes tutorial-pulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); } - 50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); } + 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--primary-color) 60%, transparent); } + 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--primary-color) 0%, transparent); } } .tutorial-tooltip { 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 54ea845..fe2fcd0 100644 --- a/server/src/wled_controller/static/js/features/audio-sources.js +++ b/server/src/wled_controller/static/js/features/audio-sources.js @@ -60,6 +60,7 @@ export async function showAudioSourceModal(sourceType, editData) { if (editData.source_type === 'multichannel') { _loadAudioTemplates(editData.audio_template_id); + document.getElementById('audio-source-audio-template').onchange = _filterDevicesBySelectedTemplate; await _loadAudioDevices(); _selectAudioDevice(editData.device_index, editData.is_loopback); } else { @@ -72,6 +73,7 @@ export async function showAudioSourceModal(sourceType, editData) { if (sourceType === 'multichannel') { _loadAudioTemplates(); + document.getElementById('audio-source-audio-template').onchange = _filterDevicesBySelectedTemplate; await _loadAudioDevices(); } else { _loadMultichannelSources(); @@ -191,25 +193,54 @@ export async function deleteAudioSource(sourceId) { // ── Helpers ─────────────────────────────────────────────────── +let _cachedDevicesByEngine = {}; + async function _loadAudioDevices() { - const select = document.getElementById('audio-source-device'); - if (!select) return; try { const resp = await fetchWithAuth('/audio-devices'); if (!resp.ok) throw new Error('fetch failed'); const data = await resp.json(); - const devices = data.devices || []; - select.innerHTML = devices.map(d => { - const label = d.name; - const val = `${d.index}:${d.is_loopback ? '1' : '0'}`; - return ``; - }).join(''); - if (devices.length === 0) { - select.innerHTML = ''; - } + _cachedDevicesByEngine = data.by_engine || {}; } catch { + _cachedDevicesByEngine = {}; + } + _filterDevicesBySelectedTemplate(); +} + +function _filterDevicesBySelectedTemplate() { + const select = document.getElementById('audio-source-device'); + if (!select) return; + + const prevOption = select.options[select.selectedIndex]; + const prevName = prevOption ? prevOption.textContent : ''; + + const templateId = (document.getElementById('audio-source-audio-template') || {}).value; + const templates = _cachedAudioTemplates || []; + const template = templates.find(t => t.id === templateId); + const engineType = template ? template.engine_type : null; + + let devices = []; + if (engineType && _cachedDevicesByEngine[engineType]) { + devices = _cachedDevicesByEngine[engineType]; + } else { + for (const devList of Object.values(_cachedDevicesByEngine)) { + devices = devices.concat(devList); + } + } + + select.innerHTML = devices.map(d => { + const val = `${d.index}:${d.is_loopback ? '1' : '0'}`; + return ``; + }).join(''); + + if (devices.length === 0) { select.innerHTML = ''; } + + if (prevName) { + const match = Array.from(select.options).find(o => o.textContent === prevName); + if (match) select.value = match.value; + } } function _selectAudioDevice(deviceIndex, isLoopback) { diff --git a/server/src/wled_controller/static/js/features/tutorials.js b/server/src/wled_controller/static/js/features/tutorials.js index fa91a30..3dfaf5c 100644 --- a/server/src/wled_controller/static/js/features/tutorials.js +++ b/server/src/wled_controller/static/js/features/tutorials.js @@ -24,12 +24,14 @@ const TOUR_KEY = 'tour_completed'; const gettingStartedSteps = [ { selector: 'header .header-title', textKey: 'tour.welcome', position: 'bottom' }, { selector: '#tab-btn-dashboard', textKey: 'tour.dashboard', position: 'bottom' }, + { selector: '#tab-btn-profiles', textKey: 'tour.profiles', position: 'bottom' }, { selector: '#tab-btn-targets', textKey: 'tour.targets', position: 'bottom' }, { selector: '#tab-btn-streams', textKey: 'tour.sources', position: 'bottom' }, - { selector: '#tab-btn-profiles', textKey: 'tour.profiles', position: 'bottom' }, - { selector: '[onclick*="openSettingsModal"]', textKey: 'tour.settings', position: 'bottom' }, + { selector: 'a.header-link[href="/docs"]', textKey: 'tour.api', position: 'bottom' }, { selector: '[onclick*="openCommandPalette"]', textKey: 'tour.search', position: 'bottom' }, { selector: '[onclick*="toggleTheme"]', textKey: 'tour.theme', position: 'bottom' }, + { selector: '#cp-wrap-accent', textKey: 'tour.accent', position: 'bottom' }, + { selector: '[onclick*="openSettingsModal"]', textKey: 'tour.settings', position: 'bottom' }, { selector: '#locale-select', textKey: 'tour.language', position: 'bottom' } ]; @@ -83,6 +85,8 @@ const deviceTutorialSteps = [ export function startTutorial(config) { closeTutorial(); + // Remove focus from the trigger button so its outline doesn't persist + if (document.activeElement) document.activeElement.blur(); const overlay = document.getElementById(config.overlayId); if (!overlay) return; diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 5af77d9..cfd40b8 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -222,8 +222,10 @@ "tour.sources": "Sources — manage capture templates, picture sources, audio sources, and color strips.", "tour.profiles": "Profiles — group targets and automate switching with time, audio, or value conditions.", "tour.settings": "Settings — backup and restore configuration, manage auto-backups.", + "tour.api": "API Docs — interactive REST API documentation powered by Swagger.", "tour.search": "Search — quickly find and navigate to any entity with Ctrl+K.", "tour.theme": "Theme — switch between dark and light mode.", + "tour.accent": "Accent color — customize the UI accent color to your preference.", "tour.language": "Language — choose your preferred interface language.", "tour.restart": "Restart tutorial", "tour.dash.perf": "Performance — real-time FPS charts, latency metrics, and poll interval control.", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 89291f5..5990744 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -222,8 +222,10 @@ "tour.sources": "Источники — управление шаблонами захвата, источниками изображений, звука и цветовых полос.", "tour.profiles": "Профили — группируйте цели и автоматизируйте переключение по расписанию, звуку или значениям.", "tour.settings": "Настройки — резервное копирование и восстановление конфигурации.", + "tour.api": "API Документация — интерактивная документация REST API на базе Swagger.", "tour.search": "Поиск — быстрый поиск и переход к любому объекту по Ctrl+K.", "tour.theme": "Тема — переключение между тёмной и светлой темой.", + "tour.accent": "Цвет акцента — настройте цвет интерфейса по своему вкусу.", "tour.language": "Язык — выберите предпочитаемый язык интерфейса.", "tour.restart": "Запустить тур заново", "tour.dash.perf": "Производительность — графики FPS в реальном времени, метрики задержки и интервал опроса.", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 02b47d3..5bd9fb4 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -222,8 +222,10 @@ "tour.sources": "来源 — 管理捕获模板、图片来源、音频来源和色带。", "tour.profiles": "配置文件 — 将目标分组,并通过时间、音频或数值条件自动切换。", "tour.settings": "设置 — 备份和恢复配置,管理自动备份。", + "tour.api": "API 文档 — 基于 Swagger 的交互式 REST API 文档。", "tour.search": "搜索 — 使用 Ctrl+K 快速查找并导航到任意实体。", "tour.theme": "主题 — 在深色和浅色模式之间切换。", + "tour.accent": "主题色 — 自定义界面的强调颜色。", "tour.language": "语言 — 选择您偏好的界面语言。", "tour.restart": "重新开始导览", "tour.dash.perf": "性能 — 实时 FPS 图表、延迟指标和轮询间隔控制。",