Refactor capture engine architecture, rename PictureStream to PictureSource, and split API modules
- Separate CaptureEngine into stateless factory + stateful CaptureStream session - Add LiveStream/LiveStreamManager for shared capture with reference counting - Rename PictureStream to PictureSource across storage, API, and UI - Remove legacy migration logic and unused compatibility code - Split monolithic routes.py (1935 lines) into 5 focused route modules - Split schemas.py (480 lines) into 7 schema modules with re-exports - Extract dependency injection into dedicated dependencies.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -91,7 +91,7 @@ function closeLightbox(event) {
|
||||
|
||||
async function openFullImageLightbox(imageSource) {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/picture-streams/full-image?source=${encodeURIComponent(imageSource)}`, {
|
||||
const resp = await fetch(`${API_BASE}/picture-sources/full-image?source=${encodeURIComponent(imageSource)}`, {
|
||||
headers: getHeaders()
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
@@ -402,7 +402,7 @@ function updateAllText() {
|
||||
if (apiKey) {
|
||||
loadDisplays();
|
||||
loadDevices();
|
||||
loadPictureStreams();
|
||||
loadPictureSources();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,15 +576,11 @@ async function loadDisplays() {
|
||||
let _cachedDisplays = null;
|
||||
|
||||
function switchTab(name) {
|
||||
// Migrate legacy tab values from localStorage
|
||||
if (name === 'templates' || name === 'pp-templates') {
|
||||
name = 'streams';
|
||||
}
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
|
||||
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
|
||||
localStorage.setItem('activeTab', name);
|
||||
if (name === 'streams') {
|
||||
loadPictureStreams();
|
||||
loadPictureSources();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2532,7 +2528,7 @@ async function loadCaptureTemplates() {
|
||||
const data = await response.json();
|
||||
_cachedCaptureTemplates = data.templates || [];
|
||||
// Re-render the streams tab which now contains template sections
|
||||
renderPictureStreamsList(_cachedStreams);
|
||||
renderPictureSourcesList(_cachedStreams);
|
||||
} catch (error) {
|
||||
console.error('Error loading capture templates:', error);
|
||||
}
|
||||
@@ -2540,7 +2536,7 @@ async function loadCaptureTemplates() {
|
||||
|
||||
// Get engine icon
|
||||
function getEngineIcon(engineType) {
|
||||
return '🖥️';
|
||||
return '🚀';
|
||||
}
|
||||
|
||||
// Show add template modal
|
||||
@@ -2786,7 +2782,7 @@ async function loadAvailableEngines() {
|
||||
availableEngines = data.engines || [];
|
||||
|
||||
const select = document.getElementById('template-engine');
|
||||
select.innerHTML = `<option value="">${t('templates.engine.select')}</option>`;
|
||||
select.innerHTML = '';
|
||||
|
||||
availableEngines.forEach(engine => {
|
||||
const option = document.createElement('option');
|
||||
@@ -2798,6 +2794,12 @@ async function loadAvailableEngines() {
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// Auto-select first available engine if nothing selected
|
||||
if (!select.value) {
|
||||
const firstAvailable = availableEngines.find(e => e.available);
|
||||
if (firstAvailable) select.value = firstAvailable.type;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading engines:', error);
|
||||
showToast(t('templates.error.engines') + ': ' + error.message, 'error');
|
||||
@@ -3085,14 +3087,14 @@ async function deleteTemplate(templateId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Picture Streams =====
|
||||
// ===== Picture Sources =====
|
||||
|
||||
let _cachedStreams = [];
|
||||
let _cachedPPTemplates = [];
|
||||
let _cachedCaptureTemplates = [];
|
||||
let _availableFilters = []; // Loaded from GET /filters
|
||||
|
||||
async function loadPictureStreams() {
|
||||
async function loadPictureSources() {
|
||||
try {
|
||||
// Always fetch templates, filters, and streams in parallel
|
||||
// since templates are now rendered inside stream sub-tabs
|
||||
@@ -3100,7 +3102,7 @@ async function loadPictureStreams() {
|
||||
_availableFilters.length === 0 ? fetchWithAuth('/filters') : Promise.resolve(null),
|
||||
fetchWithAuth('/postprocessing-templates'),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
fetchWithAuth('/picture-streams')
|
||||
fetchWithAuth('/picture-sources')
|
||||
]);
|
||||
|
||||
if (filtersResp && filtersResp.ok) {
|
||||
@@ -3120,9 +3122,9 @@ async function loadPictureStreams() {
|
||||
}
|
||||
const data = await streamsResp.json();
|
||||
_cachedStreams = data.streams || [];
|
||||
renderPictureStreamsList(_cachedStreams);
|
||||
renderPictureSourcesList(_cachedStreams);
|
||||
} catch (error) {
|
||||
console.error('Error loading picture streams:', error);
|
||||
console.error('Error loading picture sources:', error);
|
||||
document.getElementById('streams-list').innerHTML = `
|
||||
<div class="error-message">${t('streams.error.load')}: ${error.message}</div>
|
||||
`;
|
||||
@@ -3139,7 +3141,7 @@ function switchStreamTab(tabKey) {
|
||||
localStorage.setItem('activeStreamTab', tabKey);
|
||||
}
|
||||
|
||||
function renderPictureStreamsList(streams) {
|
||||
function renderPictureSourcesList(streams) {
|
||||
const container = document.getElementById('streams-list');
|
||||
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
||||
|
||||
@@ -3157,7 +3159,7 @@ function renderPictureStreamsList(streams) {
|
||||
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.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>`;
|
||||
} else if (stream.stream_type === 'processed') {
|
||||
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
||||
@@ -3169,7 +3171,7 @@ function renderPictureStreamsList(streams) {
|
||||
}
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('streams.source')}">📺 ${sourceName}</span>
|
||||
${ppTmplName ? `<span class="stream-card-prop" title="${t('streams.pp_template')}">🎨 ${ppTmplName}</span>` : ''}
|
||||
${ppTmplName ? `<span class="stream-card-prop" title="${t('streams.pp_template')}">📋 ${ppTmplName}</span>` : ''}
|
||||
</div>`;
|
||||
} else if (stream.stream_type === 'static_image') {
|
||||
const src = stream.image_source || '';
|
||||
@@ -3208,12 +3210,12 @@ function renderPictureStreamsList(streams) {
|
||||
<button class="card-remove-btn" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">
|
||||
${engineIcon} ${escapeHtml(template.name)}
|
||||
📋 ${escapeHtml(template.name)}
|
||||
</div>
|
||||
</div>
|
||||
${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
|
||||
<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('templates.engine')}">⚙️ ${template.engine_type.toUpperCase()}</span>
|
||||
<span class="stream-card-prop" title="${t('templates.engine')}">🚀 ${template.engine_type.toUpperCase()}</span>
|
||||
${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('templates.config.show')}">🔧 ${configEntries.length}</span>` : ''}
|
||||
</div>
|
||||
${configEntries.length > 0 ? `
|
||||
@@ -3252,7 +3254,7 @@ function renderPictureStreamsList(streams) {
|
||||
<button class="card-remove-btn" onclick="deletePPTemplate('${tmpl.id}')" title="${t('common.delete')}">✕</button>
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">
|
||||
🎨 ${escapeHtml(tmpl.name)}
|
||||
📋 ${escapeHtml(tmpl.name)}
|
||||
</div>
|
||||
</div>
|
||||
${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
|
||||
@@ -3390,7 +3392,7 @@ async function showAddStreamModal(presetType) {
|
||||
|
||||
async function editStream(streamId) {
|
||||
try {
|
||||
const response = await fetchWithAuth(`/picture-streams/${streamId}`);
|
||||
const response = await fetchWithAuth(`/picture-sources/${streamId}`);
|
||||
if (!response.ok) throw new Error(`Failed to load stream: ${response.status}`);
|
||||
const stream = await response.json();
|
||||
|
||||
@@ -3450,7 +3452,7 @@ async function populateStreamModalDropdowns() {
|
||||
const [displaysRes, captureTemplatesRes, streamsRes, ppTemplatesRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
fetchWithAuth('/picture-streams'),
|
||||
fetchWithAuth('/picture-sources'),
|
||||
fetchWithAuth('/postprocessing-templates'),
|
||||
]);
|
||||
|
||||
@@ -3556,12 +3558,12 @@ async function saveStream() {
|
||||
try {
|
||||
let response;
|
||||
if (streamId) {
|
||||
response = await fetchWithAuth(`/picture-streams/${streamId}`, {
|
||||
response = await fetchWithAuth(`/picture-sources/${streamId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
} else {
|
||||
response = await fetchWithAuth('/picture-streams', {
|
||||
response = await fetchWithAuth('/picture-sources', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
@@ -3574,7 +3576,7 @@ async function saveStream() {
|
||||
|
||||
showToast(streamId ? t('streams.updated') : t('streams.created'), 'success');
|
||||
closeStreamModal();
|
||||
await loadPictureStreams();
|
||||
await loadPictureSources();
|
||||
} catch (error) {
|
||||
console.error('Error saving stream:', error);
|
||||
errorEl.textContent = error.message;
|
||||
@@ -3587,7 +3589,7 @@ async function deleteStream(streamId) {
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/picture-streams/${streamId}`, {
|
||||
const response = await fetchWithAuth(`/picture-sources/${streamId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
@@ -3597,7 +3599,7 @@ async function deleteStream(streamId) {
|
||||
}
|
||||
|
||||
showToast(t('streams.deleted'), 'success');
|
||||
await loadPictureStreams();
|
||||
await loadPictureSources();
|
||||
} catch (error) {
|
||||
console.error('Error deleting stream:', error);
|
||||
showToast(t('streams.error.delete') + ': ' + error.message, 'error');
|
||||
@@ -3635,7 +3637,7 @@ async function validateStaticImage() {
|
||||
previewContainer.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/picture-streams/validate-image', {
|
||||
const response = await fetchWithAuth('/picture-sources/validate-image', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ image_source: source }),
|
||||
});
|
||||
@@ -3662,7 +3664,7 @@ async function validateStaticImage() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Picture Stream Test =====
|
||||
// ===== Picture Source Test =====
|
||||
|
||||
let _currentTestStreamId = null;
|
||||
|
||||
@@ -3701,7 +3703,7 @@ async function runStreamTest() {
|
||||
showOverlaySpinner(t('streams.test.running'), captureDuration);
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/picture-streams/${_currentTestStreamId}/test`, {
|
||||
const response = await fetchWithAuth(`/picture-sources/${_currentTestStreamId}/test`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ capture_duration: captureDuration })
|
||||
});
|
||||
@@ -3740,7 +3742,7 @@ async function showTestPPTemplateModal(templateId) {
|
||||
// Ensure streams are cached
|
||||
if (_cachedStreams.length === 0) {
|
||||
try {
|
||||
const resp = await fetchWithAuth('/picture-streams');
|
||||
const resp = await fetchWithAuth('/picture-sources');
|
||||
if (resp.ok) { const d = await resp.json(); _cachedStreams = d.streams || []; }
|
||||
} catch (e) { console.warn('Could not load streams for PP test:', e); }
|
||||
}
|
||||
@@ -3842,7 +3844,7 @@ async function loadPPTemplates() {
|
||||
const data = await response.json();
|
||||
_cachedPPTemplates = data.templates || [];
|
||||
// Re-render the streams tab which now contains template sections
|
||||
renderPictureStreamsList(_cachedStreams);
|
||||
renderPictureSourcesList(_cachedStreams);
|
||||
} catch (error) {
|
||||
console.error('Error loading PP templates:', error);
|
||||
}
|
||||
@@ -4163,7 +4165,7 @@ async function showStreamSelector(deviceId) {
|
||||
try {
|
||||
const [deviceResponse, streamsResponse, settingsResponse] = await Promise.all([
|
||||
fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/picture-streams'),
|
||||
fetchWithAuth('/picture-sources'),
|
||||
fetch(`${API_BASE}/devices/${deviceId}/settings`, { headers: getHeaders() }),
|
||||
]);
|
||||
|
||||
@@ -4195,7 +4197,7 @@ async function showStreamSelector(deviceId) {
|
||||
});
|
||||
}
|
||||
|
||||
const currentStreamId = device.picture_stream_id || '';
|
||||
const currentStreamId = device.picture_source_id || '';
|
||||
streamSelect.value = currentStreamId;
|
||||
|
||||
// Populate LED projection fields
|
||||
@@ -4238,7 +4240,7 @@ async function updateStreamSelectorInfo(streamId) {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth(`/picture-streams/${streamId}`);
|
||||
const response = await fetchWithAuth(`/picture-sources/${streamId}`);
|
||||
if (!response.ok) {
|
||||
infoPanel.style.display = 'none';
|
||||
return;
|
||||
@@ -4266,12 +4268,12 @@ async function updateStreamSelectorInfo(streamId) {
|
||||
propsHtml = `
|
||||
<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>
|
||||
${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') {
|
||||
if ((!_cachedStreams || _cachedStreams.length === 0) && stream.source_stream_id) {
|
||||
try {
|
||||
const streamsResp = await fetchWithAuth('/picture-streams');
|
||||
const streamsResp = await fetchWithAuth('/picture-sources');
|
||||
if (streamsResp.ok) { const d = await streamsResp.json(); _cachedStreams = d.streams || []; }
|
||||
} catch {}
|
||||
}
|
||||
@@ -4292,7 +4294,7 @@ async function updateStreamSelectorInfo(streamId) {
|
||||
}
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop" title="${t('streams.source')}">📺 ${sourceName}</span>
|
||||
${ppTmplName ? `<span class="stream-card-prop" title="${t('streams.pp_template')}">🎨 ${ppTmplName}</span>` : ''}
|
||||
${ppTmplName ? `<span class="stream-card-prop" title="${t('streams.pp_template')}">📋 ${ppTmplName}</span>` : ''}
|
||||
`;
|
||||
} else if (stream.stream_type === 'static_image') {
|
||||
const src = stream.image_source || '';
|
||||
@@ -4313,18 +4315,18 @@ async function updateStreamSelectorInfo(streamId) {
|
||||
|
||||
async function saveStreamSelector() {
|
||||
const deviceId = document.getElementById('stream-selector-device-id').value;
|
||||
const pictureStreamId = document.getElementById('stream-selector-stream').value;
|
||||
const pictureSourceId = document.getElementById('stream-selector-stream').value;
|
||||
const borderWidth = parseInt(document.getElementById('stream-selector-border-width').value) || 10;
|
||||
const interpolation = document.getElementById('stream-selector-interpolation').value;
|
||||
const smoothing = parseFloat(document.getElementById('stream-selector-smoothing').value);
|
||||
const errorEl = document.getElementById('stream-selector-error');
|
||||
|
||||
try {
|
||||
// Save picture stream assignment
|
||||
// Save picture source assignment
|
||||
const response = await fetch(`${API_BASE}/devices/${deviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ picture_stream_id: pictureStreamId })
|
||||
body: JSON.stringify({ picture_source_id: pictureSourceId })
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn active" data-tab="devices" onclick="switchTab('devices')"><span data-i18n="devices.title">💡 Devices</span></button>
|
||||
|
||||
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')"><span data-i18n="streams.title">📺 Streams</span></button>
|
||||
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')"><span data-i18n="streams.title">📺 Sources</span></button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel active" id="tab-devices">
|
||||
@@ -228,11 +228,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stream Settings Modal (picture stream + LED projection settings) -->
|
||||
<!-- Stream Settings Modal (picture source + LED projection settings) -->
|
||||
<div id="stream-selector-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 data-i18n="device.stream_settings.title">📺 Stream Settings</h2>
|
||||
<h2 data-i18n="device.stream_settings.title">📺 Source Settings</h2>
|
||||
<button class="modal-close-btn" onclick="closeStreamSelectorModal()" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -241,10 +241,10 @@
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="stream-selector-stream" data-i18n="device.stream_selector.label">Stream:</label>
|
||||
<label for="stream-selector-stream" data-i18n="device.stream_selector.label">Source:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="device.stream_selector.hint">Select a stream that defines what this device captures and processes</small>
|
||||
<small class="input-hint" style="display:none" data-i18n="device.stream_selector.hint">Select a source that defines what this device captures and processes</small>
|
||||
<select id="stream-selector-stream"></select>
|
||||
<div id="stream-selector-info" class="stream-info-panel" style="display: none;"></div>
|
||||
</div>
|
||||
@@ -405,7 +405,6 @@
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="templates.engine.hint">Select the screen capture technology to use</small>
|
||||
<select id="template-engine" onchange="onEngineChange()" required>
|
||||
<option value="" data-i18n="templates.engine.select">Select an engine...</option>
|
||||
</select>
|
||||
<small id="engine-availability-hint" class="form-hint" style="display: none;"></small>
|
||||
</div>
|
||||
@@ -457,11 +456,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Stream Modal -->
|
||||
<!-- Test Source Modal -->
|
||||
<div id="test-stream-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 data-i18n="streams.test.title">Test Stream</h2>
|
||||
<h2 data-i18n="streams.test.title">Test Source</h2>
|
||||
<button class="modal-close-btn" onclick="closeTestStreamModal()" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -490,7 +489,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label data-i18n="postprocessing.test.source_stream">Source Stream:</label>
|
||||
<label data-i18n="postprocessing.test.source_stream">Source:</label>
|
||||
<select id="test-pp-source-stream"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -509,24 +508,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stream Modal -->
|
||||
<!-- Source Modal -->
|
||||
<div id="stream-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="stream-modal-title" data-i18n="streams.add">Add Stream</h2>
|
||||
<h2 id="stream-modal-title" data-i18n="streams.add">Add Source</h2>
|
||||
<button class="modal-close-btn" onclick="closeStreamModal()" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="stream-id">
|
||||
<form id="stream-form">
|
||||
<div class="form-group">
|
||||
<label for="stream-name" data-i18n="streams.name">Stream Name:</label>
|
||||
<input type="text" id="stream-name" data-i18n-placeholder="streams.name.placeholder" placeholder="My Stream" required>
|
||||
<label for="stream-name" data-i18n="streams.name">Source Name:</label>
|
||||
<input type="text" id="stream-name" data-i18n-placeholder="streams.name.placeholder" placeholder="My Source" required>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="stream-type" value="raw">
|
||||
|
||||
<!-- Raw stream fields -->
|
||||
<!-- Raw source fields -->
|
||||
<div id="stream-raw-fields">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
@@ -560,14 +559,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processed stream fields -->
|
||||
<!-- Processed source fields -->
|
||||
<div id="stream-processed-fields" style="display: none;">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="stream-source" data-i18n="streams.source">Source Stream:</label>
|
||||
<label for="stream-source" data-i18n="streams.source">Source:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="streams.source.hint">The stream to apply processing filters to</small>
|
||||
<small class="input-hint" style="display:none" data-i18n="streams.source.hint">The source to apply processing filters to</small>
|
||||
<select id="stream-source"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -575,7 +574,7 @@
|
||||
<label for="stream-pp-template" data-i18n="streams.pp_template">Processing Template:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="streams.pp_template.hint">Filter template to apply to the source stream</small>
|
||||
<small class="input-hint" style="display:none" data-i18n="streams.pp_template.hint">Filter template to apply to the source</small>
|
||||
<select id="stream-pp-template"></select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -599,7 +598,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="stream-description" data-i18n="streams.description_label">Description (optional):</label>
|
||||
<input type="text" id="stream-description" data-i18n-placeholder="streams.description_placeholder" placeholder="Describe this stream...">
|
||||
<input type="text" id="stream-description" data-i18n-placeholder="streams.description_placeholder" placeholder="Describe this source...">
|
||||
</div>
|
||||
|
||||
<div id="stream-error" class="error-message" style="display: none;"></div>
|
||||
|
||||
@@ -199,19 +199,19 @@
|
||||
"confirm.no": "No",
|
||||
"common.delete": "Delete",
|
||||
"common.edit": "Edit",
|
||||
"streams.title": "\uD83D\uDCFA Streams",
|
||||
"streams.description": "Streams define the capture pipeline. A raw stream captures from a display using a capture template. A processed stream applies postprocessing to another stream. Assign streams to devices.",
|
||||
"streams.title": "\uD83D\uDCFA Sources",
|
||||
"streams.description": "Sources define the capture pipeline. A raw source captures from a display using a capture template. A processed source applies postprocessing to another source. Assign sources to devices.",
|
||||
"streams.group.raw": "Screen Capture",
|
||||
"streams.group.processed": "Processed",
|
||||
"streams.section.streams": "\uD83D\uDCFA Streams",
|
||||
"streams.add": "Add Stream",
|
||||
"streams.section.streams": "\uD83D\uDCFA Sources",
|
||||
"streams.add": "Add Source",
|
||||
"streams.add.raw": "Add Screen Capture",
|
||||
"streams.add.processed": "Add Processed Stream",
|
||||
"streams.edit": "Edit Stream",
|
||||
"streams.add.processed": "Add Processed Source",
|
||||
"streams.edit": "Edit Source",
|
||||
"streams.edit.raw": "Edit Screen Capture",
|
||||
"streams.edit.processed": "Edit Processed Stream",
|
||||
"streams.name": "Stream Name:",
|
||||
"streams.name.placeholder": "My Stream",
|
||||
"streams.edit.processed": "Edit Processed Source",
|
||||
"streams.name": "Source Name:",
|
||||
"streams.name.placeholder": "My Source",
|
||||
"streams.type": "Type:",
|
||||
"streams.type.raw": "Screen Capture",
|
||||
"streams.type.processed": "Processed",
|
||||
@@ -221,26 +221,26 @@
|
||||
"streams.capture_template.hint": "Engine template defining how the screen is captured",
|
||||
"streams.target_fps": "Target FPS:",
|
||||
"streams.target_fps.hint": "Target frames per second for capture (10-90)",
|
||||
"streams.source": "Source Stream:",
|
||||
"streams.source.hint": "The stream to apply processing filters to",
|
||||
"streams.source": "Source:",
|
||||
"streams.source.hint": "The source to apply processing filters to",
|
||||
"streams.pp_template": "Filter Template:",
|
||||
"streams.pp_template.hint": "Filter template to apply to the source stream",
|
||||
"streams.pp_template.hint": "Filter template to apply to the source",
|
||||
"streams.description_label": "Description (optional):",
|
||||
"streams.description_placeholder": "Describe this stream...",
|
||||
"streams.created": "Stream created successfully",
|
||||
"streams.updated": "Stream updated successfully",
|
||||
"streams.deleted": "Stream deleted successfully",
|
||||
"streams.delete.confirm": "Are you sure you want to delete this stream?",
|
||||
"streams.error.load": "Failed to load streams",
|
||||
"streams.description_placeholder": "Describe this source...",
|
||||
"streams.created": "Source created successfully",
|
||||
"streams.updated": "Source updated successfully",
|
||||
"streams.deleted": "Source deleted successfully",
|
||||
"streams.delete.confirm": "Are you sure you want to delete this source?",
|
||||
"streams.error.load": "Failed to load sources",
|
||||
"streams.error.required": "Please fill in all required fields",
|
||||
"streams.error.delete": "Failed to delete stream",
|
||||
"streams.test.title": "Test Stream",
|
||||
"streams.error.delete": "Failed to delete source",
|
||||
"streams.test.title": "Test Source",
|
||||
"streams.test.run": "🧪 Run",
|
||||
"streams.test.running": "Testing stream...",
|
||||
"streams.test.running": "Testing source...",
|
||||
"streams.test.duration": "Capture Duration (s):",
|
||||
"streams.test.error.failed": "Stream test failed",
|
||||
"streams.test.error.failed": "Source test failed",
|
||||
"postprocessing.title": "\uD83D\uDCC4 Filter Templates",
|
||||
"postprocessing.description": "Processing templates define image filters and color correction. Assign them to processed picture streams for consistent postprocessing across devices.",
|
||||
"postprocessing.description": "Processing templates define image filters and color correction. Assign them to processed picture sources for consistent postprocessing across devices.",
|
||||
"postprocessing.add": "Add Filter Template",
|
||||
"postprocessing.edit": "Edit Filter Template",
|
||||
"postprocessing.name": "Template Name:",
|
||||
@@ -269,16 +269,16 @@
|
||||
"postprocessing.error.delete": "Failed to delete processing template",
|
||||
"postprocessing.config.show": "Show settings",
|
||||
"postprocessing.test.title": "Test Filter Template",
|
||||
"postprocessing.test.source_stream": "Source Stream:",
|
||||
"postprocessing.test.source_stream": "Source:",
|
||||
"postprocessing.test.running": "Testing processing template...",
|
||||
"postprocessing.test.error.no_stream": "Please select a source stream",
|
||||
"postprocessing.test.error.no_stream": "Please select a source",
|
||||
"postprocessing.test.error.failed": "Processing template test failed",
|
||||
"device.button.stream_selector": "Stream Settings",
|
||||
"device.stream_settings.title": "📺 Stream Settings",
|
||||
"device.stream_selector.label": "Stream:",
|
||||
"device.stream_selector.hint": "Select a stream that defines what this device captures and processes",
|
||||
"device.stream_selector.none": "-- No stream assigned --",
|
||||
"device.stream_selector.saved": "Stream settings updated",
|
||||
"device.button.stream_selector": "Source Settings",
|
||||
"device.stream_settings.title": "📺 Source Settings",
|
||||
"device.stream_selector.label": "Source:",
|
||||
"device.stream_selector.hint": "Select a source that defines what this device captures and processes",
|
||||
"device.stream_selector.none": "-- No source assigned --",
|
||||
"device.stream_selector.saved": "Source settings updated",
|
||||
"device.stream_settings.border_width": "Border Width (px):",
|
||||
"device.stream_settings.border_width_hint": "How many pixels from the screen edge to sample for LED colors (1-100)",
|
||||
"device.stream_settings.interpolation": "Interpolation Mode:",
|
||||
@@ -288,10 +288,10 @@
|
||||
"device.stream_settings.interpolation_hint": "How to calculate LED color from sampled pixels",
|
||||
"device.stream_settings.smoothing": "Smoothing:",
|
||||
"device.stream_settings.smoothing_hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
|
||||
"device.tip.stream_selector": "Configure picture stream and LED projection settings for this device",
|
||||
"device.tip.stream_selector": "Configure picture source and LED projection settings for this device",
|
||||
"streams.group.static_image": "Static Image",
|
||||
"streams.add.static_image": "Add Static Image",
|
||||
"streams.edit.static_image": "Edit Static Image",
|
||||
"streams.add.static_image": "Add Static Image Source",
|
||||
"streams.edit.static_image": "Edit Static Image Source",
|
||||
"streams.type.static_image": "Static Image",
|
||||
"streams.image_source": "Image Source:",
|
||||
"streams.image_source.placeholder": "https://example.com/image.jpg or C:\\path\\to\\image.png",
|
||||
|
||||
@@ -199,19 +199,19 @@
|
||||
"confirm.no": "Нет",
|
||||
"common.delete": "Удалить",
|
||||
"common.edit": "Редактировать",
|
||||
"streams.title": "\uD83D\uDCFA Потоки",
|
||||
"streams.description": "Потоки определяют конвейер захвата. Сырой поток захватывает экран с помощью шаблона захвата. Обработанный поток применяет постобработку к другому потоку. Назначайте потоки устройствам.",
|
||||
"streams.title": "\uD83D\uDCFA Источники",
|
||||
"streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.",
|
||||
"streams.group.raw": "Захват Экрана",
|
||||
"streams.group.processed": "Обработанные",
|
||||
"streams.section.streams": "\uD83D\uDCFA Потоки",
|
||||
"streams.add": "Добавить Поток",
|
||||
"streams.section.streams": "\uD83D\uDCFA Источники",
|
||||
"streams.add": "Добавить Источник",
|
||||
"streams.add.raw": "Добавить Захват Экрана",
|
||||
"streams.add.processed": "Добавить Обработанный",
|
||||
"streams.edit": "Редактировать Поток",
|
||||
"streams.edit": "Редактировать Источник",
|
||||
"streams.edit.raw": "Редактировать Захват Экрана",
|
||||
"streams.edit.processed": "Редактировать Обработанный Поток",
|
||||
"streams.name": "Имя Потока:",
|
||||
"streams.name.placeholder": "Мой Поток",
|
||||
"streams.edit.processed": "Редактировать Обработанный Источник",
|
||||
"streams.name": "Имя Источника:",
|
||||
"streams.name.placeholder": "Мой Источник",
|
||||
"streams.type": "Тип:",
|
||||
"streams.type.raw": "Захват экрана",
|
||||
"streams.type.processed": "Обработанный",
|
||||
@@ -221,26 +221,26 @@
|
||||
"streams.capture_template.hint": "Шаблон движка, определяющий способ захвата экрана",
|
||||
"streams.target_fps": "Целевой FPS:",
|
||||
"streams.target_fps.hint": "Целевое количество кадров в секунду (10-90)",
|
||||
"streams.source": "Исходный Поток:",
|
||||
"streams.source.hint": "Поток, к которому применяются фильтры обработки",
|
||||
"streams.source": "Источник:",
|
||||
"streams.source.hint": "Источник, к которому применяются фильтры обработки",
|
||||
"streams.pp_template": "Шаблон Фильтра:",
|
||||
"streams.pp_template.hint": "Шаблон фильтра для применения к исходному потоку",
|
||||
"streams.pp_template.hint": "Шаблон фильтра для применения к источнику",
|
||||
"streams.description_label": "Описание (необязательно):",
|
||||
"streams.description_placeholder": "Опишите этот поток...",
|
||||
"streams.created": "Поток успешно создан",
|
||||
"streams.updated": "Поток успешно обновлён",
|
||||
"streams.deleted": "Поток успешно удалён",
|
||||
"streams.delete.confirm": "Вы уверены, что хотите удалить этот поток?",
|
||||
"streams.error.load": "Не удалось загрузить потоки",
|
||||
"streams.description_placeholder": "Опишите этот источник...",
|
||||
"streams.created": "Источник успешно создан",
|
||||
"streams.updated": "Источник успешно обновлён",
|
||||
"streams.deleted": "Источник успешно удалён",
|
||||
"streams.delete.confirm": "Вы уверены, что хотите удалить этот источник?",
|
||||
"streams.error.load": "Не удалось загрузить источники",
|
||||
"streams.error.required": "Пожалуйста, заполните все обязательные поля",
|
||||
"streams.error.delete": "Не удалось удалить поток",
|
||||
"streams.test.title": "Тест Потока",
|
||||
"streams.error.delete": "Не удалось удалить источник",
|
||||
"streams.test.title": "Тест Источника",
|
||||
"streams.test.run": "🧪 Запустить",
|
||||
"streams.test.running": "Тестирование потока...",
|
||||
"streams.test.running": "Тестирование источника...",
|
||||
"streams.test.duration": "Длительность Захвата (с):",
|
||||
"streams.test.error.failed": "Тест потока не удался",
|
||||
"streams.test.error.failed": "Тест источника не удался",
|
||||
"postprocessing.title": "\uD83D\uDCC4 Шаблоны Фильтров",
|
||||
"postprocessing.description": "Шаблоны обработки определяют фильтры изображений и цветокоррекцию. Назначайте их обработанным видеопотокам для единообразной постобработки на всех устройствах.",
|
||||
"postprocessing.description": "Шаблоны обработки определяют фильтры изображений и цветокоррекцию. Назначайте их обработанным источникам для единообразной постобработки на всех устройствах.",
|
||||
"postprocessing.add": "Добавить Шаблон Фильтра",
|
||||
"postprocessing.edit": "Редактировать Шаблон Фильтра",
|
||||
"postprocessing.name": "Имя Шаблона:",
|
||||
@@ -269,16 +269,16 @@
|
||||
"postprocessing.error.delete": "Не удалось удалить шаблон фильтра",
|
||||
"postprocessing.config.show": "Показать настройки",
|
||||
"postprocessing.test.title": "Тест шаблона фильтра",
|
||||
"postprocessing.test.source_stream": "Источник потока:",
|
||||
"postprocessing.test.source_stream": "Источник:",
|
||||
"postprocessing.test.running": "Тестирование шаблона фильтра...",
|
||||
"postprocessing.test.error.no_stream": "Пожалуйста, выберите источник потока",
|
||||
"postprocessing.test.error.no_stream": "Пожалуйста, выберите источник",
|
||||
"postprocessing.test.error.failed": "Тест шаблона фильтра не удался",
|
||||
"device.button.stream_selector": "Настройки потока",
|
||||
"device.stream_settings.title": "📺 Настройки потока",
|
||||
"device.stream_selector.label": "Поток:",
|
||||
"device.stream_selector.hint": "Выберите поток, определяющий что это устройство захватывает и обрабатывает",
|
||||
"device.stream_selector.none": "-- Поток не назначен --",
|
||||
"device.stream_selector.saved": "Настройки потока обновлены",
|
||||
"device.button.stream_selector": "Настройки источника",
|
||||
"device.stream_settings.title": "📺 Настройки источника",
|
||||
"device.stream_selector.label": "Источник:",
|
||||
"device.stream_selector.hint": "Выберите источник, определяющий что это устройство захватывает и обрабатывает",
|
||||
"device.stream_selector.none": "-- Источник не назначен --",
|
||||
"device.stream_selector.saved": "Настройки источника обновлены",
|
||||
"device.stream_settings.border_width": "Ширина границы (px):",
|
||||
"device.stream_settings.border_width_hint": "Сколько пикселей от края экрана выбирать для цвета LED (1-100)",
|
||||
"device.stream_settings.interpolation": "Режим интерполяции:",
|
||||
@@ -288,10 +288,10 @@
|
||||
"device.stream_settings.interpolation_hint": "Как вычислять цвет LED из выбранных пикселей",
|
||||
"device.stream_settings.smoothing": "Сглаживание:",
|
||||
"device.stream_settings.smoothing_hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.",
|
||||
"device.tip.stream_selector": "Настройки видеопотока и проекции LED для этого устройства",
|
||||
"device.tip.stream_selector": "Настройки источника и проекции LED для этого устройства",
|
||||
"streams.group.static_image": "Статические",
|
||||
"streams.add.static_image": "Добавить статическое изображение",
|
||||
"streams.edit.static_image": "Редактировать статическое изображение",
|
||||
"streams.add.static_image": "Добавить статическое изображение (источник)",
|
||||
"streams.edit.static_image": "Редактировать статическое изображение (источник)",
|
||||
"streams.type.static_image": "Статическое изображение",
|
||||
"streams.image_source": "Источник изображения:",
|
||||
"streams.image_source.placeholder": "https://example.com/image.jpg или C:\\path\\to\\image.png",
|
||||
|
||||
Reference in New Issue
Block a user