Replace display selection dropdowns with visual display picker lightbox
Remove the Displays tab and replace <select> dropdowns in stream and template test modals with a lightbox overlay showing the spatial display layout. Clicking a display selects it. Uses percentage-based positioning so the layout always fits its container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,11 +56,125 @@ function closeLightbox(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && document.getElementById('image-lightbox').classList.contains('active')) {
|
if (e.key === 'Escape') {
|
||||||
|
if (document.getElementById('display-picker-lightbox').classList.contains('active')) {
|
||||||
|
closeDisplayPicker();
|
||||||
|
} else if (document.getElementById('image-lightbox').classList.contains('active')) {
|
||||||
closeLightbox();
|
closeLightbox();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Display picker lightbox
|
||||||
|
let _displayPickerCallback = null;
|
||||||
|
|
||||||
|
function openDisplayPicker(callback) {
|
||||||
|
_displayPickerCallback = callback;
|
||||||
|
const lightbox = document.getElementById('display-picker-lightbox');
|
||||||
|
const canvas = document.getElementById('display-picker-canvas');
|
||||||
|
|
||||||
|
lightbox.classList.add('active');
|
||||||
|
|
||||||
|
// Defer render to next frame so the lightbox has been laid out and canvas has dimensions
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (_cachedDisplays && _cachedDisplays.length > 0) {
|
||||||
|
renderDisplayPickerLayout(_cachedDisplays);
|
||||||
|
} else {
|
||||||
|
canvas.innerHTML = '<div class="loading-spinner"></div>';
|
||||||
|
loadDisplays().then(() => {
|
||||||
|
if (_cachedDisplays && _cachedDisplays.length > 0) {
|
||||||
|
renderDisplayPickerLayout(_cachedDisplays);
|
||||||
|
} else {
|
||||||
|
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDisplayPicker(event) {
|
||||||
|
if (event && event.target && event.target.closest('.display-picker-content')) return;
|
||||||
|
const lightbox = document.getElementById('display-picker-lightbox');
|
||||||
|
lightbox.classList.remove('active');
|
||||||
|
_displayPickerCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDisplay(displayIndex) {
|
||||||
|
if (_displayPickerCallback) {
|
||||||
|
const display = _cachedDisplays ? _cachedDisplays.find(d => d.index === displayIndex) : null;
|
||||||
|
_displayPickerCallback(displayIndex, display);
|
||||||
|
}
|
||||||
|
closeDisplayPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDisplayPickerLayout(displays) {
|
||||||
|
const canvas = document.getElementById('display-picker-canvas');
|
||||||
|
|
||||||
|
if (!displays || displays.length === 0) {
|
||||||
|
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
|
displays.forEach(display => {
|
||||||
|
minX = Math.min(minX, display.x);
|
||||||
|
minY = Math.min(minY, display.y);
|
||||||
|
maxX = Math.max(maxX, display.x + display.width);
|
||||||
|
maxY = Math.max(maxY, display.y + display.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalWidth = maxX - minX;
|
||||||
|
const totalHeight = maxY - minY;
|
||||||
|
const aspect = totalHeight / totalWidth;
|
||||||
|
|
||||||
|
// Use percentage-based positioning so layout always fits its container
|
||||||
|
const displayElements = displays.map(display => {
|
||||||
|
const leftPct = ((display.x - minX) / totalWidth) * 100;
|
||||||
|
const topPct = ((display.y - minY) / totalHeight) * 100;
|
||||||
|
const widthPct = (display.width / totalWidth) * 100;
|
||||||
|
const heightPct = (display.height / totalHeight) * 100;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="layout-display layout-display-pickable ${display.is_primary ? 'primary' : 'secondary'}"
|
||||||
|
style="left: ${leftPct}%; top: ${topPct}%; width: ${widthPct}%; height: ${heightPct}%;"
|
||||||
|
onclick="selectDisplay(${display.index})"
|
||||||
|
title="${t('displays.picker.click_to_select')}">
|
||||||
|
<div class="layout-position-label">(${display.x}, ${display.y})</div>
|
||||||
|
<div class="layout-index-label">#${display.index}</div>
|
||||||
|
<div class="layout-display-label">
|
||||||
|
<strong>${display.name}</strong>
|
||||||
|
<small>${display.width}×${display.height}</small>
|
||||||
|
<small>${display.refresh_rate}Hz</small>
|
||||||
|
</div>
|
||||||
|
${display.is_primary ? '<div class="primary-indicator">★</div>' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
canvas.innerHTML = `
|
||||||
|
<div class="layout-container" style="width: 100%; padding-bottom: ${aspect * 100}%; position: relative;">
|
||||||
|
${displayElements}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDisplayLabel(displayIndex, display) {
|
||||||
|
if (display) {
|
||||||
|
return `#${display.index}: ${display.width}×${display.height}${display.is_primary ? ' (' + t('displays.badge.primary') + ')' : ''}`;
|
||||||
|
}
|
||||||
|
return `Display ${displayIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStreamDisplaySelected(displayIndex, display) {
|
||||||
|
document.getElementById('stream-display-index').value = displayIndex;
|
||||||
|
document.getElementById('stream-display-picker-label').textContent = formatDisplayLabel(displayIndex, display);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTestDisplaySelected(displayIndex, display) {
|
||||||
|
document.getElementById('test-template-display').value = displayIndex;
|
||||||
|
document.getElementById('test-display-picker-label').textContent = formatDisplayLabel(displayIndex, display);
|
||||||
|
}
|
||||||
|
|
||||||
// Locale management
|
// Locale management
|
||||||
let currentLocale = 'en';
|
let currentLocale = 'en';
|
||||||
let translations = {};
|
let translations = {};
|
||||||
@@ -346,23 +460,11 @@ async function loadDisplays() {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const container = document.getElementById('displays-list');
|
if (data.displays && data.displays.length > 0) {
|
||||||
|
|
||||||
if (!data.displays || data.displays.length === 0) {
|
|
||||||
container.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
|
||||||
document.getElementById('display-layout-canvas').innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache and render visual layout
|
|
||||||
_cachedDisplays = data.displays;
|
_cachedDisplays = data.displays;
|
||||||
renderDisplayLayout(data.displays);
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load displays:', error);
|
console.error('Failed to load displays:', error);
|
||||||
document.getElementById('displays-list').innerHTML =
|
|
||||||
`<div class="loading">${t('displays.failed')}</div>`;
|
|
||||||
document.getElementById('display-layout-canvas').innerHTML =
|
|
||||||
`<div class="loading">${t('displays.failed')}</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,9 +474,6 @@ function switchTab(name) {
|
|||||||
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 === 'displays' && _cachedDisplays) {
|
|
||||||
requestAnimationFrame(() => renderDisplayLayout(_cachedDisplays));
|
|
||||||
}
|
|
||||||
if (name === 'templates') {
|
if (name === 'templates') {
|
||||||
loadCaptureTemplates();
|
loadCaptureTemplates();
|
||||||
}
|
}
|
||||||
@@ -393,66 +492,6 @@ function initTabs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDisplayLayout(displays) {
|
|
||||||
const canvas = document.getElementById('display-layout-canvas');
|
|
||||||
|
|
||||||
if (!displays || displays.length === 0) {
|
|
||||||
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate bounding box for all displays
|
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
||||||
|
|
||||||
displays.forEach(display => {
|
|
||||||
minX = Math.min(minX, display.x);
|
|
||||||
minY = Math.min(minY, display.y);
|
|
||||||
maxX = Math.max(maxX, display.x + display.width);
|
|
||||||
maxY = Math.max(maxY, display.y + display.height);
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalWidth = maxX - minX;
|
|
||||||
const totalHeight = maxY - minY;
|
|
||||||
|
|
||||||
// Scale factor to fit in canvas (respect available width, maintain aspect ratio)
|
|
||||||
const availableWidth = canvas.clientWidth - 60; // account for padding
|
|
||||||
const maxCanvasHeight = 350;
|
|
||||||
const scaleX = availableWidth / totalWidth;
|
|
||||||
const scaleY = maxCanvasHeight / totalHeight;
|
|
||||||
const scale = Math.min(scaleX, scaleY);
|
|
||||||
|
|
||||||
const canvasWidth = totalWidth * scale;
|
|
||||||
const canvasHeight = totalHeight * scale;
|
|
||||||
|
|
||||||
// Create display elements
|
|
||||||
const displayElements = displays.map(display => {
|
|
||||||
const left = (display.x - minX) * scale;
|
|
||||||
const top = (display.y - minY) * scale;
|
|
||||||
const width = display.width * scale;
|
|
||||||
const height = display.height * scale;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="layout-display ${display.is_primary ? 'primary' : 'secondary'}"
|
|
||||||
style="left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px;"
|
|
||||||
title="${display.name}\n${display.width}×${display.height}\nPosition: (${display.x}, ${display.y})">
|
|
||||||
<div class="layout-position-label">(${display.x}, ${display.y})</div>
|
|
||||||
<div class="layout-index-label">#${display.index}</div>
|
|
||||||
<div class="layout-display-label">
|
|
||||||
<strong>${display.name}</strong>
|
|
||||||
<small>${display.width}×${display.height}</small>
|
|
||||||
<small>${display.refresh_rate}Hz</small>
|
|
||||||
</div>
|
|
||||||
${display.is_primary ? '<div class="primary-indicator">★</div>' : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
canvas.innerHTML = `
|
|
||||||
<div class="layout-container" style="width: ${canvasWidth}px; height: ${canvasHeight}px; margin: 0 auto; position: relative;">
|
|
||||||
${displayElements}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load devices
|
// Load devices
|
||||||
async function loadDevices() {
|
async function loadDevices() {
|
||||||
@@ -2815,33 +2854,31 @@ function collectEngineConfig() {
|
|||||||
// Load displays for test selector
|
// Load displays for test selector
|
||||||
async function loadDisplaysForTest() {
|
async function loadDisplaysForTest() {
|
||||||
try {
|
try {
|
||||||
|
if (!_cachedDisplays) {
|
||||||
const response = await fetchWithAuth('/config/displays');
|
const response = await fetchWithAuth('/config/displays');
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error(`Failed to load displays: ${response.status}`);
|
||||||
throw new Error(`Failed to load displays: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const displaysData = await response.json();
|
const displaysData = await response.json();
|
||||||
const select = document.getElementById('test-template-display');
|
_cachedDisplays = displaysData.displays || [];
|
||||||
select.innerHTML = '';
|
|
||||||
|
|
||||||
let primaryIndex = null;
|
|
||||||
(displaysData.displays || []).forEach(display => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = display.index;
|
|
||||||
option.textContent = `Display ${display.index} (${display.width}x${display.height})`;
|
|
||||||
if (display.is_primary) {
|
|
||||||
option.textContent += ' ★';
|
|
||||||
primaryIndex = display.index;
|
|
||||||
}
|
}
|
||||||
select.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-select: last used display, or primary as fallback
|
// Auto-select: last used display, or primary as fallback
|
||||||
|
let selectedIndex = null;
|
||||||
const lastDisplay = localStorage.getItem('lastTestDisplayIndex');
|
const lastDisplay = localStorage.getItem('lastTestDisplayIndex');
|
||||||
if (lastDisplay !== null && select.querySelector(`option[value="${lastDisplay}"]`)) {
|
|
||||||
select.value = lastDisplay;
|
if (lastDisplay !== null) {
|
||||||
} else if (primaryIndex !== null) {
|
const found = _cachedDisplays.find(d => d.index === parseInt(lastDisplay));
|
||||||
select.value = String(primaryIndex);
|
if (found) selectedIndex = found.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIndex === null) {
|
||||||
|
const primary = _cachedDisplays.find(d => d.is_primary);
|
||||||
|
if (primary) selectedIndex = primary.index;
|
||||||
|
else if (_cachedDisplays.length > 0) selectedIndex = _cachedDisplays[0].index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIndex !== null) {
|
||||||
|
const display = _cachedDisplays.find(d => d.index === selectedIndex);
|
||||||
|
onTestDisplaySelected(selectedIndex, display);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading displays:', error);
|
console.error('Error loading displays:', error);
|
||||||
@@ -3147,6 +3184,8 @@ async function showAddStreamModal(presetType) {
|
|||||||
document.getElementById('stream-modal-title').textContent = t(titleKey);
|
document.getElementById('stream-modal-title').textContent = t(titleKey);
|
||||||
document.getElementById('stream-form').reset();
|
document.getElementById('stream-form').reset();
|
||||||
document.getElementById('stream-id').value = '';
|
document.getElementById('stream-id').value = '';
|
||||||
|
document.getElementById('stream-display-index').value = '';
|
||||||
|
document.getElementById('stream-display-picker-label').textContent = t('displays.picker.select');
|
||||||
document.getElementById('stream-error').style.display = 'none';
|
document.getElementById('stream-error').style.display = 'none';
|
||||||
document.getElementById('stream-type').value = streamType;
|
document.getElementById('stream-type').value = streamType;
|
||||||
onStreamTypeChange();
|
onStreamTypeChange();
|
||||||
@@ -3181,7 +3220,9 @@ async function editStream(streamId) {
|
|||||||
await populateStreamModalDropdowns();
|
await populateStreamModalDropdowns();
|
||||||
|
|
||||||
if (stream.stream_type === 'raw') {
|
if (stream.stream_type === 'raw') {
|
||||||
document.getElementById('stream-display-index').value = String(stream.display_index ?? 0);
|
const displayIdx = stream.display_index ?? 0;
|
||||||
|
const display = _cachedDisplays ? _cachedDisplays.find(d => d.index === displayIdx) : null;
|
||||||
|
onStreamDisplaySelected(displayIdx, display);
|
||||||
document.getElementById('stream-capture-template').value = stream.capture_template_id || '';
|
document.getElementById('stream-capture-template').value = stream.capture_template_id || '';
|
||||||
const fps = stream.target_fps ?? 30;
|
const fps = stream.target_fps ?? 30;
|
||||||
document.getElementById('stream-target-fps').value = fps;
|
document.getElementById('stream-target-fps').value = fps;
|
||||||
@@ -3210,23 +3251,16 @@ async function populateStreamModalDropdowns() {
|
|||||||
fetchWithAuth('/postprocessing-templates'),
|
fetchWithAuth('/postprocessing-templates'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Displays
|
// Displays - warm cache for display picker
|
||||||
const displaySelect = document.getElementById('stream-display-index');
|
|
||||||
displaySelect.innerHTML = '';
|
|
||||||
if (displaysRes.ok) {
|
if (displaysRes.ok) {
|
||||||
const displaysData = await displaysRes.json();
|
const displaysData = await displaysRes.json();
|
||||||
(displaysData.displays || []).forEach(d => {
|
_cachedDisplays = displaysData.displays || [];
|
||||||
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');
|
// Auto-select primary display if none selected yet
|
||||||
opt.value = '0';
|
if (!document.getElementById('stream-display-index').value && _cachedDisplays && _cachedDisplays.length > 0) {
|
||||||
opt.textContent = '0';
|
const primary = _cachedDisplays.find(d => d.is_primary) || _cachedDisplays[0];
|
||||||
displaySelect.appendChild(opt);
|
onStreamDisplaySelected(primary.index, primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture templates
|
// Capture templates
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<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="displays" onclick="switchTab('displays')"><span data-i18n="displays.layout">🖥️ Displays</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">📺 Picture 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="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>
|
<button class="tab-btn" data-tab="pp-templates" onclick="switchTab('pp-templates')"><span data-i18n="postprocessing.title">🎨 Processing Templates</span></button>
|
||||||
@@ -55,14 +55,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel" id="tab-displays">
|
|
||||||
<div class="display-layout-preview">
|
|
||||||
<div id="display-layout-canvas" class="display-layout-canvas">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="displays-list" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-panel" id="tab-streams">
|
<div class="tab-panel" id="tab-streams">
|
||||||
<p class="section-tip">
|
<p class="section-tip">
|
||||||
@@ -451,10 +443,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="test-template-display" data-i18n="templates.test.display">Display:</label>
|
<label data-i18n="templates.test.display">Display:</label>
|
||||||
<select id="test-template-display">
|
<input type="hidden" id="test-template-display" value="">
|
||||||
<option value="" data-i18n="templates.test.display.select">Select display...</option>
|
<button type="button" class="btn btn-display-picker" id="test-display-picker-btn" onclick="openDisplayPicker(onTestDisplaySelected)">
|
||||||
</select>
|
<span id="test-display-picker-label" data-i18n="displays.picker.select">Select display...</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -517,8 +510,11 @@
|
|||||||
<!-- Raw stream fields -->
|
<!-- Raw stream fields -->
|
||||||
<div id="stream-raw-fields">
|
<div id="stream-raw-fields">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="stream-display-index" data-i18n="streams.display">Display:</label>
|
<label data-i18n="streams.display">Display:</label>
|
||||||
<select id="stream-display-index"></select>
|
<input type="hidden" id="stream-display-index" value="">
|
||||||
|
<button type="button" class="btn btn-display-picker" id="stream-display-picker-btn" onclick="openDisplayPicker(onStreamDisplaySelected)">
|
||||||
|
<span id="stream-display-picker-label" data-i18n="displays.picker.select">Select display...</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="stream-capture-template" data-i18n="streams.capture_template">Capture Template:</label>
|
<label for="stream-capture-template" data-i18n="streams.capture_template">Capture Template:</label>
|
||||||
@@ -627,6 +623,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Display Picker Lightbox -->
|
||||||
|
<div id="display-picker-lightbox" class="lightbox" onclick="closeDisplayPicker(event)">
|
||||||
|
<button class="lightbox-close" onclick="closeDisplayPicker()" title="Close">✕</button>
|
||||||
|
<div class="lightbox-content display-picker-content">
|
||||||
|
<h3 class="display-picker-title" data-i18n="displays.picker.title">Select a Display</h3>
|
||||||
|
<div id="display-picker-canvas" class="display-picker-canvas">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Initialize theme
|
// Initialize theme
|
||||||
@@ -682,7 +689,6 @@
|
|||||||
|
|
||||||
// Clear the UI
|
// Clear the UI
|
||||||
document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
|
document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
|
||||||
document.getElementById('displays-list').innerHTML = `<div class="loading">${t('auth.please_login')} displays</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on load
|
// Initialize on load
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
"displays.loading": "Loading displays...",
|
"displays.loading": "Loading displays...",
|
||||||
"displays.none": "No displays available",
|
"displays.none": "No displays available",
|
||||||
"displays.failed": "Failed to load displays",
|
"displays.failed": "Failed to load displays",
|
||||||
|
"displays.picker.title": "Select a Display",
|
||||||
|
"displays.picker.select": "Select display...",
|
||||||
|
"displays.picker.click_to_select": "Click to select this display",
|
||||||
"templates.title": "\uD83C\uDFAF Capture Templates",
|
"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.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...",
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
"displays.loading": "Загрузка дисплеев...",
|
"displays.loading": "Загрузка дисплеев...",
|
||||||
"displays.none": "Нет доступных дисплеев",
|
"displays.none": "Нет доступных дисплеев",
|
||||||
"displays.failed": "Не удалось загрузить дисплеи",
|
"displays.failed": "Не удалось загрузить дисплеи",
|
||||||
|
"displays.picker.title": "Выберите Дисплей",
|
||||||
|
"displays.picker.select": "Выберите дисплей...",
|
||||||
|
"displays.picker.click_to_select": "Нажмите, чтобы выбрать этот дисплей",
|
||||||
"templates.title": "\uD83C\uDFAF Шаблоны Захвата",
|
"templates.title": "\uD83C\uDFAF Шаблоны Захвата",
|
||||||
"templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.",
|
"templates.description": "Шаблоны захвата определяют, как захватывается экран. Каждый шаблон использует определённый движок захвата (MSS, DXcam, WGC) с настраиваемыми параметрами. Назначайте шаблоны устройствам для оптимальной производительности.",
|
||||||
"templates.loading": "Загрузка шаблонов...",
|
"templates.loading": "Загрузка шаблонов...",
|
||||||
|
|||||||
@@ -487,32 +487,6 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Display Layout Visualization */
|
/* Display Layout Visualization */
|
||||||
.display-layout-preview {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-layout-preview h3 {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-layout-canvas {
|
|
||||||
background: var(--bg-color);
|
|
||||||
border: 2px dashed var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 30px;
|
|
||||||
min-height: 280px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-container {
|
.layout-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -527,12 +501,10 @@ section {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
cursor: help;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-display:hover {
|
.layout-display:hover {
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
@@ -597,39 +569,6 @@ section {
|
|||||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
|
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-legend {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
padding-top: 15px;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-dot {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 2px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-dot.primary {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
background: rgba(76, 175, 80, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-dot.secondary {
|
|
||||||
border-color: var(--border-color);
|
|
||||||
background: rgba(128, 128, 128, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card brightness slider */
|
/* Card brightness slider */
|
||||||
.brightness-control {
|
.brightness-control {
|
||||||
@@ -2327,6 +2266,58 @@ input:-webkit-autofill:focus {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Display Picker Lightbox */
|
||||||
|
.display-picker-content {
|
||||||
|
max-width: 900px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-picker-title {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-picker-canvas {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-pickable {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-pickable:hover {
|
||||||
|
box-shadow: 0 0 20px rgba(76, 175, 80, 0.4);
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display picker button in forms */
|
||||||
|
.btn-display-picker {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-display-picker:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.templates-grid {
|
.templates-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user