Filter audio devices by engine type and update tutorial steps
- Add enumerate_devices_by_engine() returning per-engine device lists without cross-engine dedup so frontend can filter correctly - API /audio-devices now includes by_engine dict alongside flat list - Frontend caches per-engine data, filters device dropdown by selected template's engine_type, refreshes on template change - Reorder getting-started tutorial: add API docs and accent color steps - Fix tutorial trigger button focus outline persisting on step 2 - Use accent color variable for tutorial pulse ring animation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<option value="${val}">${escapeHtml(label)}</option>`;
|
||||
}).join('');
|
||||
if (devices.length === 0) {
|
||||
select.innerHTML = '<option value="-1:1">Default</option>';
|
||||
}
|
||||
_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 `<option value="${val}">${escapeHtml(d.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
if (devices.length === 0) {
|
||||
select.innerHTML = '<option value="-1:1">Default</option>';
|
||||
}
|
||||
|
||||
if (prevName) {
|
||||
const match = Array.from(select.options).find(o => o.textContent === prevName);
|
||||
if (match) select.value = match.value;
|
||||
}
|
||||
}
|
||||
|
||||
function _selectAudioDevice(deviceIndex, isLoopback) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user