Improve stream UI: grouped sections, full-size preview lightbox, and test redesign

- Separate Screen Capture and Processed streams into grouped sections with headers
- Remove redundant Type dropdown from stream modal (type inferred from add button)
- Add full-resolution image to test endpoints alongside thumbnails
- Add image lightbox with clickable preview for full-size viewing
- Move test results from modal into lightbox overlay with capture stats
- Apply postprocessing to both thumbnail and full image for processed streams
- Rename "Assigned Picture Stream" to "Picture Stream" in device settings
- Fix null reference errors from removed test result HTML elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 00:35:06 +03:00
parent 493f14fba9
commit e8cbc73161
7 changed files with 308 additions and 138 deletions

View File

@@ -30,6 +30,37 @@ function unlockBody() {
document.body.style.paddingRight = '';
}
// Image lightbox
function openLightbox(imageSrc, statsHtml) {
const lightbox = document.getElementById('image-lightbox');
const img = document.getElementById('lightbox-image');
const statsEl = document.getElementById('lightbox-stats');
img.src = imageSrc;
if (statsHtml) {
statsEl.innerHTML = statsHtml;
statsEl.style.display = '';
} else {
statsEl.style.display = 'none';
}
lightbox.classList.add('active');
lockBody();
}
function closeLightbox(event) {
if (event && event.target && event.target.closest('.lightbox-content')) return;
const lightbox = document.getElementById('image-lightbox');
lightbox.classList.remove('active');
document.getElementById('lightbox-image').src = '';
document.getElementById('lightbox-stats').style.display = 'none';
unlockBody();
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.getElementById('image-lightbox').classList.contains('active')) {
closeLightbox();
}
});
// Locale management
let currentLocale = 'en';
let translations = {};
@@ -2638,9 +2669,6 @@ async function showTestTemplateModal(templateId) {
// Restore last used capture duration
restoreCaptureDuration();
// Reset results
document.getElementById('test-template-results').style.display = 'none';
// Show modal
const modal = document.getElementById('test-template-modal');
modal.style.display = 'flex';
@@ -2836,7 +2864,6 @@ async function runTemplateTest() {
}
const template = window.currentTestingTemplate;
const resultsDiv = document.getElementById('test-template-results');
// Show full-page overlay spinner with progress
showOverlaySpinner(t('templates.test.running'), captureDuration);
@@ -2869,25 +2896,23 @@ async function runTemplateTest() {
}
}
// Display test results
function buildTestStatsHtml(result) {
const p = result.performance;
const res = `${result.full_capture.width}x${result.full_capture.height}`;
return `
<div class="stat-item"><span>${t('templates.test.results.duration')}:</span> <strong>${p.capture_duration_s.toFixed(2)}s</strong></div>
<div class="stat-item"><span>${t('templates.test.results.frame_count')}:</span> <strong>${p.frame_count}</strong></div>
<div class="stat-item"><span>${t('templates.test.results.actual_fps')}:</span> <strong>${p.actual_fps.toFixed(1)}</strong></div>
<div class="stat-item"><span>${t('templates.test.results.avg_capture_time')}:</span> <strong>${p.avg_capture_time_ms.toFixed(1)}ms</strong></div>
<div class="stat-item"><span>Resolution:</span> <strong>${res}</strong></div>
`;
}
// Display test results — opens lightbox with stats overlay
function displayTestResults(result) {
const resultsDiv = document.getElementById('test-template-results');
// Hide overlay spinner
hideOverlaySpinner();
// Full capture preview
const previewImg = document.getElementById('test-template-preview-image');
previewImg.innerHTML = `<img src="${result.full_capture.image}" alt="Capture preview" style="max-width: 100%; border-radius: 4px;">`;
// Performance stats
document.getElementById('test-template-actual-duration').textContent = `${result.performance.capture_duration_s.toFixed(2)}s`;
document.getElementById('test-template-frame-count').textContent = result.performance.frame_count;
document.getElementById('test-template-actual-fps').textContent = `${result.performance.actual_fps.toFixed(1)} FPS`;
document.getElementById('test-template-avg-capture-time').textContent = `${result.performance.avg_capture_time_ms.toFixed(1)}ms`;
// Show results
resultsDiv.style.display = 'block';
const fullImageSrc = result.full_capture.full_image || result.full_capture.image;
openLightbox(fullImageSrc, buildTestStatsHtml(result));
}
// Save template
@@ -2989,9 +3014,32 @@ function renderPictureStreamsList(streams) {
const container = document.getElementById('streams-list');
if (streams.length === 0) {
container.innerHTML = `<div class="template-card add-template-card" onclick="showAddStreamModal()">
<div class="add-template-icon">+</div>
<div class="add-template-label">${t('streams.add')}</div>
container.innerHTML = `
<div class="stream-group">
<div class="stream-group-header">
<span class="stream-group-icon">📷</span>
<span class="stream-group-title">${t('streams.group.raw')}</span>
<span class="stream-group-count">0</span>
</div>
<div class="templates-grid">
<div class="template-card add-template-card" onclick="showAddStreamModal('raw')">
<div class="add-template-icon">+</div>
<div class="add-template-label">${t('streams.add.raw')}</div>
</div>
</div>
</div>
<div class="stream-group">
<div class="stream-group-header">
<span class="stream-group-icon">🎨</span>
<span class="stream-group-title">${t('streams.group.processed')}</span>
<span class="stream-group-count">0</span>
</div>
<div class="templates-grid">
<div class="template-card add-template-card" onclick="showAddStreamModal('processed')">
<div class="add-template-icon">+</div>
<div class="add-template-label">${t('streams.add.processed')}</div>
</div>
</div>
</div>`;
return;
}
@@ -3046,10 +3094,41 @@ function renderPictureStreamsList(streams) {
`;
};
let html = streams.map(renderCard).join('');
html += `<div class="template-card add-template-card" onclick="showAddStreamModal()">
<div class="add-template-icon">+</div>
<div class="add-template-label">${t('streams.add')}</div>
const rawStreams = streams.filter(s => s.stream_type === 'raw');
const processedStreams = streams.filter(s => s.stream_type === 'processed');
let html = '';
// Screen Capture streams section
html += `<div class="stream-group">
<div class="stream-group-header">
<span class="stream-group-icon">📷</span>
<span class="stream-group-title">${t('streams.group.raw')}</span>
<span class="stream-group-count">${rawStreams.length}</span>
</div>
<div class="templates-grid">
${rawStreams.map(renderCard).join('')}
<div class="template-card add-template-card" onclick="showAddStreamModal('raw')">
<div class="add-template-icon">+</div>
<div class="add-template-label">${t('streams.add.raw')}</div>
</div>
</div>
</div>`;
// Processed streams section
html += `<div class="stream-group">
<div class="stream-group-header">
<span class="stream-group-icon">🎨</span>
<span class="stream-group-title">${t('streams.group.processed')}</span>
<span class="stream-group-count">${processedStreams.length}</span>
</div>
<div class="templates-grid">
${processedStreams.map(renderCard).join('')}
<div class="template-card add-template-card" onclick="showAddStreamModal('processed')">
<div class="add-template-icon">+</div>
<div class="add-template-label">${t('streams.add.processed')}</div>
</div>
</div>
</div>`;
container.innerHTML = html;
@@ -3061,15 +3140,14 @@ function onStreamTypeChange() {
document.getElementById('stream-processed-fields').style.display = streamType === 'processed' ? '' : 'none';
}
async function showAddStreamModal() {
document.getElementById('stream-modal-title').textContent = t('streams.add');
async function showAddStreamModal(presetType) {
const streamType = presetType || 'raw';
const titleKey = streamType === 'raw' ? 'streams.add.raw' : 'streams.add.processed';
document.getElementById('stream-modal-title').textContent = t(titleKey);
document.getElementById('stream-form').reset();
document.getElementById('stream-id').value = '';
document.getElementById('stream-error').style.display = 'none';
document.getElementById('stream-type').disabled = false;
// Reset to raw type
document.getElementById('stream-type').value = 'raw';
document.getElementById('stream-type').value = streamType;
onStreamTypeChange();
// Populate dropdowns
@@ -3087,15 +3165,15 @@ async function editStream(streamId) {
if (!response.ok) throw new Error(`Failed to load stream: ${response.status}`);
const stream = await response.json();
document.getElementById('stream-modal-title').textContent = t('streams.edit');
const editTitleKey = stream.stream_type === 'raw' ? 'streams.edit.raw' : 'streams.edit.processed';
document.getElementById('stream-modal-title').textContent = t(editTitleKey);
document.getElementById('stream-id').value = streamId;
document.getElementById('stream-name').value = stream.name;
document.getElementById('stream-description').value = stream.description || '';
document.getElementById('stream-error').style.display = 'none';
// Set type and disable changing it for existing streams
// Set type (hidden input)
document.getElementById('stream-type').value = stream.stream_type;
document.getElementById('stream-type').disabled = true;
onStreamTypeChange();
// Populate dropdowns before setting values
@@ -3286,7 +3364,6 @@ let _currentTestStreamId = null;
async function showTestStreamModal(streamId) {
_currentTestStreamId = streamId;
restoreStreamTestDuration();
document.getElementById('test-stream-results').style.display = 'none';
const modal = document.getElementById('test-stream-modal');
modal.style.display = 'flex';
@@ -3340,16 +3417,8 @@ async function runStreamTest() {
function displayStreamTestResults(result) {
hideOverlaySpinner();
const previewImg = document.getElementById('test-stream-preview-image');
previewImg.innerHTML = `<img src="${result.full_capture.image}" alt="Stream preview" style="max-width: 100%; border-radius: 4px;">`;
document.getElementById('test-stream-actual-duration').textContent = `${result.performance.capture_duration_s.toFixed(2)}s`;
document.getElementById('test-stream-frame-count').textContent = result.performance.frame_count;
document.getElementById('test-stream-actual-fps').textContent = `${result.performance.actual_fps.toFixed(1)} FPS`;
document.getElementById('test-stream-avg-capture-time').textContent = `${result.performance.avg_capture_time_ms.toFixed(1)}ms`;
document.getElementById('test-stream-results').style.display = 'block';
const fullImageSrc = result.full_capture.full_image || result.full_capture.image;
openLightbox(fullImageSrc, buildTestStatsHtml(result));
}
// ===== Processing Templates =====