Added camera enabling and disabling

This commit is contained in:
2026-06-04 16:32:30 +02:00
parent 988cb53de1
commit b71a685c58
12 changed files with 11539 additions and 168 deletions
+246 -91
View File
@@ -124,28 +124,21 @@
height: calc(100vh - 50px);
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 20px;
padding: 20px;
box-sizing: border-box;
align-content: stretch;
}
.camera-card {
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
position: relative;
align-items: center;
justify-content: center;
}
.camera-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
@@ -189,12 +182,7 @@
flex: 1;
}
/* Calculate optimal size to fit within grid cell */
@supports (width: min(100%, calc((100vh - 50px - 20px) / 2 - 60px) * 4 / 3)) {
.stream-wrapper {
width: min(100%, calc(((100vh - 50px - 20px) / 2 - 60px) * 4 / 3));
}
}
/* stream-wrapper width is set dynamically via JS (reshapeGrid) */
.stream-placeholder {
position: absolute;
@@ -263,35 +251,7 @@
background: linear-gradient(90deg, #999 0%, #999 60px, #1a1a1a 60px);
}
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 60px, #2d2d2d 60px);
border-bottom-color: #28a745;
}
/* Tooltip for camera cards */
.camera-card::after {
content: attr(data-tooltip);
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.85rem;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
pointer-events: none;
z-index: 10;
}
.camera-card:hover::after {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-5px);
}
/* Settings Panel */
#overlay {
@@ -491,6 +451,157 @@
transform: translateY(-1px);
}
/* Camera settings panel components */
.cam-setting-toggle {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
font-size: 0.9rem;
color: #495057;
cursor: pointer;
border-bottom: 1px solid #e9ecef;
margin-bottom: 14px;
}
.cam-setting-toggle input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #28a745;
}
.cam-slider-row {
margin-bottom: 14px;
}
.cam-slider-label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #495057;
margin-bottom: 6px;
}
.cam-slider-value {
font-weight: 600;
color: #28a745;
}
.cam-slider {
width: 100%;
accent-color: #28a745;
}
.cam-list {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.cam-row {
border: 1.5px solid #dee2e6;
border-radius: 10px;
overflow: hidden;
background: white;
transition: opacity 0.2s ease;
}
.cam-row--disabled {
opacity: 0.55;
}
.cam-row-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.cam-row-label {
font-size: 0.85rem;
font-weight: 600;
color: #495057;
}
.cam-row .camera-input {
border: none;
border-radius: 0;
box-shadow: none;
padding: 10px 12px;
font-size: 0.9rem;
}
.cam-row .camera-input:focus {
border: none;
box-shadow: inset 0 0 0 2px #28a745;
transform: none;
}
.cam-row .camera-input:hover:not(:focus) {
border: none;
box-shadow: inset 0 0 0 2px #28a74580;
transform: none;
}
/* Toggle switch */
.cam-toggle {
display: flex;
align-items: center;
gap: 7px;
cursor: pointer;
}
.cam-toggle input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.cam-toggle-track {
width: 36px;
height: 20px;
background: #dc3545;
border-radius: 10px;
position: relative;
transition: background 0.2s ease;
flex-shrink: 0;
}
.cam-toggle input:checked ~ .cam-toggle-track {
background: #28a745;
}
.cam-toggle-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
transition: transform 0.2s ease;
}
.cam-toggle input:checked ~ .cam-toggle-track .cam-toggle-thumb {
transform: translateX(16px);
}
.cam-toggle-text {
font-size: 0.78rem;
font-weight: 600;
color: #28a745;
min-width: 22px;
}
.cam-toggle-text--off {
color: #dc3545;
}
.tournament-status-info {
background: #f0f8ff;
border: 2px solid #28a745;
@@ -539,7 +650,6 @@
.grid {
height: calc(100vh - 100px);
grid-template-columns: 1fr;
grid-template-rows: repeat(6, 1fr);
gap: 15px;
padding: 20px;
}
@@ -549,9 +659,6 @@
padding: 10px 12px 10px 0;
}
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 50px, #2d2d2d 50px);
}
.target-number {
width: 50px;
@@ -611,9 +718,6 @@
padding: 8px 10px 8px 0;
}
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 45px, #2d2d2d 45px);
}
.target-number {
width: 45px;
@@ -719,46 +823,48 @@
<div class="settings-group" id="displayOptionsGroup">
<h4 data-i18n="league.camera">Kamera</h4>
<div style="margin-bottom: 15px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="toggleTitles" {% if settings.display_options.show_titles %}checked{% endif %} onchange="toggleTitles()" style="width: 18px; height: 18px;">
<span data-i18n="camera.show_card_titles">Prikaiž Naslove Kartic</span>
</label>
<!-- Display toggles -->
<label class="cam-setting-toggle">
<input type="checkbox" id="toggleTitles" {% if settings.display_options.show_titles %}checked{% endif %} onchange="toggleTitles()">
<span data-i18n="camera.show_card_titles">Prikaži Naslove Kartic</span>
</label>
<!-- Sliders -->
<div class="cam-slider-row">
<div class="cam-slider-label">
<span data-i18n="camera.title_text_size">Naslov</span>
<span class="cam-slider-value"><span id="titleSizeValue">{{ settings.display_options.title_size }}</span>rem</span>
</div>
<input type="range" id="titleSize" class="cam-slider" min="0.8" max="2.5" step="0.1"
value="{{ settings.display_options.title_size }}" oninput="adjustTitleSize(this.value)">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;">
<span data-i18n="camera.title_text_size">Velikost Besedila Naslova</span>: <span id="titleSizeValue">{{ settings.display_options.title_size }}</span>rem
</label>
<input type="range"
id="titleSize"
min="0.8"
max="2.5"
step="0.1"
value="{{ settings.display_options.title_size }}"
oninput="adjustTitleSize(this.value)"
style="width: 100%;">
<div class="cam-slider-row">
<div class="cam-slider-label">
<span data-i18n="camera.target_number_size">Številka</span>
<span class="cam-slider-value"><span id="targetNumberSizeValue">{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}</span>rem</span>
</div>
<input type="range" id="targetNumberSize" class="cam-slider" min="0.8" max="2.5" step="0.1"
value="{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}" oninput="adjustTargetNumberSize(this.value)">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;">
<span data-i18n="camera.target_number_size">Velikost Števila Tarče</span>: <span id="targetNumberSizeValue">{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}</span>rem
</label>
<input type="range"
id="targetNumberSize"
min="0.8"
max="2.5"
step="0.1"
value="{{ settings.display_options.get('target_number_size', settings.display_options.title_size) }}"
oninput="adjustTargetNumberSize(this.value)"
style="width: 100%;">
</div>
<div style="margin-top: 20px;">
<h5 data-i18n="camera.titles">Naslovi Kamer</h5>
<!-- Camera rows -->
<div class="cam-list">
{% for i in range(1, 7) %}
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #495057; font-size: 0.9rem;"><span data-i18n="camera.camera">Kamera</span> {{ i }}:</label>
{% set is_disabled = i in settings.get('disabled_cameras', []) %}
<div class="cam-row {% if is_disabled %}cam-row--disabled{% endif %}" id="cameraSettingsRow{{ i }}">
<div class="cam-row-header">
<span class="cam-row-label"><span data-i18n="camera.camera">Kamera</span> {{ i }}</span>
<label class="cam-toggle" id="cameraToggleLabel{{ i }}">
<input type="checkbox" id="cameraEnabled{{ i }}"
{% if not is_disabled %}checked{% endif %}
onchange="toggleCamera({{ i }}, this.checked)">
<span class="cam-toggle-track">
<span class="cam-toggle-thumb"></span>
</span>
<span class="cam-toggle-text {% if is_disabled %}cam-toggle-text--off{% endif %}" id="cameraStatusText{{ i }}">{% if is_disabled %}Off{% else %}On{% endif %}</span>
</label>
</div>
<input type="text"
id="titleInput{{ i }}"
value="{{ settings.camera_titles[i|string] }}"
@@ -779,7 +885,8 @@
<div class="grid">
{% for i in range(1, 7) %}
<div class="camera-card" onclick="openCameraFullscreen({{ i }})" data-camera-id="{{ i }}" data-tooltip="" data-i18n-tooltip="camera.click_to_view_fullscreen">
<div class="camera-card" data-camera-id="{{ i }}"
{% if i in settings.get('disabled_cameras', []) %}style="display:none;"{% endif %}>
<div class="stream-wrapper">
<div class="card-header">
<div class="target-number">{{ i }}</div>
@@ -1174,6 +1281,58 @@
}
}
// Toggle camera enabled/disabled
function reshapeGrid() {
const grid = document.querySelector('.grid');
if (!grid) return;
const activeCount = document.querySelectorAll('.camera-card:not([style*="display: none"]):not([style*="display:none"])').length;
let cols;
if (activeCount <= 1) cols = 1;
else if (activeCount === 2) cols = 2;
else if (activeCount === 3) cols = 3;
else if (activeCount === 4) cols = 2;
else cols = 3;
const rows = Math.ceil(activeCount / cols);
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
// Recompute stream-wrapper max-width so cards fill available height at 4:3
// gap=20px, padding=20px each side, header~40px, toolbar=50px
const gap = 20;
const padding = 40;
const toolbar = 50;
const headerH = 40;
const availH = (window.innerHeight - toolbar - padding - gap * (rows - 1)) / rows - headerH;
const maxW = availH * (4 / 3);
document.querySelectorAll('.stream-wrapper').forEach(w => {
w.style.width = `min(100%, ${maxW}px)`;
});
}
function toggleCamera(index, enabled) {
const card = document.querySelector(`.camera-card[data-camera-id="${index}"]`);
const titleInput = document.getElementById(`titleInput${index}`);
const label = document.getElementById(`cameraToggleLabel${index}`);
const statusText = document.getElementById(`cameraStatusText${index}`);
const row = document.getElementById(`cameraSettingsRow${index}`);
if (enabled) {
if (card) card.style.display = '';
if (row) row.classList.remove('cam-row--disabled');
if (statusText) { statusText.textContent = 'On'; statusText.classList.remove('cam-toggle-text--off'); }
currentSettings.disabled_cameras = (currentSettings.disabled_cameras || []).filter(c => c !== index);
} else {
if (card) card.style.display = 'none';
if (row) row.classList.add('cam-row--disabled');
if (statusText) { statusText.textContent = 'Off'; statusText.classList.add('cam-toggle-text--off'); }
if (!currentSettings.disabled_cameras) currentSettings.disabled_cameras = [];
if (!currentSettings.disabled_cameras.includes(index)) currentSettings.disabled_cameras.push(index);
}
reshapeGrid();
saveSettings({ disabled_cameras: currentSettings.disabled_cameras });
}
// Toggle titles
function toggleTitles() {
const show = document.getElementById('toggleTitles').checked;
@@ -1236,12 +1395,6 @@
}
}
// Open camera fullscreen
function openCameraFullscreen(cameraId) {
const customTitle = currentSettings.camera_titles[cameraId.toString()] || `Camera ${cameraId}`;
window.location.href = `/fullscreen/${cameraId}?title=${encodeURIComponent(customTitle)}`;
}
// Apply current settings
function applyCurrentSettings() {
// Apply title visibility
@@ -1460,6 +1613,8 @@
setInterval(updateDateTime, 1000);
updateRoundNavigation();
applyCurrentSettings();
reshapeGrid();
window.addEventListener('resize', reshapeGrid);
addTouchFeedback();
// Initialize language selector after i18n is ready