/** * Display picker lightbox — display selection for streams and tests. * Supports engine-specific displays (e.g. scrcpy → Android devices). */ import { _cachedDisplays, _displayPickerCallback, _displayPickerSelectedIndex, set_displayPickerCallback, set_displayPickerSelectedIndex, displaysCache, availableEngines, } from '../core/state.js'; import { t } from '../core/i18n.js'; import { fetchWithAuth } from '../core/api.js'; import { showToast } from '../core/ui.js'; /** Currently active engine type for the picker (null = desktop monitors). */ let _pickerEngineType = null; /** Check if an engine type has its own device list (for inline onclick use). */ window._engineHasOwnDisplays = (engineType) => !!(engineType && availableEngines.find(e => e.type === engineType)?.has_own_displays); export function openDisplayPicker(callback, selectedIndex, engineType = null) { set_displayPickerCallback(callback); set_displayPickerSelectedIndex((selectedIndex !== undefined && selectedIndex !== null && selectedIndex !== '') ? Number(selectedIndex) : null); _pickerEngineType = engineType || null; const lightbox = document.getElementById('display-picker-lightbox'); // Use "Select a Device" title for engines with own display lists (camera, scrcpy, etc.) const titleEl = lightbox.querySelector('.display-picker-title'); if (titleEl) { titleEl.textContent = t(_pickerEngineType ? 'displays.picker.title.device' : 'displays.picker.title'); } lightbox.classList.add('active'); requestAnimationFrame(() => { // Always fetch fresh when engine type is specified (different list each time) if (_pickerEngineType) { _fetchAndRenderEngineDisplays(_pickerEngineType); } else if (_cachedDisplays && _cachedDisplays.length > 0) { renderDisplayPickerLayout(_cachedDisplays); } else { const canvas = document.getElementById('display-picker-canvas'); canvas.innerHTML = '
'; displaysCache.fetch().then(displays => { if (displays && displays.length > 0) { renderDisplayPickerLayout(displays); } else { canvas.innerHTML = `
${t('displays.none')}
`; } }); } }); } async function _fetchAndRenderEngineDisplays(engineType) { const canvas = document.getElementById('display-picker-canvas'); canvas.innerHTML = '
'; try { const resp = await fetchWithAuth(`/config/displays?engine_type=${engineType}`); if (!resp.ok) throw new Error(`${resp.status}`); const data = await resp.json(); const displays = data.displays || []; // Store in cache so selectDisplay() can look them up displaysCache.update(displays); if (displays.length > 0) { renderDisplayPickerLayout(displays, engineType); } else if (engineType === 'scrcpy') { _renderEmptyAndroidPicker(canvas); } else { canvas.innerHTML = `
${t('displays.none')}
`; } } catch (error) { console.error('Error fetching engine displays:', error); canvas.innerHTML = `
${t('displays.failed')}
`; } } function _renderEmptyAndroidPicker(canvas) { canvas.innerHTML = `
${t('displays.picker.no_android')}
${_buildAdbConnectHtml()} `; } function _buildAdbConnectHtml() { return `
`; } /** Called from the inline Connect button inside the display picker. */ window._adbConnectFromPicker = async function () { const input = document.getElementById('adb-connect-ip'); if (!input) return; const address = input.value.trim(); if (!address) return; input.disabled = true; try { const resp = await fetchWithAuth('/adb/connect', { method: 'POST', body: JSON.stringify({ address }), }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || 'Connection failed'); } showToast(t('displays.picker.adb_connect.success'), 'success'); // Refresh the picker with updated device list if (_pickerEngineType) { await _fetchAndRenderEngineDisplays(_pickerEngineType); } } catch (error) { showToast(`${t('displays.picker.adb_connect.error')}: ${error.message}`, 'error'); } finally { if (input) input.disabled = false; } }; export function closeDisplayPicker(event) { if (event && event.target && event.target.closest('.display-picker-content')) return; const lightbox = document.getElementById('display-picker-lightbox'); lightbox.classList.remove('active'); set_displayPickerCallback(null); _pickerEngineType = null; } export function selectDisplay(displayIndex) { // Re-read live bindings import('../core/state.js').then(({ _displayPickerCallback: cb, _cachedDisplays: displays }) => { if (cb) { const display = displays ? displays.find(d => d.index === displayIndex) : null; cb(displayIndex, display); } closeDisplayPicker(); }); } export function renderDisplayPickerLayout(displays, engineType = null) { const canvas = document.getElementById('display-picker-canvas'); if (!displays || displays.length === 0) { canvas.innerHTML = `
${t('displays.none')}
`; return; } // Engines with own displays (camera, scrcpy) → device list layout if (engineType) { const items = displays.map(display => { const isSelected = _displayPickerSelectedIndex !== null && display.index === _displayPickerSelectedIndex; return `
#${display.index}
${display.name} ${display.width}×${display.height} · ${display.refresh_rate} FPS
`; }).join(''); let html = `
${items}
`; if (engineType === 'scrcpy') { html += _buildAdbConnectHtml(); } canvas.innerHTML = html; return; } // Desktop monitors → positioned rectangle layout let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; displays.forEach(display => { minX = Math.min(minX, display.x); minY = Math.min(minY, display.y); maxX = Math.max(maxX, display.x + display.width); maxY = Math.max(maxY, display.y + display.height); }); const totalWidth = maxX - minX; const totalHeight = maxY - minY; const aspect = totalHeight / totalWidth; const displayElements = displays.map(display => { const leftPct = ((display.x - minX) / totalWidth) * 100; const topPct = ((display.y - minY) / totalHeight) * 100; const widthPct = (display.width / totalWidth) * 100; const heightPct = (display.height / totalHeight) * 100; const isSelected = _displayPickerSelectedIndex !== null && display.index === _displayPickerSelectedIndex; return `
(${display.x}, ${display.y})
#${display.index}
${display.name} ${display.width}×${display.height} ${display.refresh_rate}Hz
`; }).join(''); canvas.innerHTML = `
${displayElements}
`; } export function formatDisplayLabel(displayIndex, display, engineType = null) { if (display) { if (engineType && window._engineHasOwnDisplays(engineType)) { return `${display.name} (${display.width}×${display.height})`; } return `#${display.index}: ${display.width}×${display.height}${display.is_primary ? ' (' + t('displays.badge.primary') + ')' : ''}`; } return `Display ${displayIndex}`; }