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
|
||||
|
||||
@@ -206,11 +206,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Settings Modal -->
|
||||
<!-- General Settings Modal -->
|
||||
<div id="device-settings-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 data-i18n="settings.title">⚙️ Device Settings</h2>
|
||||
<h2 data-i18n="settings.general.title">⚙️ General Settings</h2>
|
||||
<button class="modal-close-btn" onclick="closeDeviceSettingsModal()" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -228,18 +228,6 @@
|
||||
<small class="input-hint" data-i18n="settings.url.hint">IP address or hostname of your WLED device</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-display-index" data-i18n="settings.display_index">Display:</label>
|
||||
<select id="settings-display-index"></select>
|
||||
<small class="input-hint" data-i18n="settings.display_index.hint">Which screen to capture for this device</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-capture-template" data-i18n="settings.capture_template">Capture Template:</label>
|
||||
<select id="settings-capture-template"></select>
|
||||
<small class="input-hint" data-i18n="settings.capture_template.hint">Screen capture engine and configuration for this device</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-health-interval" data-i18n="settings.health_interval">Health Check Interval (s):</label>
|
||||
<input type="number" id="settings-health-interval" min="5" max="600" value="30">
|
||||
@@ -256,6 +244,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Capture Settings Modal -->
|
||||
<div id="capture-settings-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 data-i18n="settings.capture.title">🎬 Capture Settings</h2>
|
||||
<button class="modal-close-btn" onclick="closeCaptureSettingsModal()" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="capture-settings-form">
|
||||
<input type="hidden" id="capture-settings-device-id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="capture-settings-display-index" data-i18n="settings.display_index">Display:</label>
|
||||
<select id="capture-settings-display-index"></select>
|
||||
<small class="input-hint" data-i18n="settings.display_index.hint">Which screen to capture for this device</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="capture-settings-template" data-i18n="settings.capture_template">Capture Template:</label>
|
||||
<select id="capture-settings-template"></select>
|
||||
<small class="input-hint" data-i18n="settings.capture_template.hint">Screen capture engine and configuration for this device</small>
|
||||
</div>
|
||||
|
||||
<div id="capture-settings-error" class="error-message" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-icon btn-secondary" onclick="closeCaptureSettingsModal()" title="Cancel">✕</button>
|
||||
<button class="btn btn-icon btn-primary" onclick="saveCaptureSettings()" title="Save">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div id="api-key-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
|
||||
@@ -32,6 +32,61 @@
|
||||
"displays.loading": "Loading displays...",
|
||||
"displays.none": "No displays available",
|
||||
"displays.failed": "Failed to load displays",
|
||||
"templates.title": "\uD83C\uDFAF Capture Templates",
|
||||
"templates.description": "Capture templates define how the screen is captured. Each template uses a specific capture engine (MSS, DXcam, WGC) with custom settings. Assign templates to devices for optimal performance.",
|
||||
"templates.loading": "Loading templates...",
|
||||
"templates.empty": "No capture templates configured",
|
||||
"templates.add": "Add Capture Template",
|
||||
"templates.edit": "Edit Capture Template",
|
||||
"templates.name": "Template Name:",
|
||||
"templates.name.placeholder": "My Custom Template",
|
||||
"templates.description.label": "Description (optional):",
|
||||
"templates.description.placeholder": "Describe this template...",
|
||||
"templates.engine": "Capture Engine:",
|
||||
"templates.engine.select": "Select an engine...",
|
||||
"templates.engine.unavailable": "Unavailable",
|
||||
"templates.engine.unavailable.hint": "This engine is not available on your system",
|
||||
"templates.config": "Engine Configuration",
|
||||
"templates.config.show": "Show configuration",
|
||||
"templates.config.none": "No additional configuration",
|
||||
"templates.config.default": "Default",
|
||||
"templates.default": "Default",
|
||||
"templates.custom": "Custom Templates",
|
||||
"templates.default.locked": "Default template (cannot edit/delete)",
|
||||
"templates.created": "Template created successfully",
|
||||
"templates.updated": "Template updated successfully",
|
||||
"templates.deleted": "Template deleted successfully",
|
||||
"templates.delete.confirm": "Are you sure you want to delete this template?",
|
||||
"templates.error.load": "Failed to load templates",
|
||||
"templates.error.engines": "Failed to load engines",
|
||||
"templates.error.required": "Please fill in all required fields",
|
||||
"templates.error.delete": "Failed to delete template",
|
||||
"templates.test.title": "Test Capture",
|
||||
"templates.test.description": "Test this template before saving to see a capture preview and performance metrics.",
|
||||
"templates.test.display": "Display:",
|
||||
"templates.test.display.select": "Select display...",
|
||||
"templates.test.duration": "Capture Duration (s):",
|
||||
"templates.test.border_width": "Border Width (px):",
|
||||
"templates.test.run": "\uD83E\uDDEA Run Test",
|
||||
"templates.test.running": "Running test...",
|
||||
"templates.test.results.preview": "Full Capture Preview",
|
||||
"templates.test.results.borders": "Border Extraction",
|
||||
"templates.test.results.top": "Top",
|
||||
"templates.test.results.right": "Right",
|
||||
"templates.test.results.bottom": "Bottom",
|
||||
"templates.test.results.left": "Left",
|
||||
"templates.test.results.performance": "Performance",
|
||||
"templates.test.results.capture_time": "Capture",
|
||||
"templates.test.results.extraction_time": "Extraction",
|
||||
"templates.test.results.total_time": "Total",
|
||||
"templates.test.results.max_fps": "Max FPS",
|
||||
"templates.test.results.duration": "Duration",
|
||||
"templates.test.results.frame_count": "Frames",
|
||||
"templates.test.results.actual_fps": "Actual FPS",
|
||||
"templates.test.results.avg_capture_time": "Avg Capture",
|
||||
"templates.test.error.no_engine": "Please select a capture engine",
|
||||
"templates.test.error.no_display": "Please select a display",
|
||||
"templates.test.error.failed": "Test failed",
|
||||
"devices.title": "\uD83D\uDCA1 Devices",
|
||||
"devices.add": "Add New Device",
|
||||
"devices.loading": "Loading devices...",
|
||||
@@ -54,7 +109,8 @@
|
||||
"device.button.add": "Add Device",
|
||||
"device.button.start": "Start",
|
||||
"device.button.stop": "Stop",
|
||||
"device.button.settings": "Settings",
|
||||
"device.button.settings": "General Settings",
|
||||
"device.button.capture_settings": "Capture Settings",
|
||||
"device.button.calibrate": "Calibrate",
|
||||
"device.button.remove": "Remove",
|
||||
"device.button.webui": "Open WLED Web UI",
|
||||
@@ -81,16 +137,23 @@
|
||||
"device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from WLED",
|
||||
"device.tip.brightness": "Slide to adjust device brightness",
|
||||
"device.tip.start": "Start or stop screen capture processing",
|
||||
"device.tip.settings": "Configure device settings (display, FPS, health check)",
|
||||
"device.tip.settings": "Configure general device settings (name, URL, health check)",
|
||||
"device.tip.capture_settings": "Configure capture settings (display, capture template)",
|
||||
"device.tip.calibrate": "Calibrate LED positions, direction, and coverage",
|
||||
"device.tip.webui": "Open WLED's built-in web interface for advanced configuration",
|
||||
"device.tip.add": "Click here to add a new WLED device",
|
||||
"settings.title": "Device Settings",
|
||||
"settings.general.title": "General Settings",
|
||||
"settings.capture.title": "Capture Settings",
|
||||
"settings.capture.saved": "Capture settings updated",
|
||||
"settings.capture.failed": "Failed to save capture settings",
|
||||
"settings.brightness": "Brightness:",
|
||||
"settings.brightness.hint": "Global brightness for this WLED device (0-100%)",
|
||||
"settings.url.hint": "IP address or hostname of your WLED device",
|
||||
"settings.display_index": "Display:",
|
||||
"settings.display_index.hint": "Which screen to capture for this device",
|
||||
"settings.capture_template": "Capture Template:",
|
||||
"settings.capture_template.hint": "Screen capture engine and configuration for this device",
|
||||
"settings.button.cancel": "Cancel",
|
||||
"settings.health_interval": "Health Check Interval (s):",
|
||||
"settings.health_interval.hint": "How often to check the WLED device status (5-600 seconds)",
|
||||
|
||||
@@ -32,6 +32,61 @@
|
||||
"displays.loading": "Загрузка дисплеев...",
|
||||
"displays.none": "Нет доступных дисплеев",
|
||||
"displays.failed": "Не удалось загрузить дисплеи",
|
||||
"templates.title": "\uD83C\uDFAF Шаблоны Захвата",
|
||||
"templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.",
|
||||
"templates.loading": "Загрузка шаблонов...",
|
||||
"templates.empty": "Шаблоны захвата не настроены",
|
||||
"templates.add": "Добавить Шаблон Захвата",
|
||||
"templates.edit": "Редактировать Шаблон Захвата",
|
||||
"templates.name": "Имя Шаблона:",
|
||||
"templates.name.placeholder": "Мой Пользовательский Шаблон",
|
||||
"templates.description.label": "Описание (необязательно):",
|
||||
"templates.description.placeholder": "Опишите этот шаблон...",
|
||||
"templates.engine": "Движок Захвата:",
|
||||
"templates.engine.select": "Выберите движок...",
|
||||
"templates.engine.unavailable": "Недоступен",
|
||||
"templates.engine.unavailable.hint": "Этот движок недоступен в вашей системе",
|
||||
"templates.config": "Конфигурация Движка",
|
||||
"templates.config.show": "Показать конфигурацию",
|
||||
"templates.config.none": "Нет дополнительных настроек",
|
||||
"templates.config.default": "По умолчанию",
|
||||
"templates.default": "По умолчанию",
|
||||
"templates.custom": "Пользовательские шаблоны",
|
||||
"templates.default.locked": "Системный шаблон (нельзя редактировать/удалить)",
|
||||
"templates.created": "Шаблон успешно создан",
|
||||
"templates.updated": "Шаблон успешно обновлён",
|
||||
"templates.deleted": "Шаблон успешно удалён",
|
||||
"templates.delete.confirm": "Вы уверены, что хотите удалить этот шаблон?",
|
||||
"templates.error.load": "Не удалось загрузить шаблоны",
|
||||
"templates.error.engines": "Не удалось загрузить движки",
|
||||
"templates.error.required": "Пожалуйста, заполните все обязательные поля",
|
||||
"templates.error.delete": "Не удалось удалить шаблон",
|
||||
"templates.test.title": "Тест Захвата",
|
||||
"templates.test.description": "Протестируйте этот шаблон перед сохранением, чтобы увидеть предпросмотр захвата и метрики производительности.",
|
||||
"templates.test.display": "Дисплей:",
|
||||
"templates.test.display.select": "Выберите дисплей...",
|
||||
"templates.test.duration": "Длительность Захвата (с):",
|
||||
"templates.test.border_width": "Ширина Границы (px):",
|
||||
"templates.test.run": "\uD83E\uDDEA Запустить Тест",
|
||||
"templates.test.running": "Выполняется тест...",
|
||||
"templates.test.results.preview": "Полный Предпросмотр Захвата",
|
||||
"templates.test.results.borders": "Извлечение Границ",
|
||||
"templates.test.results.top": "Сверху",
|
||||
"templates.test.results.right": "Справа",
|
||||
"templates.test.results.bottom": "Снизу",
|
||||
"templates.test.results.left": "Слева",
|
||||
"templates.test.results.performance": "Производительность",
|
||||
"templates.test.results.capture_time": "Захват",
|
||||
"templates.test.results.extraction_time": "Извлечение",
|
||||
"templates.test.results.total_time": "Всего",
|
||||
"templates.test.results.max_fps": "Макс. FPS",
|
||||
"templates.test.results.duration": "Длительность",
|
||||
"templates.test.results.frame_count": "Кадры",
|
||||
"templates.test.results.actual_fps": "Факт. FPS",
|
||||
"templates.test.results.avg_capture_time": "Средн. Захват",
|
||||
"templates.test.error.no_engine": "Пожалуйста, выберите движок захвата",
|
||||
"templates.test.error.no_display": "Пожалуйста, выберите дисплей",
|
||||
"templates.test.error.failed": "Тест не удался",
|
||||
"devices.title": "\uD83D\uDCA1 Устройства",
|
||||
"devices.add": "Добавить Новое Устройство",
|
||||
"devices.loading": "Загрузка устройств...",
|
||||
@@ -54,7 +109,8 @@
|
||||
"device.button.add": "Добавить Устройство",
|
||||
"device.button.start": "Запустить",
|
||||
"device.button.stop": "Остановить",
|
||||
"device.button.settings": "Настройки",
|
||||
"device.button.settings": "Основные настройки",
|
||||
"device.button.capture_settings": "Настройки захвата",
|
||||
"device.button.calibrate": "Калибровка",
|
||||
"device.button.remove": "Удалить",
|
||||
"device.button.webui": "Открыть веб-интерфейс WLED",
|
||||
@@ -81,16 +137,23 @@
|
||||
"device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически из WLED",
|
||||
"device.tip.brightness": "Перетащите для регулировки яркости",
|
||||
"device.tip.start": "Запуск или остановка захвата экрана",
|
||||
"device.tip.settings": "Настройки устройства (дисплей, FPS, интервал проверки)",
|
||||
"device.tip.settings": "Основные настройки устройства (имя, URL, интервал проверки)",
|
||||
"device.tip.capture_settings": "Настройки захвата (дисплей, шаблон захвата)",
|
||||
"device.tip.calibrate": "Калибровка позиций LED, направления и зоны покрытия",
|
||||
"device.tip.webui": "Открыть встроенный веб-интерфейс WLED для расширенной настройки",
|
||||
"device.tip.add": "Нажмите, чтобы добавить новое WLED устройство",
|
||||
"settings.title": "Настройки Устройства",
|
||||
"settings.general.title": "Основные Настройки",
|
||||
"settings.capture.title": "Настройки Захвата",
|
||||
"settings.capture.saved": "Настройки захвата обновлены",
|
||||
"settings.capture.failed": "Не удалось сохранить настройки захвата",
|
||||
"settings.brightness": "Яркость:",
|
||||
"settings.brightness.hint": "Общая яркость для этого WLED устройства (0-100%)",
|
||||
"settings.url.hint": "IP адрес или имя хоста вашего WLED устройства",
|
||||
"settings.display_index": "Дисплей:",
|
||||
"settings.display_index.hint": "Какой экран захватывать для этого устройства",
|
||||
"settings.capture_template": "Шаблон Захвата:",
|
||||
"settings.capture_template.hint": "Движок захвата экрана и конфигурация для этого устройства",
|
||||
"settings.button.cancel": "Отмена",
|
||||
"settings.health_interval": "Интервал Проверки (с):",
|
||||
"settings.health_interval.hint": "Как часто проверять статус WLED устройства (5-600 секунд)",
|
||||
|
||||
@@ -1711,6 +1711,24 @@ input:-webkit-autofill:focus {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.templates-separator {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.templates-separator::before,
|
||||
.templates-separator::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
Reference in New Issue
Block a user