Add WLED brightness control, auto-name dialogs, and UI fixes
- Brightness slider now reads/writes actual WLED device brightness via
new GET/PUT /devices/{id}/brightness endpoints (0-255 range)
- Cache last-known brightness to prevent slider jumping on card refresh
- Auto-generate names for stream, engine template, and PP template dialogs
- Fix sub-tabs not re-rendering on language change
- Add capture template icon (camera) to stream card pills
- Fix last calibration tutorial popup position
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -667,6 +667,60 @@ async def update_settings(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ===== WLED BRIGHTNESS ENDPOINT =====
|
||||||
|
|
||||||
|
@router.get("/api/v1/devices/{device_id}/brightness", tags=["Settings"])
|
||||||
|
async def get_device_brightness(
|
||||||
|
device_id: str,
|
||||||
|
_auth: AuthRequired,
|
||||||
|
store: DeviceStore = Depends(get_device_store),
|
||||||
|
):
|
||||||
|
"""Get current brightness from the WLED device."""
|
||||||
|
device = store.get_device(device_id)
|
||||||
|
if not device:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=5.0) as http_client:
|
||||||
|
resp = await http_client.get(f"{device.url}/json/state")
|
||||||
|
resp.raise_for_status()
|
||||||
|
state = resp.json()
|
||||||
|
bri = state.get("bri", 255)
|
||||||
|
return {"brightness": bri}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get WLED brightness for {device_id}: {e}")
|
||||||
|
raise HTTPException(status_code=502, detail=f"Failed to reach WLED device: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/v1/devices/{device_id}/brightness", tags=["Settings"])
|
||||||
|
async def set_device_brightness(
|
||||||
|
device_id: str,
|
||||||
|
body: dict,
|
||||||
|
_auth: AuthRequired,
|
||||||
|
store: DeviceStore = Depends(get_device_store),
|
||||||
|
):
|
||||||
|
"""Set brightness on the WLED device directly."""
|
||||||
|
device = store.get_device(device_id)
|
||||||
|
if not device:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
|
||||||
|
|
||||||
|
bri = body.get("brightness")
|
||||||
|
if bri is None or not isinstance(bri, int) or not 0 <= bri <= 255:
|
||||||
|
raise HTTPException(status_code=400, detail="brightness must be an integer 0-255")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=5.0) as http_client:
|
||||||
|
resp = await http_client.post(
|
||||||
|
f"{device.url}/json/state",
|
||||||
|
json={"bri": bri},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return {"brightness": bri}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to set WLED brightness for {device_id}: {e}")
|
||||||
|
raise HTTPException(status_code=502, detail=f"Failed to reach WLED device: {e}")
|
||||||
|
|
||||||
|
|
||||||
# ===== CALIBRATION ENDPOINTS =====
|
# ===== CALIBRATION ENDPOINTS =====
|
||||||
|
|
||||||
@router.get("/api/v1/devices/{device_id}/calibration", response_model=CalibrationSchema, tags=["Calibration"])
|
@router.get("/api/v1/devices/{device_id}/calibration", response_model=CalibrationSchema, tags=["Calibration"])
|
||||||
|
|||||||
@@ -219,9 +219,40 @@ function formatDisplayLabel(displayIndex, display) {
|
|||||||
return `Display ${displayIndex}`;
|
return `Display ${displayIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _streamNameManuallyEdited = false;
|
||||||
|
|
||||||
function onStreamDisplaySelected(displayIndex, display) {
|
function onStreamDisplaySelected(displayIndex, display) {
|
||||||
document.getElementById('stream-display-index').value = displayIndex;
|
document.getElementById('stream-display-index').value = displayIndex;
|
||||||
document.getElementById('stream-display-picker-label').textContent = formatDisplayLabel(displayIndex, display);
|
document.getElementById('stream-display-picker-label').textContent = formatDisplayLabel(displayIndex, display);
|
||||||
|
_autoGenerateStreamName();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _streamModalPPTemplates = [];
|
||||||
|
|
||||||
|
function _autoGenerateStreamName() {
|
||||||
|
if (_streamNameManuallyEdited) return;
|
||||||
|
if (document.getElementById('stream-id').value) return; // editing, not creating
|
||||||
|
const streamType = document.getElementById('stream-type').value;
|
||||||
|
const nameInput = document.getElementById('stream-name');
|
||||||
|
|
||||||
|
if (streamType === 'raw') {
|
||||||
|
const displayIndex = document.getElementById('stream-display-index').value;
|
||||||
|
const templateSelect = document.getElementById('stream-capture-template');
|
||||||
|
const templateName = templateSelect.selectedOptions[0]?.dataset?.name || '';
|
||||||
|
if (displayIndex === '' || !templateName) return;
|
||||||
|
nameInput.value = `D${displayIndex}_${templateName}`;
|
||||||
|
} else if (streamType === 'processed') {
|
||||||
|
const sourceSelect = document.getElementById('stream-source');
|
||||||
|
const sourceName = sourceSelect.selectedOptions[0]?.dataset?.name || '';
|
||||||
|
const ppTemplateId = document.getElementById('stream-pp-template').value;
|
||||||
|
const ppTemplate = _streamModalPPTemplates.find(t => t.id === ppTemplateId);
|
||||||
|
if (!sourceName) return;
|
||||||
|
if (ppTemplate && ppTemplate.name) {
|
||||||
|
nameInput.value = `${sourceName} (${ppTemplate.name})`;
|
||||||
|
} else {
|
||||||
|
nameInput.value = sourceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTestDisplaySelected(displayIndex, display) {
|
function onTestDisplaySelected(displayIndex, display) {
|
||||||
@@ -352,6 +383,7 @@ function updateAllText() {
|
|||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
loadDisplays();
|
loadDisplays();
|
||||||
loadDevices();
|
loadDevices();
|
||||||
|
loadPictureStreams();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,9 +676,10 @@ async function loadDevices() {
|
|||||||
webuiLink.rel = 'noopener';
|
webuiLink.rel = 'noopener';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach event listeners
|
// Attach event listeners and fetch real WLED brightness
|
||||||
devicesWithState.forEach(device => {
|
devicesWithState.forEach(device => {
|
||||||
attachDeviceListeners(device.id);
|
attachDeviceListeners(device.id);
|
||||||
|
fetchDeviceBrightness(device.id);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load devices:', error);
|
console.error('Failed to load devices:', error);
|
||||||
@@ -661,7 +694,6 @@ function createDeviceCard(device) {
|
|||||||
const settings = device.settings || {};
|
const settings = device.settings || {};
|
||||||
|
|
||||||
const isProcessing = state.processing || false;
|
const isProcessing = state.processing || false;
|
||||||
const brightnessPercent = Math.round((settings.brightness !== undefined ? settings.brightness : 1.0) * 100);
|
|
||||||
const statusKey = isProcessing ? 'device.status.processing' : 'device.status.idle';
|
const statusKey = isProcessing ? 'device.status.processing' : 'device.status.idle';
|
||||||
const status = isProcessing ? 'processing' : 'idle';
|
const status = isProcessing ? 'processing' : 'idle';
|
||||||
|
|
||||||
@@ -733,11 +765,11 @@ function createDeviceCard(device) {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="brightness-control">
|
<div class="brightness-control">
|
||||||
<input type="range" class="brightness-slider" min="0" max="100"
|
<input type="range" class="brightness-slider" min="0" max="255"
|
||||||
value="${brightnessPercent}"
|
value="${_deviceBrightnessCache[device.id] ?? 128}" data-device-brightness="${device.id}"
|
||||||
oninput="updateBrightnessLabel('${device.id}', this.value)"
|
oninput="updateBrightnessLabel('${device.id}', this.value)"
|
||||||
onchange="saveCardBrightness('${device.id}', this.value)"
|
onchange="saveCardBrightness('${device.id}', this.value)"
|
||||||
title="${brightnessPercent}%">
|
title="${_deviceBrightnessCache[device.id] != null ? Math.round(_deviceBrightnessCache[device.id] / 255 * 100) + '%' : '...'}">
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
@@ -1163,19 +1195,23 @@ async function saveCaptureSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card brightness controls
|
// Brightness cache: stores last known WLED brightness per device (0-255)
|
||||||
|
const _deviceBrightnessCache = {};
|
||||||
|
|
||||||
|
// Card brightness controls — talks directly to WLED device
|
||||||
function updateBrightnessLabel(deviceId, value) {
|
function updateBrightnessLabel(deviceId, value) {
|
||||||
const slider = document.querySelector(`[data-device-id="${deviceId}"] .brightness-slider`);
|
const slider = document.querySelector(`[data-device-brightness="${deviceId}"]`);
|
||||||
if (slider) slider.title = value + '%';
|
if (slider) slider.title = Math.round(parseInt(value) / 255 * 100) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCardBrightness(deviceId, value) {
|
async function saveCardBrightness(deviceId, value) {
|
||||||
const brightness = parseInt(value) / 100.0;
|
const bri = parseInt(value);
|
||||||
|
_deviceBrightnessCache[deviceId] = bri;
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_BASE}/devices/${deviceId}/settings`, {
|
await fetch(`${API_BASE}/devices/${deviceId}/brightness`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: getHeaders(),
|
headers: getHeaders(),
|
||||||
body: JSON.stringify({ brightness })
|
body: JSON.stringify({ brightness: bri })
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update brightness:', err);
|
console.error('Failed to update brightness:', err);
|
||||||
@@ -1183,6 +1219,24 @@ async function saveCardBrightness(deviceId, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchDeviceBrightness(deviceId) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${API_BASE}/devices/${deviceId}/brightness`, {
|
||||||
|
headers: getHeaders()
|
||||||
|
});
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const data = await resp.json();
|
||||||
|
_deviceBrightnessCache[deviceId] = data.brightness;
|
||||||
|
const slider = document.querySelector(`[data-device-brightness="${deviceId}"]`);
|
||||||
|
if (slider) {
|
||||||
|
slider.value = data.brightness;
|
||||||
|
slider.title = Math.round(data.brightness / 255 * 100) + '%';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silently fail — device may be offline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add device modal
|
// Add device modal
|
||||||
function showAddDevice() {
|
function showAddDevice() {
|
||||||
const modal = document.getElementById('add-device-modal');
|
const modal = document.getElementById('add-device-modal');
|
||||||
@@ -2225,7 +2279,7 @@ const calibrationTutorialSteps = [
|
|||||||
{ selector: '.offset-control', textKey: 'calibration.tip.offset', position: 'bottom' },
|
{ selector: '.offset-control', textKey: 'calibration.tip.offset', position: 'bottom' },
|
||||||
{ selector: '.edge-span-bar[data-edge="top"]', textKey: 'calibration.tip.span', position: 'bottom' },
|
{ selector: '.edge-span-bar[data-edge="top"]', textKey: 'calibration.tip.span', position: 'bottom' },
|
||||||
{ selector: '.toggle-top', textKey: 'calibration.tip.test', position: 'top' },
|
{ selector: '.toggle-top', textKey: 'calibration.tip.test', position: 'top' },
|
||||||
{ selector: '.preview-screen-total', textKey: 'calibration.tip.toggle_inputs', position: 'bottom' }
|
{ selector: '.preview-screen-total', textKey: 'calibration.tip.toggle_inputs', position: 'top' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const deviceTutorialSteps = [
|
const deviceTutorialSteps = [
|
||||||
@@ -2467,6 +2521,8 @@ function getEngineIcon(engineType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show add template modal
|
// Show add template modal
|
||||||
|
let _templateNameManuallyEdited = false;
|
||||||
|
|
||||||
async function showAddTemplateModal() {
|
async function showAddTemplateModal() {
|
||||||
currentEditingTemplateId = null;
|
currentEditingTemplateId = null;
|
||||||
document.getElementById('template-modal-title').textContent = t('templates.add');
|
document.getElementById('template-modal-title').textContent = t('templates.add');
|
||||||
@@ -2475,6 +2531,10 @@ async function showAddTemplateModal() {
|
|||||||
document.getElementById('engine-config-section').style.display = 'none';
|
document.getElementById('engine-config-section').style.display = 'none';
|
||||||
document.getElementById('template-error').style.display = 'none';
|
document.getElementById('template-error').style.display = 'none';
|
||||||
|
|
||||||
|
// Auto-name: reset flag and wire listener
|
||||||
|
_templateNameManuallyEdited = false;
|
||||||
|
document.getElementById('template-name').oninput = () => { _templateNameManuallyEdited = true; };
|
||||||
|
|
||||||
// Load available engines
|
// Load available engines
|
||||||
await loadAvailableEngines();
|
await loadAvailableEngines();
|
||||||
|
|
||||||
@@ -2738,6 +2798,11 @@ async function onEngineChange() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-name: set template name to engine name if user hasn't edited
|
||||||
|
if (!_templateNameManuallyEdited && !document.getElementById('template-id').value) {
|
||||||
|
document.getElementById('template-name').value = engine.name || engineType;
|
||||||
|
}
|
||||||
|
|
||||||
// Show availability hint
|
// Show availability hint
|
||||||
const hint = document.getElementById('engine-availability-hint');
|
const hint = document.getElementById('engine-availability-hint');
|
||||||
if (!engine.available) {
|
if (!engine.available) {
|
||||||
@@ -3069,7 +3134,7 @@ function renderPictureStreamsList(streams) {
|
|||||||
detailsHtml = `<div class="stream-card-props">
|
detailsHtml = `<div class="stream-card-props">
|
||||||
<span class="stream-card-prop" title="${t('streams.display')}">🖥️ ${stream.display_index ?? 0}</span>
|
<span class="stream-card-prop" title="${t('streams.display')}">🖥️ ${stream.display_index ?? 0}</span>
|
||||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">⚡ ${stream.target_fps ?? 30}</span>
|
<span class="stream-card-prop" title="${t('streams.target_fps')}">⚡ ${stream.target_fps ?? 30}</span>
|
||||||
${capTmplName ? `<span class="stream-card-prop" title="${t('streams.capture_template')}">${capTmplName}</span>` : ''}
|
${capTmplName ? `<span class="stream-card-prop" title="${t('streams.capture_template')}">📷 ${capTmplName}</span>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (stream.stream_type === 'processed') {
|
} else if (stream.stream_type === 'processed') {
|
||||||
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
||||||
@@ -3284,6 +3349,13 @@ async function showAddStreamModal(presetType) {
|
|||||||
imgSrcInput.onpaste = () => setTimeout(() => validateStaticImage(), 0);
|
imgSrcInput.onpaste = () => setTimeout(() => validateStaticImage(), 0);
|
||||||
onStreamTypeChange();
|
onStreamTypeChange();
|
||||||
|
|
||||||
|
// Auto-name: reset flag and wire listeners
|
||||||
|
_streamNameManuallyEdited = false;
|
||||||
|
document.getElementById('stream-name').oninput = () => { _streamNameManuallyEdited = true; };
|
||||||
|
document.getElementById('stream-capture-template').onchange = () => _autoGenerateStreamName();
|
||||||
|
document.getElementById('stream-source').onchange = () => _autoGenerateStreamName();
|
||||||
|
document.getElementById('stream-pp-template').onchange = () => _autoGenerateStreamName();
|
||||||
|
|
||||||
// Populate dropdowns
|
// Populate dropdowns
|
||||||
await populateStreamModalDropdowns();
|
await populateStreamModalDropdowns();
|
||||||
|
|
||||||
@@ -3379,6 +3451,7 @@ async function populateStreamModalDropdowns() {
|
|||||||
(data.templates || []).forEach(tmpl => {
|
(data.templates || []).forEach(tmpl => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = tmpl.id;
|
opt.value = tmpl.id;
|
||||||
|
opt.dataset.name = tmpl.name;
|
||||||
opt.textContent = `${getEngineIcon(tmpl.engine_type)} ${tmpl.name}`;
|
opt.textContent = `${getEngineIcon(tmpl.engine_type)} ${tmpl.name}`;
|
||||||
templateSelect.appendChild(opt);
|
templateSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
@@ -3395,6 +3468,7 @@ async function populateStreamModalDropdowns() {
|
|||||||
if (s.id === editingId) return;
|
if (s.id === editingId) return;
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = s.id;
|
opt.value = s.id;
|
||||||
|
opt.dataset.name = s.name;
|
||||||
const typeLabels = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
const typeLabels = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
||||||
const typeLabel = typeLabels[s.stream_type] || '📺';
|
const typeLabel = typeLabels[s.stream_type] || '📺';
|
||||||
opt.textContent = `${typeLabel} ${s.name}`;
|
opt.textContent = `${typeLabel} ${s.name}`;
|
||||||
@@ -3403,17 +3477,22 @@ async function populateStreamModalDropdowns() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PP templates
|
// PP templates
|
||||||
|
_streamModalPPTemplates = [];
|
||||||
const ppSelect = document.getElementById('stream-pp-template');
|
const ppSelect = document.getElementById('stream-pp-template');
|
||||||
ppSelect.innerHTML = '';
|
ppSelect.innerHTML = '';
|
||||||
if (ppTemplatesRes.ok) {
|
if (ppTemplatesRes.ok) {
|
||||||
const data = await ppTemplatesRes.json();
|
const data = await ppTemplatesRes.json();
|
||||||
(data.templates || []).forEach(tmpl => {
|
_streamModalPPTemplates = data.templates || [];
|
||||||
|
_streamModalPPTemplates.forEach(tmpl => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = tmpl.id;
|
opt.value = tmpl.id;
|
||||||
opt.textContent = tmpl.name;
|
opt.textContent = tmpl.name;
|
||||||
ppSelect.appendChild(opt);
|
ppSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger auto-name after dropdowns are populated
|
||||||
|
_autoGenerateStreamName();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveStream() {
|
async function saveStream() {
|
||||||
@@ -3854,6 +3933,7 @@ function addFilterFromSelect() {
|
|||||||
_modalFilters.push({ filter_id: filterId, options, _expanded: true });
|
_modalFilters.push({ filter_id: filterId, options, _expanded: true });
|
||||||
select.value = '';
|
select.value = '';
|
||||||
renderModalFilterList();
|
renderModalFilterList();
|
||||||
|
_autoGeneratePPTemplateName();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFilterExpand(index) {
|
function toggleFilterExpand(index) {
|
||||||
@@ -3866,6 +3946,7 @@ function toggleFilterExpand(index) {
|
|||||||
function removeFilter(index) {
|
function removeFilter(index) {
|
||||||
_modalFilters.splice(index, 1);
|
_modalFilters.splice(index, 1);
|
||||||
renderModalFilterList();
|
renderModalFilterList();
|
||||||
|
_autoGeneratePPTemplateName();
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveFilter(index, direction) {
|
function moveFilter(index, direction) {
|
||||||
@@ -3875,6 +3956,7 @@ function moveFilter(index, direction) {
|
|||||||
_modalFilters[index] = _modalFilters[newIndex];
|
_modalFilters[index] = _modalFilters[newIndex];
|
||||||
_modalFilters[newIndex] = tmp;
|
_modalFilters[newIndex] = tmp;
|
||||||
renderModalFilterList();
|
renderModalFilterList();
|
||||||
|
_autoGeneratePPTemplateName();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFilterOption(filterIndex, optionKey, value) {
|
function updateFilterOption(filterIndex, optionKey, value) {
|
||||||
@@ -3904,6 +3986,20 @@ function collectFilters() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ppTemplateNameManuallyEdited = false;
|
||||||
|
|
||||||
|
function _autoGeneratePPTemplateName() {
|
||||||
|
if (_ppTemplateNameManuallyEdited) return;
|
||||||
|
if (document.getElementById('pp-template-id').value) return; // editing, not creating
|
||||||
|
const nameInput = document.getElementById('pp-template-name');
|
||||||
|
if (_modalFilters.length > 0) {
|
||||||
|
const filterNames = _modalFilters.map(f => _getFilterName(f.filter_id)).join(' + ');
|
||||||
|
nameInput.value = filterNames;
|
||||||
|
} else {
|
||||||
|
nameInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function showAddPPTemplateModal() {
|
async function showAddPPTemplateModal() {
|
||||||
if (_availableFilters.length === 0) await loadAvailableFilters();
|
if (_availableFilters.length === 0) await loadAvailableFilters();
|
||||||
|
|
||||||
@@ -3914,6 +4010,10 @@ async function showAddPPTemplateModal() {
|
|||||||
|
|
||||||
_modalFilters = [];
|
_modalFilters = [];
|
||||||
|
|
||||||
|
// Auto-name: reset flag and wire listener
|
||||||
|
_ppTemplateNameManuallyEdited = false;
|
||||||
|
document.getElementById('pp-template-name').oninput = () => { _ppTemplateNameManuallyEdited = true; };
|
||||||
|
|
||||||
_populateFilterSelect();
|
_populateFilterSelect();
|
||||||
renderModalFilterList();
|
renderModalFilterList();
|
||||||
|
|
||||||
@@ -4141,7 +4241,7 @@ async function updateStreamSelectorInfo(streamId) {
|
|||||||
propsHtml = `
|
propsHtml = `
|
||||||
<span class="stream-card-prop" title="${t('streams.display')}">🖥️ ${stream.display_index ?? 0}</span>
|
<span class="stream-card-prop" title="${t('streams.display')}">🖥️ ${stream.display_index ?? 0}</span>
|
||||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">⚡ ${stream.target_fps ?? 30}</span>
|
<span class="stream-card-prop" title="${t('streams.target_fps')}">⚡ ${stream.target_fps ?? 30}</span>
|
||||||
${capTmplName ? `<span class="stream-card-prop" title="${t('streams.capture_template')}">${capTmplName}</span>` : ''}
|
${capTmplName ? `<span class="stream-card-prop" title="${t('streams.capture_template')}">📷 ${capTmplName}</span>` : ''}
|
||||||
`;
|
`;
|
||||||
} else if (stream.stream_type === 'processed') {
|
} else if (stream.stream_type === 'processed') {
|
||||||
if ((!_cachedStreams || _cachedStreams.length === 0) && stream.source_stream_id) {
|
if ((!_cachedStreams || _cachedStreams.length === 0) && stream.source_stream_id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user