/** * Streams — Capture template CRUD, engine config, test modal. * Extracted from streams.ts to reduce file size. */ import { availableEngines, setAvailableEngines, currentEditingTemplateId, setCurrentEditingTemplateId, _templateNameManuallyEdited, set_templateNameManuallyEdited, currentTestingTemplate, setCurrentTestingTemplate, _cachedStreams, _cachedDisplays, captureTemplatesCache, displaysCache, apiKey, } from '../core/state.ts'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.ts'; import { t } from '../core/i18n.ts'; import { Modal } from '../core/modal.ts'; import { showToast, showConfirm, openLightbox, showOverlaySpinner, hideOverlaySpinner, updateOverlayPreview, setupBackdropClose } from '../core/ui.ts'; import { openDisplayPicker, formatDisplayLabel } from './displays.ts'; import { getEngineIcon, ICON_CAPTURE_TEMPLATE, } from '../core/icons.ts'; import * as P from '../core/icon-paths.ts'; import { TagInput } from '../core/tag-input.ts'; import { IconSelect } from '../core/icon-select.ts'; import { loadPictureSources } from './streams.ts'; const _icon = (d: string) => ``; // ── TagInput instance for capture template modal ── let _captureTemplateTagsInput: TagInput | null = null; class CaptureTemplateModal extends Modal { constructor() { super('template-modal'); } snapshotValues() { const vals: any = { name: (document.getElementById('template-name') as HTMLInputElement).value, description: (document.getElementById('template-description') as HTMLInputElement).value, engine: (document.getElementById('template-engine') as HTMLSelectElement).value, tags: JSON.stringify(_captureTemplateTagsInput ? _captureTemplateTagsInput.getValue() : []), }; document.querySelectorAll('[data-config-key]').forEach((field: any) => { vals['cfg_' + field.dataset.configKey] = field.value; }); return vals; } onForceClose() { if (_captureTemplateTagsInput) { _captureTemplateTagsInput.destroy(); _captureTemplateTagsInput = null; } setCurrentEditingTemplateId(null); set_templateNameManuallyEdited(false); } } const templateModal = new CaptureTemplateModal(); const testTemplateModal = new Modal('test-template-modal'); // ===== Capture Templates ===== async function loadCaptureTemplates() { try { await captureTemplatesCache.fetch(); await loadPictureSources(); } catch (error) { if (error.isAuth) return; console.error('Error loading capture templates:', error); showToast(t('streams.error.load'), 'error'); } } export async function showAddTemplateModal(cloneData: any = null) { setCurrentEditingTemplateId(null); document.getElementById('template-modal-title')!.innerHTML = `${ICON_CAPTURE_TEMPLATE} ${t('templates.add')}`; (document.getElementById('template-form') as HTMLFormElement).reset(); (document.getElementById('template-id') as HTMLInputElement).value = ''; document.getElementById('engine-config-section')!.style.display = 'none'; document.getElementById('template-error')!.style.display = 'none'; set_templateNameManuallyEdited(!!cloneData); (document.getElementById('template-name') as HTMLInputElement).oninput = () => { set_templateNameManuallyEdited(true); }; await loadAvailableEngines(); // Pre-fill from clone data after engines are loaded if (cloneData) { (document.getElementById('template-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)'; (document.getElementById('template-description') as HTMLInputElement).value = cloneData.description || ''; (document.getElementById('template-engine') as HTMLSelectElement).value = cloneData.engine_type; await onEngineChange(); populateEngineConfig(cloneData.engine_config); } // Tags if (_captureTemplateTagsInput) { _captureTemplateTagsInput.destroy(); _captureTemplateTagsInput = null; } _captureTemplateTagsInput = new TagInput(document.getElementById('capture-template-tags-container'), { placeholder: t('tags.placeholder') }); _captureTemplateTagsInput.setValue(cloneData ? (cloneData.tags || []) : []); templateModal.open(); templateModal.snapshot(); } export async function editTemplate(templateId: any) { try { const response = await fetchWithAuth(`/capture-templates/${templateId}`); if (!response.ok) throw new Error(`Failed to load template: ${response.status}`); const template = await response.json(); setCurrentEditingTemplateId(templateId); document.getElementById('template-modal-title')!.innerHTML = `${ICON_CAPTURE_TEMPLATE} ${t('templates.edit')}`; (document.getElementById('template-id') as HTMLInputElement).value = templateId; (document.getElementById('template-name') as HTMLInputElement).value = template.name; (document.getElementById('template-description') as HTMLInputElement).value = template.description || ''; await loadAvailableEngines(); (document.getElementById('template-engine') as HTMLSelectElement).value = template.engine_type; await onEngineChange(); populateEngineConfig(template.engine_config); await loadDisplaysForTest(); const testResults = document.getElementById('template-test-results'); if (testResults) testResults.style.display = 'none'; document.getElementById('template-error')!.style.display = 'none'; // Tags if (_captureTemplateTagsInput) { _captureTemplateTagsInput.destroy(); _captureTemplateTagsInput = null; } _captureTemplateTagsInput = new TagInput(document.getElementById('capture-template-tags-container'), { placeholder: t('tags.placeholder') }); _captureTemplateTagsInput.setValue(template.tags || []); templateModal.open(); templateModal.snapshot(); } catch (error) { console.error('Error loading template:', error); showToast(t('templates.error.load') + ': ' + error.message, 'error'); } } export async function closeTemplateModal() { await templateModal.close(); } export function updateCaptureDuration(value: any) { document.getElementById('test-template-duration-value')!.textContent = value; localStorage.setItem('capture_duration', value); } function restoreCaptureDuration() { const savedDuration = localStorage.getItem('capture_duration'); if (savedDuration) { const durationInput = document.getElementById('test-template-duration') as HTMLInputElement; const durationValue = document.getElementById('test-template-duration-value')!; durationInput.value = savedDuration; durationValue.textContent = savedDuration; } } export async function showTestTemplateModal(templateId: any) { try { const templates = await captureTemplatesCache.fetch(); const template = templates.find(tp => tp.id === templateId); if (!template) { showToast(t('templates.error.load'), 'error'); return; } setCurrentTestingTemplate(template); await loadDisplaysForTest(); restoreCaptureDuration(); testTemplateModal.open(); setupBackdropClose((testTemplateModal as any).el, () => closeTestTemplateModal()); } catch (error) { if (error.isAuth) return; showToast(t('templates.error.load'), 'error'); } } export function closeTestTemplateModal() { testTemplateModal.forceClose(); setCurrentTestingTemplate(null); } async function loadAvailableEngines() { try { const response = await fetchWithAuth('/capture-engines'); if (!response.ok) throw new Error(`Failed to load engines: ${response.status}`); const data = await response.json(); setAvailableEngines(data.engines || []); const select = document.getElementById('template-engine') as HTMLSelectElement; select.innerHTML = ''; availableEngines.forEach((engine: any) => { const option = document.createElement('option'); option.value = engine.type; option.textContent = engine.name; if (!engine.available) { option.disabled = true; option.textContent += ` (${t('templates.engine.unavailable')})`; } select.appendChild(option); }); if (!select.value) { const firstAvailable = availableEngines.find(e => e.available); if (firstAvailable) select.value = firstAvailable.type; } // Update icon-grid selector with dynamic engine list const items = availableEngines .filter(e => e.available) .map(e => ({ value: e.type, icon: getEngineIcon(e.type), label: e.name, desc: t(`templates.engine.${e.type}.desc`) })); if (_engineIconSelect) { _engineIconSelect.updateItems(items); } else { _engineIconSelect = new IconSelect({ target: select, items, columns: 2 }); } _engineIconSelect.setValue(select.value); } catch (error) { console.error('Error loading engines:', error); showToast(t('templates.error.engines') + ': ' + error.message, 'error'); } } let _engineIconSelect: IconSelect | null = null; export async function onEngineChange() { const engineType = (document.getElementById('template-engine') as HTMLSelectElement).value; if (_engineIconSelect) _engineIconSelect.setValue(engineType); const configSection = document.getElementById('engine-config-section')!; const configFields = document.getElementById('engine-config-fields')!; if (!engineType) { configSection.style.display = 'none'; return; } const engine = availableEngines.find((e: any) => e.type === engineType); if (!engine) { configSection.style.display = 'none'; return; } if (!_templateNameManuallyEdited && !(document.getElementById('template-id') as HTMLInputElement).value) { (document.getElementById('template-name') as HTMLInputElement).value = engine.name || engineType; } const hint = document.getElementById('engine-availability-hint')!; if (!engine.available) { hint.textContent = t('templates.engine.unavailable.hint'); hint.style.display = 'block'; hint.style.color = 'var(--error-color)'; } else { hint.style.display = 'none'; } configFields.innerHTML = ''; const defaultConfig = engine.default_config || {}; // Known select options for specific config keys const CONFIG_SELECT_OPTIONS = { camera_backend: ['auto', 'dshow', 'msmf', 'v4l2'], }; // IconSelect definitions for specific config keys const CONFIG_ICON_SELECT = { camera_backend: { columns: 2, items: [ { value: 'auto', icon: _icon(P.refreshCw), label: 'Auto', desc: t('templates.config.camera_backend.auto') }, { value: 'dshow', icon: _icon(P.camera), label: 'DShow', desc: t('templates.config.camera_backend.dshow') }, { value: 'msmf', icon: _icon(P.film), label: 'MSMF', desc: t('templates.config.camera_backend.msmf') }, { value: 'v4l2', icon: _icon(P.monitor), label: 'V4L2', desc: t('templates.config.camera_backend.v4l2') }, ], }, }; if (Object.keys(defaultConfig).length === 0) { configSection.style.display = 'none'; return; } else { let gridHtml = '