Add capture template system with in-memory defaults and split device settings UI
Some checks failed
Validate / validate (push) Failing after 8s
Some checks failed
Validate / validate (push) Failing after 8s
- Generate default templates (MSS, DXcam, WGC) in memory from EngineRegistry at startup - Only persist user-created templates to JSON, skip defaults on load/save - Add capture_template_id to Device model and DeviceCreate schema - Remember last used template in localStorage, use it for new devices with fallback - Split Device Settings dialog into General Settings and Capture Settings - Add capture settings button (🎬) to device card - Separate default and custom templates with visual separator in Templates tab - Add capture engine integration to ProcessorManager - Add CLAUDE.md with git commit/push policy and server restart instructions - Add en/ru localization for all new UI elements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -630,6 +630,9 @@ function createDeviceCard(device) {
|
||||
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
||||
⚙️
|
||||
</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="showCaptureSettings('${device.id}')" title="${t('device.button.capture_settings')}">
|
||||
🎬
|
||||
</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="showCalibration('${device.id}')" title="${t('device.button.calibrate')}">
|
||||
📐
|
||||
</button>
|
||||
@@ -737,12 +740,7 @@ async function removeDevice(deviceId) {
|
||||
|
||||
async function showSettings(deviceId) {
|
||||
try {
|
||||
// Fetch device data, displays, and templates in parallel
|
||||
const [deviceResponse, displaysResponse, templatesResponse] = await Promise.all([
|
||||
fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
]);
|
||||
const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() });
|
||||
|
||||
if (deviceResponse.status === 401) {
|
||||
handle401Error();
|
||||
@@ -756,48 +754,7 @@ async function showSettings(deviceId) {
|
||||
|
||||
const device = await deviceResponse.json();
|
||||
|
||||
// Populate display index select
|
||||
const displaySelect = document.getElementById('settings-display-index');
|
||||
displaySelect.innerHTML = '';
|
||||
if (displaysResponse.ok) {
|
||||
const displaysData = await displaysResponse.json();
|
||||
(displaysData.displays || []).forEach(d => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = d.index;
|
||||
opt.textContent = `${d.index}: ${d.width}x${d.height}${d.is_primary ? ` (${t('displays.badge.primary')})` : ''}`;
|
||||
displaySelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
if (displaySelect.options.length === 0) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '0';
|
||||
opt.textContent = '0';
|
||||
displaySelect.appendChild(opt);
|
||||
}
|
||||
displaySelect.value = String(device.settings.display_index ?? 0);
|
||||
|
||||
// Populate capture template select
|
||||
const templateSelect = document.getElementById('settings-capture-template');
|
||||
templateSelect.innerHTML = '';
|
||||
if (templatesResponse.ok) {
|
||||
const templatesData = await templatesResponse.json();
|
||||
(templatesData.templates || []).forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.id;
|
||||
const engineIcon = getEngineIcon(t.engine_type);
|
||||
opt.textContent = `${engineIcon} ${t.name}`;
|
||||
templateSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
if (templateSelect.options.length === 0) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = 'tpl_mss_default';
|
||||
opt.textContent = 'MSS (Default)';
|
||||
templateSelect.appendChild(opt);
|
||||
}
|
||||
templateSelect.value = device.capture_template_id || 'tpl_mss_default';
|
||||
|
||||
// Populate other fields
|
||||
// Populate fields
|
||||
document.getElementById('settings-device-id').value = device.id;
|
||||
document.getElementById('settings-device-name').value = device.name;
|
||||
document.getElementById('settings-device-url').value = device.url;
|
||||
@@ -807,9 +764,7 @@ async function showSettings(deviceId) {
|
||||
settingsInitialValues = {
|
||||
name: device.name,
|
||||
url: device.url,
|
||||
display_index: String(device.settings.display_index ?? 0),
|
||||
state_check_interval: String(device.settings.state_check_interval || 30),
|
||||
capture_template_id: device.capture_template_id || 'tpl_mss_default',
|
||||
};
|
||||
|
||||
// Show modal
|
||||
@@ -832,9 +787,7 @@ function isSettingsDirty() {
|
||||
return (
|
||||
document.getElementById('settings-device-name').value !== settingsInitialValues.name ||
|
||||
document.getElementById('settings-device-url').value !== settingsInitialValues.url ||
|
||||
document.getElementById('settings-display-index').value !== settingsInitialValues.display_index ||
|
||||
document.getElementById('settings-health-interval').value !== settingsInitialValues.state_check_interval ||
|
||||
document.getElementById('settings-capture-template').value !== settingsInitialValues.capture_template_id
|
||||
document.getElementById('settings-health-interval').value !== settingsInitialValues.state_check_interval
|
||||
);
|
||||
}
|
||||
|
||||
@@ -859,9 +812,7 @@ async function saveDeviceSettings() {
|
||||
const deviceId = document.getElementById('settings-device-id').value;
|
||||
const name = document.getElementById('settings-device-name').value.trim();
|
||||
const url = document.getElementById('settings-device-url').value.trim();
|
||||
const display_index = parseInt(document.getElementById('settings-display-index').value) || 0;
|
||||
const state_check_interval = parseInt(document.getElementById('settings-health-interval').value) || 30;
|
||||
const capture_template_id = document.getElementById('settings-capture-template').value;
|
||||
const error = document.getElementById('settings-error');
|
||||
|
||||
// Validation
|
||||
@@ -872,11 +823,11 @@ async function saveDeviceSettings() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Update device info (name, url, capture_template_id)
|
||||
// Update device info (name, url)
|
||||
const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ name, url, capture_template_id })
|
||||
body: JSON.stringify({ name, url })
|
||||
});
|
||||
|
||||
if (deviceResponse.status === 401) {
|
||||
@@ -895,7 +846,7 @@ async function saveDeviceSettings() {
|
||||
const settingsResponse = await fetch(`${API_BASE}/devices/${deviceId}/settings`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ display_index, state_check_interval })
|
||||
body: JSON.stringify({ state_check_interval })
|
||||
});
|
||||
|
||||
if (settingsResponse.status === 401) {
|
||||
@@ -904,7 +855,7 @@ async function saveDeviceSettings() {
|
||||
}
|
||||
|
||||
if (settingsResponse.ok) {
|
||||
showToast('Device settings updated', 'success');
|
||||
showToast(t('settings.saved'), 'success');
|
||||
forceCloseDeviceSettingsModal();
|
||||
loadDevices();
|
||||
} else {
|
||||
@@ -919,6 +870,170 @@ async function saveDeviceSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Capture Settings Modal =====
|
||||
|
||||
let captureSettingsInitialValues = {};
|
||||
|
||||
async function showCaptureSettings(deviceId) {
|
||||
try {
|
||||
// Fetch device data, displays, and templates in parallel
|
||||
const [deviceResponse, displaysResponse, templatesResponse] = await Promise.all([
|
||||
fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() }),
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
]);
|
||||
|
||||
if (deviceResponse.status === 401) {
|
||||
handle401Error();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deviceResponse.ok) {
|
||||
showToast('Failed to load capture settings', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const device = await deviceResponse.json();
|
||||
|
||||
// Populate display index select
|
||||
const displaySelect = document.getElementById('capture-settings-display-index');
|
||||
displaySelect.innerHTML = '';
|
||||
if (displaysResponse.ok) {
|
||||
const displaysData = await displaysResponse.json();
|
||||
(displaysData.displays || []).forEach(d => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = d.index;
|
||||
opt.textContent = `${d.index}: ${d.width}x${d.height}${d.is_primary ? ` (${t('displays.badge.primary')})` : ''}`;
|
||||
displaySelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
if (displaySelect.options.length === 0) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '0';
|
||||
opt.textContent = '0';
|
||||
displaySelect.appendChild(opt);
|
||||
}
|
||||
displaySelect.value = String(device.settings.display_index ?? 0);
|
||||
|
||||
// Populate capture template select
|
||||
const templateSelect = document.getElementById('capture-settings-template');
|
||||
templateSelect.innerHTML = '';
|
||||
if (templatesResponse.ok) {
|
||||
const templatesData = await templatesResponse.json();
|
||||
(templatesData.templates || []).forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t.id;
|
||||
const engineIcon = getEngineIcon(t.engine_type);
|
||||
opt.textContent = `${engineIcon} ${t.name}`;
|
||||
templateSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
if (templateSelect.options.length === 0) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = 'tpl_mss_default';
|
||||
opt.textContent = 'MSS (Default)';
|
||||
templateSelect.appendChild(opt);
|
||||
}
|
||||
templateSelect.value = device.capture_template_id || 'tpl_mss_default';
|
||||
|
||||
// Store device ID and snapshot initial values
|
||||
document.getElementById('capture-settings-device-id').value = device.id;
|
||||
captureSettingsInitialValues = {
|
||||
display_index: String(device.settings.display_index ?? 0),
|
||||
capture_template_id: device.capture_template_id || 'tpl_mss_default',
|
||||
};
|
||||
|
||||
// Show modal
|
||||
const modal = document.getElementById('capture-settings-modal');
|
||||
modal.style.display = 'flex';
|
||||
lockBody();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load capture settings:', error);
|
||||
showToast('Failed to load capture settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function isCaptureSettingsDirty() {
|
||||
return (
|
||||
document.getElementById('capture-settings-display-index').value !== captureSettingsInitialValues.display_index ||
|
||||
document.getElementById('capture-settings-template').value !== captureSettingsInitialValues.capture_template_id
|
||||
);
|
||||
}
|
||||
|
||||
function forceCloseCaptureSettingsModal() {
|
||||
const modal = document.getElementById('capture-settings-modal');
|
||||
const error = document.getElementById('capture-settings-error');
|
||||
modal.style.display = 'none';
|
||||
error.style.display = 'none';
|
||||
unlockBody();
|
||||
captureSettingsInitialValues = {};
|
||||
}
|
||||
|
||||
async function closeCaptureSettingsModal() {
|
||||
if (isCaptureSettingsDirty()) {
|
||||
const confirmed = await showConfirm(t('modal.discard_changes'));
|
||||
if (!confirmed) return;
|
||||
}
|
||||
forceCloseCaptureSettingsModal();
|
||||
}
|
||||
|
||||
async function saveCaptureSettings() {
|
||||
const deviceId = document.getElementById('capture-settings-device-id').value;
|
||||
const display_index = parseInt(document.getElementById('capture-settings-display-index').value) || 0;
|
||||
const capture_template_id = document.getElementById('capture-settings-template').value;
|
||||
const error = document.getElementById('capture-settings-error');
|
||||
|
||||
try {
|
||||
// Update capture template on device
|
||||
const deviceResponse = await fetch(`${API_BASE}/devices/${deviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ capture_template_id })
|
||||
});
|
||||
|
||||
if (deviceResponse.status === 401) {
|
||||
handle401Error();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deviceResponse.ok) {
|
||||
const errorData = await deviceResponse.json();
|
||||
error.textContent = `Failed to update capture template: ${errorData.detail}`;
|
||||
error.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update display index in settings
|
||||
const settingsResponse = await fetch(`${API_BASE}/devices/${deviceId}/settings`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ display_index })
|
||||
});
|
||||
|
||||
if (settingsResponse.status === 401) {
|
||||
handle401Error();
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsResponse.ok) {
|
||||
// Remember last used template for new device creation
|
||||
localStorage.setItem('lastCaptureTemplateId', capture_template_id);
|
||||
showToast(t('settings.capture.saved'), 'success');
|
||||
forceCloseCaptureSettingsModal();
|
||||
loadDevices();
|
||||
} else {
|
||||
const errorData = await settingsResponse.json();
|
||||
error.textContent = `Failed to update settings: ${errorData.detail}`;
|
||||
error.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save capture settings:', err);
|
||||
error.textContent = t('settings.capture.failed');
|
||||
error.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Card brightness controls
|
||||
function updateBrightnessLabel(deviceId, value) {
|
||||
const slider = document.querySelector(`[data-device-id="${deviceId}"] .brightness-slider`);
|
||||
@@ -971,10 +1086,16 @@ async function handleAddDevice(event) {
|
||||
}
|
||||
|
||||
try {
|
||||
const body = { name, url };
|
||||
const lastTemplateId = localStorage.getItem('lastCaptureTemplateId');
|
||||
if (lastTemplateId) {
|
||||
body.capture_template_id = lastTemplateId;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}/devices`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
body: JSON.stringify({ name, url })
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
@@ -1928,12 +2049,18 @@ document.addEventListener('click', (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Settings modal: dirty check
|
||||
// General settings modal: dirty check
|
||||
if (modalId === 'device-settings-modal') {
|
||||
closeDeviceSettingsModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture settings modal: dirty check
|
||||
if (modalId === 'capture-settings-modal') {
|
||||
closeCaptureSettingsModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calibration modal: dirty check
|
||||
if (modalId === 'calibration-modal') {
|
||||
closeCalibrationModal();
|
||||
@@ -1977,8 +2104,9 @@ const deviceTutorialSteps = [
|
||||
{ selector: '.brightness-control', textKey: 'device.tip.brightness', position: 'bottom' },
|
||||
{ selector: '.card-actions .btn:nth-child(1)', textKey: 'device.tip.start', position: 'top' },
|
||||
{ selector: '.card-actions .btn:nth-child(2)', textKey: 'device.tip.settings', position: 'top' },
|
||||
{ selector: '.card-actions .btn:nth-child(3)', textKey: 'device.tip.calibrate', position: 'top' },
|
||||
{ selector: '.card-actions .btn:nth-child(4)', textKey: 'device.tip.webui', position: 'top' }
|
||||
{ selector: '.card-actions .btn:nth-child(3)', textKey: 'device.tip.capture_settings', position: 'top' },
|
||||
{ selector: '.card-actions .btn:nth-child(4)', textKey: 'device.tip.calibrate', position: 'top' },
|
||||
{ selector: '.card-actions .btn:nth-child(5)', textKey: 'device.tip.webui', position: 'top' }
|
||||
];
|
||||
|
||||
function startTutorial(config) {
|
||||
@@ -2218,7 +2346,10 @@ function renderTemplatesList(templates) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = templates.map(template => {
|
||||
const defaultTemplates = templates.filter(t => t.is_default);
|
||||
const customTemplates = templates.filter(t => !t.is_default);
|
||||
|
||||
const renderCard = (template) => {
|
||||
const engineIcon = getEngineIcon(template.engine_type);
|
||||
const defaultBadge = template.is_default
|
||||
? `<span class="badge badge-default">${t('templates.default')}</span>`
|
||||
@@ -2258,10 +2389,21 @@ function renderTemplatesList(templates) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('') + `<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||
};
|
||||
|
||||
let html = defaultTemplates.map(renderCard).join('');
|
||||
|
||||
if (customTemplates.length > 0) {
|
||||
html += `<div class="templates-separator"><span>${t('templates.custom')}</span></div>`;
|
||||
html += customTemplates.map(renderCard).join('');
|
||||
}
|
||||
|
||||
html += `<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('templates.add')}</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Get engine icon
|
||||
|
||||
Reference in New Issue
Block a user