Merge templates into Streams tab, rename app to WLED Grab
- Merge Capture Templates and Processing Templates main tabs into Picture Streams sub-tabs (Screen Capture shows streams + engine templates, Processed shows streams + filter templates) - Rename "Capture Templates" to "Engine Templates" and "Processing Templates" to "Filter Templates" across all locale strings - Rename "Picture Streams" tab to "Streams" throughout UI and locales - Rename "WLED Screen Controller" to "WLED Grab" across all files - Add subtab section headers and styling for merged template views - Remove add card labels, keeping only plus icon for cleaner UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
"""WLED Screen Controller - Ambient lighting based on screen content."""
|
"""WLED Grab - Ambient lighting based on screen content."""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
__author__ = "Alexei Dolgolyov"
|
__author__ = "Alexei Dolgolyov"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Configuration management for WLED Screen Controller."""
|
"""Configuration management for WLED Grab."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class CaptureEngine(ABC):
|
|||||||
"""Abstract base class for screen capture engines.
|
"""Abstract base class for screen capture engines.
|
||||||
|
|
||||||
All screen capture engines must implement this interface to be
|
All screen capture engines must implement this interface to be
|
||||||
compatible with the WLED Screen Controller system.
|
compatible with the WLED Grab system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ENGINE_TYPE: str = "base" # Override in subclasses
|
ENGINE_TYPE: str = "base" # Override in subclasses
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ async def lifespan(app: FastAPI):
|
|||||||
Handles startup and shutdown events.
|
Handles startup and shutdown events.
|
||||||
"""
|
"""
|
||||||
# Startup
|
# Startup
|
||||||
logger.info(f"Starting WLED Screen Controller v{__version__}")
|
logger.info(f"Starting WLED Grab v{__version__}")
|
||||||
logger.info(f"Python version: {sys.version}")
|
logger.info(f"Python version: {sys.version}")
|
||||||
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
|
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ async def lifespan(app: FastAPI):
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
# Shutdown
|
# Shutdown
|
||||||
logger.info("Shutting down WLED Screen Controller")
|
logger.info("Shutting down WLED Grab")
|
||||||
|
|
||||||
# Stop all processing
|
# Stop all processing
|
||||||
try:
|
try:
|
||||||
@@ -163,7 +163,7 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Create FastAPI application
|
# Create FastAPI application
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="WLED Screen Controller",
|
title="WLED Grab",
|
||||||
description="Control WLED devices based on screen content for ambient lighting",
|
description="Control WLED devices based on screen content for ambient lighting",
|
||||||
version=__version__,
|
version=__version__,
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
@@ -217,7 +217,7 @@ async def root():
|
|||||||
|
|
||||||
# Fallback to JSON if static files not found
|
# Fallback to JSON if static files not found
|
||||||
return {
|
return {
|
||||||
"name": "WLED Screen Controller",
|
"name": "WLED Grab",
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"health": "/health",
|
"health": "/health",
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ const supportedLocales = {
|
|||||||
|
|
||||||
// Minimal inline fallback for critical UI elements
|
// Minimal inline fallback for critical UI elements
|
||||||
const fallbackTranslations = {
|
const fallbackTranslations = {
|
||||||
'app.title': 'WLED Screen Controller',
|
'app.title': 'WLED Grab',
|
||||||
'auth.placeholder': 'Enter your API key...',
|
'auth.placeholder': 'Enter your API key...',
|
||||||
'auth.button.login': 'Login'
|
'auth.button.login': 'Login'
|
||||||
};
|
};
|
||||||
@@ -494,18 +494,16 @@ async function loadDisplays() {
|
|||||||
let _cachedDisplays = null;
|
let _cachedDisplays = null;
|
||||||
|
|
||||||
function switchTab(name) {
|
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-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}`));
|
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
|
||||||
localStorage.setItem('activeTab', name);
|
localStorage.setItem('activeTab', name);
|
||||||
if (name === 'templates') {
|
|
||||||
loadCaptureTemplates();
|
|
||||||
}
|
|
||||||
if (name === 'streams') {
|
if (name === 'streams') {
|
||||||
loadPictureStreams();
|
loadPictureStreams();
|
||||||
}
|
}
|
||||||
if (name === 'pp-templates') {
|
|
||||||
loadPPTemplates();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTabs() {
|
function initTabs() {
|
||||||
@@ -2423,80 +2421,15 @@ async function loadCaptureTemplates() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load templates: ${response.status}`);
|
throw new Error(`Failed to load templates: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
renderTemplatesList(data.templates || []);
|
_cachedCaptureTemplates = data.templates || [];
|
||||||
|
// Re-render the streams tab which now contains template sections
|
||||||
|
renderPictureStreamsList(_cachedStreams);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading capture templates:', error);
|
console.error('Error loading capture templates:', error);
|
||||||
document.getElementById('templates-list').innerHTML = `
|
|
||||||
<div class="error-message">${t('templates.error.load')}: ${error.message}</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render templates list
|
|
||||||
function renderTemplatesList(templates) {
|
|
||||||
const container = document.getElementById('templates-list');
|
|
||||||
|
|
||||||
if (templates.length === 0) {
|
|
||||||
container.innerHTML = `<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>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderCard = (template) => {
|
|
||||||
const engineIcon = getEngineIcon(template.engine_type);
|
|
||||||
const configEntries = Object.entries(template.engine_config);
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="template-card" data-template-id="${template.id}">
|
|
||||||
<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)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stream-card-props">
|
|
||||||
<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 ? `
|
|
||||||
<details class="template-config-details">
|
|
||||||
<summary>${t('templates.config.show')}</summary>
|
|
||||||
<table class="config-table">
|
|
||||||
${configEntries.map(([key, val]) => `
|
|
||||||
<tr>
|
|
||||||
<td class="config-key">${escapeHtml(key)}</td>
|
|
||||||
<td class="config-value">${escapeHtml(String(val))}</td>
|
|
||||||
</tr>
|
|
||||||
`).join('')}
|
|
||||||
</table>
|
|
||||||
</details>
|
|
||||||
` : ''}
|
|
||||||
<div class="template-card-actions">
|
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">
|
|
||||||
🧪
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">
|
|
||||||
✏️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
let html = templates.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
|
// Get engine icon
|
||||||
function getEngineIcon(engineType) {
|
function getEngineIcon(engineType) {
|
||||||
return '🖥️';
|
return '🖥️';
|
||||||
@@ -3055,28 +2988,31 @@ let _availableFilters = []; // Loaded from GET /filters
|
|||||||
|
|
||||||
async function loadPictureStreams() {
|
async function loadPictureStreams() {
|
||||||
try {
|
try {
|
||||||
// Ensure PP templates and capture templates are cached for stream card display
|
// Always fetch templates, filters, and streams in parallel
|
||||||
if (_cachedPPTemplates.length === 0 || _cachedCaptureTemplates.length === 0) {
|
// since templates are now rendered inside stream sub-tabs
|
||||||
try {
|
const [filtersResp, ppResp, captResp, streamsResp] = await Promise.all([
|
||||||
if (_availableFilters.length === 0) {
|
_availableFilters.length === 0 ? fetchWithAuth('/filters') : Promise.resolve(null),
|
||||||
const fr = await fetchWithAuth('/filters');
|
fetchWithAuth('/postprocessing-templates'),
|
||||||
if (fr.ok) { const fd = await fr.json(); _availableFilters = fd.filters || []; }
|
fetchWithAuth('/capture-templates'),
|
||||||
}
|
fetchWithAuth('/picture-streams')
|
||||||
if (_cachedPPTemplates.length === 0) {
|
]);
|
||||||
const pr = await fetchWithAuth('/postprocessing-templates');
|
|
||||||
if (pr.ok) { const pd = await pr.json(); _cachedPPTemplates = pd.templates || []; }
|
if (filtersResp && filtersResp.ok) {
|
||||||
}
|
const fd = await filtersResp.json();
|
||||||
if (_cachedCaptureTemplates.length === 0) {
|
_availableFilters = fd.filters || [];
|
||||||
const cr = await fetchWithAuth('/capture-templates');
|
|
||||||
if (cr.ok) { const cd = await cr.json(); _cachedCaptureTemplates = cd.templates || []; }
|
|
||||||
}
|
|
||||||
} catch (e) { console.warn('Could not pre-load templates for streams:', e); }
|
|
||||||
}
|
}
|
||||||
const response = await fetchWithAuth('/picture-streams');
|
if (ppResp.ok) {
|
||||||
if (!response.ok) {
|
const pd = await ppResp.json();
|
||||||
throw new Error(`Failed to load streams: ${response.status}`);
|
_cachedPPTemplates = pd.templates || [];
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
if (captResp.ok) {
|
||||||
|
const cd = await captResp.json();
|
||||||
|
_cachedCaptureTemplates = cd.templates || [];
|
||||||
|
}
|
||||||
|
if (!streamsResp.ok) {
|
||||||
|
throw new Error(`Failed to load streams: ${streamsResp.status}`);
|
||||||
|
}
|
||||||
|
const data = await streamsResp.json();
|
||||||
_cachedStreams = data.streams || [];
|
_cachedStreams = data.streams || [];
|
||||||
renderPictureStreamsList(_cachedStreams);
|
renderPictureStreamsList(_cachedStreams);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3101,7 +3037,7 @@ function renderPictureStreamsList(streams) {
|
|||||||
const container = document.getElementById('streams-list');
|
const container = document.getElementById('streams-list');
|
||||||
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
||||||
|
|
||||||
const renderCard = (stream) => {
|
const renderStreamCard = (stream) => {
|
||||||
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
||||||
const typeIcon = typeIcons[stream.stream_type] || '📺';
|
const typeIcon = typeIcons[stream.stream_type] || '📺';
|
||||||
|
|
||||||
@@ -3158,34 +3094,145 @@ function renderPictureStreamsList(streams) {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderCaptureTemplateCard = (template) => {
|
||||||
|
const engineIcon = getEngineIcon(template.engine_type);
|
||||||
|
const configEntries = Object.entries(template.engine_config);
|
||||||
|
return `
|
||||||
|
<div class="template-card" data-template-id="${template.id}">
|
||||||
|
<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)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stream-card-props">
|
||||||
|
<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 ? `
|
||||||
|
<details class="template-config-details">
|
||||||
|
<summary>${t('templates.config.show')}</summary>
|
||||||
|
<table class="config-table">
|
||||||
|
${configEntries.map(([key, val]) => `
|
||||||
|
<tr>
|
||||||
|
<td class="config-key">${escapeHtml(key)}</td>
|
||||||
|
<td class="config-value">${escapeHtml(String(val))}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
|
` : ''}
|
||||||
|
<div class="template-card-actions">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">
|
||||||
|
🧪
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPPTemplateCard = (tmpl) => {
|
||||||
|
let filterChainHtml = '';
|
||||||
|
if (tmpl.filters && tmpl.filters.length > 0) {
|
||||||
|
const filterNames = tmpl.filters.map(fi => `<span class="filter-chain-item">${escapeHtml(_getFilterName(fi.filter_id))}</span>`);
|
||||||
|
filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">→</span>')}</div>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div class="template-card" data-pp-template-id="${tmpl.id}">
|
||||||
|
<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)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
|
||||||
|
${filterChainHtml}
|
||||||
|
<div class="template-card-actions">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="showTestPPTemplateModal('${tmpl.id}')" title="${t('postprocessing.test.title')}">
|
||||||
|
🧪
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
const rawStreams = streams.filter(s => s.stream_type === 'raw');
|
const rawStreams = streams.filter(s => s.stream_type === 'raw');
|
||||||
const processedStreams = streams.filter(s => s.stream_type === 'processed');
|
const processedStreams = streams.filter(s => s.stream_type === 'processed');
|
||||||
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
|
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
|
||||||
|
|
||||||
const addCard = (type, labelKey) => `
|
const addStreamCard = (type) => `
|
||||||
<div class="template-card add-template-card" onclick="showAddStreamModal('${type}')">
|
<div class="template-card add-template-card" onclick="showAddStreamModal('${type}')">
|
||||||
<div class="add-template-icon">+</div>
|
<div class="add-template-icon">+</div>
|
||||||
<div class="add-template-label">${t(labelKey)}</div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', streams: rawStreams, addLabelKey: 'streams.add.raw' },
|
{ key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', streams: rawStreams },
|
||||||
{ key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', streams: staticImageStreams, addLabelKey: 'streams.add.static_image' },
|
{ key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', streams: staticImageStreams },
|
||||||
{ key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', streams: processedStreams, addLabelKey: 'streams.add.processed' },
|
{ key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', streams: processedStreams },
|
||||||
];
|
];
|
||||||
|
|
||||||
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>
|
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>
|
||||||
`<button class="stream-tab-btn${tab.key === activeTab ? ' active' : ''}" data-stream-tab="${tab.key}" onclick="switchStreamTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.streams.length}</span></button>`
|
`<button class="stream-tab-btn${tab.key === activeTab ? ' active' : ''}" data-stream-tab="${tab.key}" onclick="switchStreamTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.streams.length}</span></button>`
|
||||||
).join('')}</div>`;
|
).join('')}</div>`;
|
||||||
|
|
||||||
const panels = tabs.map(tab =>
|
const panels = tabs.map(tab => {
|
||||||
`<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">
|
let panelContent = '';
|
||||||
<div class="templates-grid">
|
|
||||||
${tab.streams.map(renderCard).join('')}
|
if (tab.key === 'raw') {
|
||||||
${addCard(tab.key, tab.addLabelKey)}
|
// Screen Capture: streams section + capture templates section
|
||||||
</div>
|
panelContent = `
|
||||||
</div>`
|
<div class="subtab-section">
|
||||||
).join('');
|
<h3 class="subtab-section-header">${t('streams.section.streams')}</h3>
|
||||||
|
<div class="templates-grid">
|
||||||
|
${tab.streams.map(renderStreamCard).join('')}
|
||||||
|
${addStreamCard(tab.key)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="subtab-section">
|
||||||
|
<h3 class="subtab-section-header">${t('templates.title')}</h3>
|
||||||
|
<div class="templates-grid">
|
||||||
|
${_cachedCaptureTemplates.map(renderCaptureTemplateCard).join('')}
|
||||||
|
<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||||
|
<div class="add-template-icon">+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} else if (tab.key === 'processed') {
|
||||||
|
// Processed: streams section + PP templates section
|
||||||
|
panelContent = `
|
||||||
|
<div class="subtab-section">
|
||||||
|
<h3 class="subtab-section-header">${t('streams.section.streams')}</h3>
|
||||||
|
<div class="templates-grid">
|
||||||
|
${tab.streams.map(renderStreamCard).join('')}
|
||||||
|
${addStreamCard(tab.key)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="subtab-section">
|
||||||
|
<h3 class="subtab-section-header">${t('postprocessing.title')}</h3>
|
||||||
|
<div class="templates-grid">
|
||||||
|
${_cachedPPTemplates.map(renderPPTemplateCard).join('')}
|
||||||
|
<div class="template-card add-template-card" onclick="showAddPPTemplateModal()">
|
||||||
|
<div class="add-template-icon">+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
// Static Image: just the stream cards, no section headers
|
||||||
|
panelContent = `
|
||||||
|
<div class="templates-grid">
|
||||||
|
${tab.streams.map(renderStreamCard).join('')}
|
||||||
|
${addStreamCard(tab.key)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">${panelContent}</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
container.innerHTML = tabBar + panels;
|
container.innerHTML = tabBar + panels;
|
||||||
}
|
}
|
||||||
@@ -3671,12 +3718,10 @@ async function loadPPTemplates() {
|
|||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
_cachedPPTemplates = data.templates || [];
|
_cachedPPTemplates = data.templates || [];
|
||||||
renderPPTemplatesList(_cachedPPTemplates);
|
// Re-render the streams tab which now contains template sections
|
||||||
|
renderPictureStreamsList(_cachedStreams);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PP templates:', error);
|
console.error('Error loading PP templates:', error);
|
||||||
document.getElementById('pp-templates-list').innerHTML = `
|
|
||||||
<div class="error-message">${t('postprocessing.error.load')}: ${error.message}</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3691,56 +3736,6 @@ function _getFilterName(filterId) {
|
|||||||
return translated;
|
return translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPPTemplatesList(templates) {
|
|
||||||
const container = document.getElementById('pp-templates-list');
|
|
||||||
|
|
||||||
if (templates.length === 0) {
|
|
||||||
container.innerHTML = `<div class="template-card add-template-card" onclick="showAddPPTemplateModal()">
|
|
||||||
<div class="add-template-icon">+</div>
|
|
||||||
<div class="add-template-label">${t('postprocessing.add')}</div>
|
|
||||||
</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderCard = (tmpl) => {
|
|
||||||
// Build filter chain pills
|
|
||||||
let filterChainHtml = '';
|
|
||||||
if (tmpl.filters && tmpl.filters.length > 0) {
|
|
||||||
const filterNames = tmpl.filters.map(fi => `<span class="filter-chain-item">${escapeHtml(_getFilterName(fi.filter_id))}</span>`);
|
|
||||||
filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">→</span>')}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="template-card" data-pp-template-id="${tmpl.id}">
|
|
||||||
<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)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
|
|
||||||
${filterChainHtml}
|
|
||||||
<div class="template-card-actions">
|
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showTestPPTemplateModal('${tmpl.id}')" title="${t('postprocessing.test.title')}">
|
|
||||||
🧪
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">
|
|
||||||
✏️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
let html = templates.map(renderCard).join('');
|
|
||||||
html += `<div class="template-card add-template-card" onclick="showAddPPTemplateModal()">
|
|
||||||
<div class="add-template-icon">+</div>
|
|
||||||
<div class="add-template-label">${t('postprocessing.add')}</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Filter list management in PP template modal ---
|
// --- Filter list management in PP template modal ---
|
||||||
|
|
||||||
let _modalFilters = []; // Current filter list being edited in modal
|
let _modalFilters = []; // Current filter list being edited in modal
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>WLED Screen Controller</title>
|
<title>WLED Grab</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>💡</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>💡</text></svg>">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
<span id="server-status" class="status-badge">●</span>
|
<span id="server-status" class="status-badge">●</span>
|
||||||
<h1 data-i18n="app.title">WLED Screen Controller</h1>
|
<h1 data-i18n="app.title">WLED Grab</h1>
|
||||||
<span id="server-version"><span id="version-number"></span></span>
|
<span id="server-version"><span id="version-number"></span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info">
|
<div class="server-info">
|
||||||
@@ -36,9 +36,7 @@
|
|||||||
<div class="tab-bar">
|
<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 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">📺 Picture Streams</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="templates" onclick="switchTab('templates')"><span data-i18n="templates.title">🎯 Capture Templates</span></button>
|
|
||||||
<button class="tab-btn" data-tab="pp-templates" onclick="switchTab('pp-templates')"><span data-i18n="postprocessing.title">🎨 Processing Templates</span></button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel active" id="tab-devices">
|
<div class="tab-panel active" id="tab-devices">
|
||||||
@@ -54,17 +52,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel" id="tab-templates">
|
|
||||||
<div id="templates-list" class="templates-grid">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-panel" id="tab-pp-templates">
|
|
||||||
<div id="pp-templates-list" class="templates-grid">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
@@ -249,9 +236,9 @@
|
|||||||
<input type="hidden" id="stream-selector-device-id">
|
<input type="hidden" id="stream-selector-device-id">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="stream-selector-stream" data-i18n="device.stream_selector.label">Assigned Picture Stream:</label>
|
<label for="stream-selector-stream" data-i18n="device.stream_selector.label">Stream:</label>
|
||||||
<select id="stream-selector-stream"></select>
|
<select id="stream-selector-stream"></select>
|
||||||
<small class="input-hint" data-i18n="device.stream_selector.hint">Select a picture stream that defines what this device captures and processes</small>
|
<small class="input-hint" data-i18n="device.stream_selector.hint">Select a stream that defines what this device captures and processes</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="stream-selector-info" class="stream-info-panel" style="display: none;"></div>
|
<div id="stream-selector-info" class="stream-info-panel" style="display: none;"></div>
|
||||||
@@ -301,7 +288,7 @@
|
|||||||
<form id="api-key-form" onsubmit="submitApiKey(event)">
|
<form id="api-key-form" onsubmit="submitApiKey(event)">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p class="modal-description" data-i18n="auth.message">
|
<p class="modal-description" data-i18n="auth.message">
|
||||||
Please enter your API key to authenticate and access the WLED Screen Controller.
|
Please enter your API key to authenticate and access the WLED Grab.
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="api-key-input" data-i18n="auth.label">API Key:</label>
|
<label for="api-key-input" data-i18n="auth.label">API Key:</label>
|
||||||
@@ -447,7 +434,7 @@
|
|||||||
<div id="test-stream-modal" class="modal">
|
<div id="test-stream-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 data-i18n="streams.test.title">Test Picture Stream</h2>
|
<h2 data-i18n="streams.test.title">Test Stream</h2>
|
||||||
<button class="modal-close-btn" onclick="closeTestStreamModal()" title="Close">✕</button>
|
<button class="modal-close-btn" onclick="closeTestStreamModal()" title="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@@ -495,11 +482,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Picture Stream Modal -->
|
<!-- Stream Modal -->
|
||||||
<div id="stream-modal" class="modal">
|
<div id="stream-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="stream-modal-title" data-i18n="streams.add">Add Picture Stream</h2>
|
<h2 id="stream-modal-title" data-i18n="streams.add">Add Stream</h2>
|
||||||
<button class="modal-close-btn" onclick="closeStreamModal()" title="Close">✕</button>
|
<button class="modal-close-btn" onclick="closeStreamModal()" title="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app.title": "WLED Screen Controller",
|
"app.title": "WLED Grab",
|
||||||
"app.version": "Version:",
|
"app.version": "Version:",
|
||||||
"theme.toggle": "Toggle theme",
|
"theme.toggle": "Toggle theme",
|
||||||
"locale.change": "Change language",
|
"locale.change": "Change language",
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"auth.logout": "Logout",
|
"auth.logout": "Logout",
|
||||||
"auth.authenticated": "● Authenticated",
|
"auth.authenticated": "● Authenticated",
|
||||||
"auth.title": "Login to WLED Controller",
|
"auth.title": "Login to WLED Controller",
|
||||||
"auth.message": "Please enter your API key to authenticate and access the WLED Screen Controller.",
|
"auth.message": "Please enter your API key to authenticate and access the WLED Grab.",
|
||||||
"auth.label": "API Key:",
|
"auth.label": "API Key:",
|
||||||
"auth.placeholder": "Enter your API key...",
|
"auth.placeholder": "Enter your API key...",
|
||||||
"auth.hint": "Your API key will be stored securely in your browser's local storage.",
|
"auth.hint": "Your API key will be stored securely in your browser's local storage.",
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
"displays.picker.title": "Select a Display",
|
"displays.picker.title": "Select a Display",
|
||||||
"displays.picker.select": "Select display...",
|
"displays.picker.select": "Select display...",
|
||||||
"displays.picker.click_to_select": "Click to select this display",
|
"displays.picker.click_to_select": "Click to select this display",
|
||||||
"templates.title": "\uD83C\uDFAF Capture Templates",
|
"templates.title": "\uD83D\uDCC4 Engine 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.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.loading": "Loading templates...",
|
||||||
"templates.empty": "No capture templates configured",
|
"templates.empty": "No capture templates configured",
|
||||||
"templates.add": "Add Capture Template",
|
"templates.add": "Add Engine Template",
|
||||||
"templates.edit": "Edit Capture Template",
|
"templates.edit": "Edit Engine Template",
|
||||||
"templates.name": "Template Name:",
|
"templates.name": "Template Name:",
|
||||||
"templates.name.placeholder": "My Custom Template",
|
"templates.name.placeholder": "My Custom Template",
|
||||||
"templates.description.label": "Description (optional):",
|
"templates.description.label": "Description (optional):",
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
"settings.display_index.hint": "Which screen to capture for this device",
|
"settings.display_index.hint": "Which screen to capture for this device",
|
||||||
"settings.fps": "Target FPS:",
|
"settings.fps": "Target FPS:",
|
||||||
"settings.fps.hint": "Target frames per second (10-90)",
|
"settings.fps.hint": "Target frames per second (10-90)",
|
||||||
"settings.capture_template": "Capture Template:",
|
"settings.capture_template": "Engine Template:",
|
||||||
"settings.capture_template.hint": "Screen capture engine and configuration for this device",
|
"settings.capture_template.hint": "Screen capture engine and configuration for this device",
|
||||||
"settings.button.cancel": "Cancel",
|
"settings.button.cancel": "Cancel",
|
||||||
"settings.health_interval": "Health Check Interval (s):",
|
"settings.health_interval": "Health Check Interval (s):",
|
||||||
@@ -198,14 +198,15 @@
|
|||||||
"confirm.no": "No",
|
"confirm.no": "No",
|
||||||
"common.delete": "Delete",
|
"common.delete": "Delete",
|
||||||
"common.edit": "Edit",
|
"common.edit": "Edit",
|
||||||
"streams.title": "\uD83D\uDCFA Picture Streams",
|
"streams.title": "\uD83D\uDCFA Streams",
|
||||||
"streams.description": "Picture 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.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.group.raw": "Screen Capture",
|
"streams.group.raw": "Screen Capture",
|
||||||
"streams.group.processed": "Processed",
|
"streams.group.processed": "Processed",
|
||||||
"streams.add": "Add Picture Stream",
|
"streams.section.streams": "\uD83D\uDCFA Streams",
|
||||||
|
"streams.add": "Add Stream",
|
||||||
"streams.add.raw": "Add Screen Capture",
|
"streams.add.raw": "Add Screen Capture",
|
||||||
"streams.add.processed": "Add Processed Stream",
|
"streams.add.processed": "Add Processed Stream",
|
||||||
"streams.edit": "Edit Picture Stream",
|
"streams.edit": "Edit Stream",
|
||||||
"streams.edit.raw": "Edit Screen Capture",
|
"streams.edit.raw": "Edit Screen Capture",
|
||||||
"streams.edit.processed": "Edit Processed Stream",
|
"streams.edit.processed": "Edit Processed Stream",
|
||||||
"streams.name": "Stream Name:",
|
"streams.name": "Stream Name:",
|
||||||
@@ -214,10 +215,10 @@
|
|||||||
"streams.type.raw": "Screen Capture",
|
"streams.type.raw": "Screen Capture",
|
||||||
"streams.type.processed": "Processed",
|
"streams.type.processed": "Processed",
|
||||||
"streams.display": "Display:",
|
"streams.display": "Display:",
|
||||||
"streams.capture_template": "Capture Template:",
|
"streams.capture_template": "Engine Template:",
|
||||||
"streams.target_fps": "Target FPS:",
|
"streams.target_fps": "Target FPS:",
|
||||||
"streams.source": "Source Stream:",
|
"streams.source": "Source Stream:",
|
||||||
"streams.pp_template": "Processing Template:",
|
"streams.pp_template": "Filter Template:",
|
||||||
"streams.description_label": "Description (optional):",
|
"streams.description_label": "Description (optional):",
|
||||||
"streams.description_placeholder": "Describe this stream...",
|
"streams.description_placeholder": "Describe this stream...",
|
||||||
"streams.created": "Stream created successfully",
|
"streams.created": "Stream created successfully",
|
||||||
@@ -227,17 +228,17 @@
|
|||||||
"streams.error.load": "Failed to load streams",
|
"streams.error.load": "Failed to load streams",
|
||||||
"streams.error.required": "Please fill in all required fields",
|
"streams.error.required": "Please fill in all required fields",
|
||||||
"streams.error.delete": "Failed to delete stream",
|
"streams.error.delete": "Failed to delete stream",
|
||||||
"streams.test.title": "Test Picture Stream",
|
"streams.test.title": "Test Stream",
|
||||||
"streams.test.run": "🧪 Run Test",
|
"streams.test.run": "🧪 Run Test",
|
||||||
"streams.test.running": "Testing stream...",
|
"streams.test.running": "Testing stream...",
|
||||||
"streams.test.duration": "Capture Duration (s):",
|
"streams.test.duration": "Capture Duration (s):",
|
||||||
"streams.test.error.failed": "Stream test failed",
|
"streams.test.error.failed": "Stream test failed",
|
||||||
"postprocessing.title": "\uD83C\uDFA8 Processing Templates",
|
"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 streams for consistent postprocessing across devices.",
|
||||||
"postprocessing.add": "Add Processing Template",
|
"postprocessing.add": "Add Filter Template",
|
||||||
"postprocessing.edit": "Edit Processing Template",
|
"postprocessing.edit": "Edit Filter Template",
|
||||||
"postprocessing.name": "Template Name:",
|
"postprocessing.name": "Template Name:",
|
||||||
"postprocessing.name.placeholder": "My Processing Template",
|
"postprocessing.name.placeholder": "My Filter Template",
|
||||||
"filters.select_type": "Select filter type...",
|
"filters.select_type": "Select filter type...",
|
||||||
"filters.add": "Add Filter",
|
"filters.add": "Add Filter",
|
||||||
"filters.remove": "Remove",
|
"filters.remove": "Remove",
|
||||||
@@ -255,20 +256,20 @@
|
|||||||
"postprocessing.created": "Template created successfully",
|
"postprocessing.created": "Template created successfully",
|
||||||
"postprocessing.updated": "Template updated successfully",
|
"postprocessing.updated": "Template updated successfully",
|
||||||
"postprocessing.deleted": "Template deleted successfully",
|
"postprocessing.deleted": "Template deleted successfully",
|
||||||
"postprocessing.delete.confirm": "Are you sure you want to delete this processing template?",
|
"postprocessing.delete.confirm": "Are you sure you want to delete this filter template?",
|
||||||
"postprocessing.error.load": "Failed to load processing templates",
|
"postprocessing.error.load": "Failed to load processing templates",
|
||||||
"postprocessing.error.required": "Please fill in all required fields",
|
"postprocessing.error.required": "Please fill in all required fields",
|
||||||
"postprocessing.error.delete": "Failed to delete processing template",
|
"postprocessing.error.delete": "Failed to delete processing template",
|
||||||
"postprocessing.config.show": "Show settings",
|
"postprocessing.config.show": "Show settings",
|
||||||
"postprocessing.test.title": "Test Processing Template",
|
"postprocessing.test.title": "Test Filter Template",
|
||||||
"postprocessing.test.source_stream": "Source Stream:",
|
"postprocessing.test.source_stream": "Source Stream:",
|
||||||
"postprocessing.test.running": "Testing processing template...",
|
"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 stream",
|
||||||
"postprocessing.test.error.failed": "Processing template test failed",
|
"postprocessing.test.error.failed": "Processing template test failed",
|
||||||
"device.button.stream_selector": "Stream Settings",
|
"device.button.stream_selector": "Stream Settings",
|
||||||
"device.stream_settings.title": "📺 Stream Settings",
|
"device.stream_settings.title": "📺 Stream Settings",
|
||||||
"device.stream_selector.label": "Picture Stream:",
|
"device.stream_selector.label": "Stream:",
|
||||||
"device.stream_selector.hint": "Select a picture stream that defines what this device captures and processes",
|
"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.none": "-- No stream assigned --",
|
||||||
"device.stream_selector.saved": "Stream settings updated",
|
"device.stream_selector.saved": "Stream settings updated",
|
||||||
"device.stream_settings.border_width": "Border Width (px):",
|
"device.stream_settings.border_width": "Border Width (px):",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"app.title": "WLED Контроллер Экрана",
|
"app.title": "WLED Grab",
|
||||||
"app.version": "Версия:",
|
"app.version": "Версия:",
|
||||||
"theme.toggle": "Переключить тему",
|
"theme.toggle": "Переключить тему",
|
||||||
"locale.change": "Изменить язык",
|
"locale.change": "Изменить язык",
|
||||||
"auth.login": "Войти",
|
"auth.login": "Войти",
|
||||||
"auth.logout": "Выйти",
|
"auth.logout": "Выйти",
|
||||||
"auth.authenticated": "● Авторизован",
|
"auth.authenticated": "● Авторизован",
|
||||||
"auth.title": "Вход в WLED Контроллер",
|
"auth.title": "Вход в WLED Grab",
|
||||||
"auth.message": "Пожалуйста, введите ваш API ключ для аутентификации и доступа к WLED Контроллеру Экрана.",
|
"auth.message": "Пожалуйста, введите ваш API ключ для аутентификации и доступа к WLED Grab.",
|
||||||
"auth.label": "API Ключ:",
|
"auth.label": "API Ключ:",
|
||||||
"auth.placeholder": "Введите ваш API ключ...",
|
"auth.placeholder": "Введите ваш API ключ...",
|
||||||
"auth.hint": "Ваш API ключ будет безопасно сохранен в локальном хранилище браузера.",
|
"auth.hint": "Ваш API ключ будет безопасно сохранен в локальном хранилище браузера.",
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
"displays.picker.title": "Выберите Дисплей",
|
"displays.picker.title": "Выберите Дисплей",
|
||||||
"displays.picker.select": "Выберите дисплей...",
|
"displays.picker.select": "Выберите дисплей...",
|
||||||
"displays.picker.click_to_select": "Нажмите, чтобы выбрать этот дисплей",
|
"displays.picker.click_to_select": "Нажмите, чтобы выбрать этот дисплей",
|
||||||
"templates.title": "\uD83C\uDFAF Шаблоны Захвата",
|
"templates.title": "\uD83D\uDCC4 Шаблоны Движков",
|
||||||
"templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.",
|
"templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.",
|
||||||
"templates.loading": "Загрузка шаблонов...",
|
"templates.loading": "Загрузка шаблонов...",
|
||||||
"templates.empty": "Шаблоны захвата не настроены",
|
"templates.empty": "Шаблоны захвата не настроены",
|
||||||
"templates.add": "Добавить Шаблон Захвата",
|
"templates.add": "Добавить Шаблон Движка",
|
||||||
"templates.edit": "Редактировать Шаблон Захвата",
|
"templates.edit": "Редактировать Шаблон Движка",
|
||||||
"templates.name": "Имя Шаблона:",
|
"templates.name": "Имя Шаблона:",
|
||||||
"templates.name.placeholder": "Мой Пользовательский Шаблон",
|
"templates.name.placeholder": "Мой Пользовательский Шаблон",
|
||||||
"templates.description.label": "Описание (необязательно):",
|
"templates.description.label": "Описание (необязательно):",
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
"settings.display_index.hint": "Какой экран захватывать для этого устройства",
|
"settings.display_index.hint": "Какой экран захватывать для этого устройства",
|
||||||
"settings.fps": "Целевой FPS:",
|
"settings.fps": "Целевой FPS:",
|
||||||
"settings.fps.hint": "Целевая частота кадров (10-90)",
|
"settings.fps.hint": "Целевая частота кадров (10-90)",
|
||||||
"settings.capture_template": "Шаблон Захвата:",
|
"settings.capture_template": "Шаблон Движка:",
|
||||||
"settings.capture_template.hint": "Движок захвата экрана и конфигурация для этого устройства",
|
"settings.capture_template.hint": "Движок захвата экрана и конфигурация для этого устройства",
|
||||||
"settings.button.cancel": "Отмена",
|
"settings.button.cancel": "Отмена",
|
||||||
"settings.health_interval": "Интервал Проверки (с):",
|
"settings.health_interval": "Интервал Проверки (с):",
|
||||||
@@ -198,14 +198,15 @@
|
|||||||
"confirm.no": "Нет",
|
"confirm.no": "Нет",
|
||||||
"common.delete": "Удалить",
|
"common.delete": "Удалить",
|
||||||
"common.edit": "Редактировать",
|
"common.edit": "Редактировать",
|
||||||
"streams.title": "\uD83D\uDCFA Видеопотоки",
|
"streams.title": "\uD83D\uDCFA Потоки",
|
||||||
"streams.description": "Видеопотоки определяют конвейер захвата. Сырой поток захватывает экран с помощью шаблона захвата. Обработанный поток применяет постобработку к другому потоку. Назначайте потоки устройствам.",
|
"streams.description": "Потоки определяют конвейер захвата. Сырой поток захватывает экран с помощью шаблона захвата. Обработанный поток применяет постобработку к другому потоку. Назначайте потоки устройствам.",
|
||||||
"streams.group.raw": "Захват Экрана",
|
"streams.group.raw": "Захват Экрана",
|
||||||
"streams.group.processed": "Обработанные",
|
"streams.group.processed": "Обработанные",
|
||||||
"streams.add": "Добавить Видеопоток",
|
"streams.section.streams": "\uD83D\uDCFA Потоки",
|
||||||
|
"streams.add": "Добавить Поток",
|
||||||
"streams.add.raw": "Добавить Захват Экрана",
|
"streams.add.raw": "Добавить Захват Экрана",
|
||||||
"streams.add.processed": "Добавить Обработанный",
|
"streams.add.processed": "Добавить Обработанный",
|
||||||
"streams.edit": "Редактировать Видеопоток",
|
"streams.edit": "Редактировать Поток",
|
||||||
"streams.edit.raw": "Редактировать Захват Экрана",
|
"streams.edit.raw": "Редактировать Захват Экрана",
|
||||||
"streams.edit.processed": "Редактировать Обработанный Поток",
|
"streams.edit.processed": "Редактировать Обработанный Поток",
|
||||||
"streams.name": "Имя Потока:",
|
"streams.name": "Имя Потока:",
|
||||||
@@ -214,10 +215,10 @@
|
|||||||
"streams.type.raw": "Захват экрана",
|
"streams.type.raw": "Захват экрана",
|
||||||
"streams.type.processed": "Обработанный",
|
"streams.type.processed": "Обработанный",
|
||||||
"streams.display": "Дисплей:",
|
"streams.display": "Дисплей:",
|
||||||
"streams.capture_template": "Шаблон Захвата:",
|
"streams.capture_template": "Шаблон Движка:",
|
||||||
"streams.target_fps": "Целевой FPS:",
|
"streams.target_fps": "Целевой FPS:",
|
||||||
"streams.source": "Исходный Поток:",
|
"streams.source": "Исходный Поток:",
|
||||||
"streams.pp_template": "Шаблон Обработки:",
|
"streams.pp_template": "Шаблон Фильтра:",
|
||||||
"streams.description_label": "Описание (необязательно):",
|
"streams.description_label": "Описание (необязательно):",
|
||||||
"streams.description_placeholder": "Опишите этот поток...",
|
"streams.description_placeholder": "Опишите этот поток...",
|
||||||
"streams.created": "Поток успешно создан",
|
"streams.created": "Поток успешно создан",
|
||||||
@@ -227,17 +228,17 @@
|
|||||||
"streams.error.load": "Не удалось загрузить потоки",
|
"streams.error.load": "Не удалось загрузить потоки",
|
||||||
"streams.error.required": "Пожалуйста, заполните все обязательные поля",
|
"streams.error.required": "Пожалуйста, заполните все обязательные поля",
|
||||||
"streams.error.delete": "Не удалось удалить поток",
|
"streams.error.delete": "Не удалось удалить поток",
|
||||||
"streams.test.title": "Тест Видеопотока",
|
"streams.test.title": "Тест Потока",
|
||||||
"streams.test.run": "🧪 Запустить Тест",
|
"streams.test.run": "🧪 Запустить Тест",
|
||||||
"streams.test.running": "Тестирование потока...",
|
"streams.test.running": "Тестирование потока...",
|
||||||
"streams.test.duration": "Длительность Захвата (с):",
|
"streams.test.duration": "Длительность Захвата (с):",
|
||||||
"streams.test.error.failed": "Тест потока не удался",
|
"streams.test.error.failed": "Тест потока не удался",
|
||||||
"postprocessing.title": "\uD83C\uDFA8 Шаблоны Обработки",
|
"postprocessing.title": "\uD83D\uDCC4 Шаблоны Фильтров",
|
||||||
"postprocessing.description": "Шаблоны обработки определяют фильтры изображений и цветокоррекцию. Назначайте их обработанным видеопотокам для единообразной постобработки на всех устройствах.",
|
"postprocessing.description": "Шаблоны обработки определяют фильтры изображений и цветокоррекцию. Назначайте их обработанным видеопотокам для единообразной постобработки на всех устройствах.",
|
||||||
"postprocessing.add": "Добавить Шаблон Обработки",
|
"postprocessing.add": "Добавить Шаблон Фильтра",
|
||||||
"postprocessing.edit": "Редактировать Шаблон Обработки",
|
"postprocessing.edit": "Редактировать Шаблон Фильтра",
|
||||||
"postprocessing.name": "Имя Шаблона:",
|
"postprocessing.name": "Имя Шаблона:",
|
||||||
"postprocessing.name.placeholder": "Мой Шаблон Обработки",
|
"postprocessing.name.placeholder": "Мой Шаблон Фильтра",
|
||||||
"filters.select_type": "Выберите тип фильтра...",
|
"filters.select_type": "Выберите тип фильтра...",
|
||||||
"filters.add": "Добавить фильтр",
|
"filters.add": "Добавить фильтр",
|
||||||
"filters.remove": "Удалить",
|
"filters.remove": "Удалить",
|
||||||
@@ -255,20 +256,20 @@
|
|||||||
"postprocessing.created": "Шаблон успешно создан",
|
"postprocessing.created": "Шаблон успешно создан",
|
||||||
"postprocessing.updated": "Шаблон успешно обновлён",
|
"postprocessing.updated": "Шаблон успешно обновлён",
|
||||||
"postprocessing.deleted": "Шаблон успешно удалён",
|
"postprocessing.deleted": "Шаблон успешно удалён",
|
||||||
"postprocessing.delete.confirm": "Вы уверены, что хотите удалить этот шаблон обработки?",
|
"postprocessing.delete.confirm": "Вы уверены, что хотите удалить этот шаблон фильтра?",
|
||||||
"postprocessing.error.load": "Не удалось загрузить шаблоны обработки",
|
"postprocessing.error.load": "Не удалось загрузить шаблоны фильтров",
|
||||||
"postprocessing.error.required": "Пожалуйста, заполните все обязательные поля",
|
"postprocessing.error.required": "Пожалуйста, заполните все обязательные поля",
|
||||||
"postprocessing.error.delete": "Не удалось удалить шаблон обработки",
|
"postprocessing.error.delete": "Не удалось удалить шаблон фильтра",
|
||||||
"postprocessing.config.show": "Показать настройки",
|
"postprocessing.config.show": "Показать настройки",
|
||||||
"postprocessing.test.title": "Тест шаблона обработки",
|
"postprocessing.test.title": "Тест шаблона фильтра",
|
||||||
"postprocessing.test.source_stream": "Источник потока:",
|
"postprocessing.test.source_stream": "Источник потока:",
|
||||||
"postprocessing.test.running": "Тестирование шаблона обработки...",
|
"postprocessing.test.running": "Тестирование шаблона фильтра...",
|
||||||
"postprocessing.test.error.no_stream": "Пожалуйста, выберите источник потока",
|
"postprocessing.test.error.no_stream": "Пожалуйста, выберите источник потока",
|
||||||
"postprocessing.test.error.failed": "Тест шаблона обработки не удался",
|
"postprocessing.test.error.failed": "Тест шаблона фильтра не удался",
|
||||||
"device.button.stream_selector": "Настройки потока",
|
"device.button.stream_selector": "Настройки потока",
|
||||||
"device.stream_settings.title": "📺 Настройки потока",
|
"device.stream_settings.title": "📺 Настройки потока",
|
||||||
"device.stream_selector.label": "Видеопоток:",
|
"device.stream_selector.label": "Поток:",
|
||||||
"device.stream_selector.hint": "Выберите видеопоток, определяющий что это устройство захватывает и обрабатывает",
|
"device.stream_selector.hint": "Выберите поток, определяющий что это устройство захватывает и обрабатывает",
|
||||||
"device.stream_selector.none": "-- Поток не назначен --",
|
"device.stream_selector.none": "-- Поток не назначен --",
|
||||||
"device.stream_selector.saved": "Настройки потока обновлены",
|
"device.stream_selector.saved": "Настройки потока обновлены",
|
||||||
"device.stream_settings.border_width": "Ширина границы (px):",
|
"device.stream_settings.border_width": "Ширина границы (px):",
|
||||||
|
|||||||
@@ -2230,6 +2230,24 @@ input:-webkit-autofill:focus {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sub-tab content sections */
|
||||||
|
.subtab-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtab-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtab-section-header {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* Image Lightbox */
|
/* Image Lightbox */
|
||||||
.lightbox {
|
.lightbox {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user