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:
@@ -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 =====
|
||||
|
||||
Reference in New Issue
Block a user