Files

1094 lines
42 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title data-i18n="camera.title">Nadzorna Plošča Kamer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/navbar.css">
<link rel="stylesheet" href="/static/css/buttons.css">
<link rel="stylesheet" href="/static/css/components.css">
<link rel="stylesheet" href="/static/css/responsive.css">
<link rel="stylesheet" href="/static/css/pages/index.css">
<script src="/static/js/i18n.js"></script>
</head>
<body>
<div class="navbar">
<img src="/static/logo.png" alt="Logo" class="logo" onerror="this.style.display='none'" />
<div class="navbar-center">
{% if settings.tournament_active %}
<div class="tournament-nav">
<button class="round-nav-btn" id="prevRoundBtn" onclick="changeRound(-1)" data-i18n-title="tournament.previous_round">
←
</button>
<div class="current-round-info">
<div class="round-title">🏆 <span data-i18n="tournament.round">Krog</span> <span id="currentRoundNum">{{ settings.current_round }}</span></div>
<div class="round-progress">{{ settings.current_round }} <span data-i18n="tournament.round_of">od</span> {{ settings.total_rounds }}</div>
</div>
<button class="round-nav-btn" id="nextRoundBtn" onclick="changeRound(1)" data-i18n-title="tournament.next_round">
→
</button>
</div>
{% else %}
<div class="datetime">
<div class="time" id="time"></div>
<div class="date" id="date"></div>
</div>
{% endif %}
</div>
<div style="display: flex; align-items: center; gap: 15px;">
{% if settings.tournament_active %}
<div class="datetime" style="text-align: right;">
<div class="time" id="time"></div>
<div class="date" id="date"></div>
</div>
{% endif %}
<button class="hamburger-menu" id="menuButton" data-i18n-title="general.settings">
<div class="hamburger-line"></div>
<div class="hamburger-line"></div>
<div class="hamburger-line"></div>
</button>
</div>
</div>
<div id="overlay"></div>
<div class="settings-panel" id="settingsPanel">
<div class="settings-header">
<h3 data-i18n="general.settings">Nastavitve</h3>
<button class="close-btn" id="closeBtn">&times;</button>
</div>
<div class="settings-content">
{% if settings.tournament_active %}
<div class="settings-group">
<h4>🏆 <span data-i18n="tournament.active_tournament">Aktiven Turnir</span></h4>
<div class="tournament-status-info">
<p><strong><span data-i18n="tournament.current_round">Trenutni Krog</span>:</strong> {{ settings.current_round }} <span data-i18n="tournament.round_of">od</span> {{ settings.total_rounds }}</p>
<p data-i18n="tournament.current_round_info">Kartice kamer prikazujejo igralce trenutnega kroga</p>
</div>
<div class="tournament-actions">
<a href="/tournament/draft" class="nav-link tournament-btn">📋 Razpored Turnirja</a>
<a href="/results/calculator" class="nav-link tournament-btn">🎯 <span data-i18n="scoring.results_calculator">Calculator</span></a>
<a href="/tournament" class="nav-link tournament-btn" id="manageTournamentLink">⚙️ <span data-i18n="tournament.manage_tournament">Upravljaj Turnir</span></a>
</div>
</div>
{% endif %}
<!-- Tournaments Section -->
<div class="settings-group">
<h4 data-i18n="league.tournaments">Turnirji</h4>
<a href="/tournament" class="nav-link" id="tournamentModeLink">🏆 <span data-i18n="tournament.tournament_mode">Način Turnirja</span></a>
<a href="/archive/player-analysis" class="nav-link">👤 <span data-i18n="players.player_analysis">Analiza Igralcev</span></a>
<a href="/archive" class="nav-link">📚 <span data-i18n="navigation.archive">Arhiv</span></a>
</div>
<!-- System Section -->
<div class="settings-group">
<h4 data-i18n="league.system">Sistem</h4>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;">
<span data-i18n="camera.language_settings">Jezikovne Nastavitve</span>:
</label>
<div id="languageSelectorContainer"></div>
</div>
</div>
{% if not settings.tournament_active %}
<!-- Camera Section -->
<div class="settings-group" id="displayOptionsGroup">
<h4 data-i18n="league.camera">Kamera</h4>
<!-- 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 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>
<!-- Camera rows -->
<div class="cam-list">
{% for i in range(1, 7) %}
{% 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] }}"
oninput="updateCardTitle({{ i }})"
class="camera-input">
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Version Information -->
<div class="version-info" style="margin-top: 30px; padding: 15px; text-align: center; border-top: 1px solid #e9ecef; color: #6c757d; font-size: 0.85rem;">
Version 1.0.2
</div>
</div>
</div>
<div class="main-wrapper" id="mainWrapper">
<div class="grid" id="cameraGrid">
{% for i in range(1, 7) %}
<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>
<div class="card-title" id="cardTitle{{ i }}">{{ settings.camera_titles[i|string] }}</div>
</div>
<div class="image-container">
<div class="stream-placeholder" id="placeholder{{ i }}">
<svg width="100%" height="100%" viewBox="0 0 640 480" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
<rect width="640" height="480" fill="#1a1a1a"/>
<rect x="50" y="50" width="540" height="380" fill="none" stroke="#333" stroke-width="2" stroke-dasharray="10,5"/>
<g transform="translate(320,240)">
<circle r="40" fill="none" stroke="#555" stroke-width="3"/>
<circle r="8" fill="#666"/>
<rect x="-25" y="-15" width="50" height="30" fill="none" stroke="#555" stroke-width="2"/>
<text x="0" y="80" text-anchor="middle" fill="#666" font-family="Arial" font-size="14">Camera {{ i }}</text>
<text x="0" y="100" text-anchor="middle" fill="#555" font-family="Arial" font-size="12">640 × 480</text>
</g>
</svg>
</div>
<img src="{{ streams[i-1].url }}" alt="Camera Stream {{ i }}" class="stream"
onerror="handleStreamError(this, {{ i }})"
onload="handleStreamLoad(this, {{ i }})">
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Leaderboard sidebar — injected by JS when calc display is enabled -->
<div class="lb-sidebar" id="lbSidebar" style="display:none;">
<div class="lb-header">
<span class="lb-title">🏆 Lestvica</span>
<span class="lb-count" id="lbCount">0</span>
</div>
<div class="lb-empty" id="lbEmpty"><span class="icon">🏅</span><span>Čakamo na rezultate…</span></div>
<div class="lb-list" id="lbList" style="display:none;"></div>
</div>
</div>
<script>
// Global settings and tournament state
let currentSettings = {{ settings|tojson }};
const tournamentActive = {{ 'true' if settings.tournament_active else 'false' }};
let currentRound = {{ settings.current_round if settings.tournament_active else 1 }};
const totalRounds = {{ settings.total_rounds if settings.tournament_active else 1 }};
// Remote control state
const remoteControlled = {{ 'true' if settings.remote_controlled else 'false' }};
let remotePollingInterval = null;
let lastRemoteUpdate = '{{ settings.dashboard_state.last_updated if settings.dashboard_state else "" }}';
// DOM Elements - declare early to avoid issues
let menuButton, settingsPanel, overlay, closeBtn;
// Initialize DOM elements
function initializeDOMElements() {
menuButton = document.getElementById('menuButton');
settingsPanel = document.getElementById('settingsPanel');
overlay = document.getElementById('overlay');
closeBtn = document.getElementById('closeBtn');
console.log('DOM Elements initialized:', {
menuButton: !!menuButton,
settingsPanel: !!settingsPanel,
overlay: !!overlay,
closeBtn: !!closeBtn
});
}
// Settings panel functionality
function openSettings() {
console.log('Opening settings panel');
if (settingsPanel && overlay) {
settingsPanel.classList.add('active');
overlay.classList.add('active');
} else {
console.error('Settings panel elements not found');
}
}
function closeSettings() {
console.log('Closing settings panel');
if (settingsPanel && overlay) {
settingsPanel.classList.remove('active');
overlay.classList.remove('active');
}
}
// Remote control indicator
function createRemoteIndicator() {
if (remoteControlled) {
const indicator = document.createElement('div');
indicator.id = 'remoteIndicator';
indicator.innerHTML = `
<div style="
position: fixed;
top: 20px;
left: 20px;
background: rgba(0, 123, 255, 0.9);
color: white;
padding: 8px 15px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
">
<div style="
width: 8px;
height: 8px;
background: #28a745;
border-radius: 50%;
animation: pulse 2s infinite;
"></div>
📱 Remote Controlled
</div>
`;
document.body.appendChild(indicator);
}
}
// Start remote control polling
function startRemotePolling() {
if (!remoteControlled) return;
if (remotePollingInterval) {
clearInterval(remotePollingInterval);
}
remotePollingInterval = setInterval(async () => {
try {
const response = await fetch('/api/remote/check_updates');
if (response.ok) {
const data = await response.json();
if (data.needs_refresh) {
console.log('🔄 Remote refresh requested');
showRemoteUpdateNotification('Dashboard refreshed remotely');
setTimeout(() => {
window.location.reload();
}, 1000);
}
// Check if view should change
const currentView = data.current_view;
const currentPath = window.location.pathname;
if (shouldRedirectToView(currentView, currentPath)) {
console.log(`🔄 Remote view change: ${currentView}`);
showRemoteUpdateNotification(`Switching to ${currentView} view`);
setTimeout(() => {
redirectToView(currentView, data.fullscreen_camera);
}, 1000);
}
// Update last update time
if (data.last_updated && data.last_updated !== lastRemoteUpdate) {
lastRemoteUpdate = data.last_updated;
}
}
} catch (error) {
console.error('Remote polling error:', error);
// Don't show error to user, just continue polling
}
}, 2000); // Poll every 2 seconds
}
// Check if current page should redirect based on remote command
function shouldRedirectToView(remoteView, currentPath) {
const viewPaths = {
'cameras': '/',
'draft': '/tournament/draft',
'results': '/results',
'calculator': '/results/calculator',
'fullscreen': '/fullscreen/'
};
// If we're on the home page and remote wants cameras, no change needed
if (remoteView === 'cameras' && currentPath === '/') {
return false;
}
// If we're on fullscreen and remote wants fullscreen, no change needed
if (remoteView === 'fullscreen' && currentPath.startsWith('/fullscreen/')) {
return false;
}
// Check if current path matches the desired view
const targetPath = viewPaths[remoteView];
if (targetPath && currentPath !== targetPath) {
return true;
}
return false;
}
// Redirect to the appropriate view
function redirectToView(view, fullscreenCamera = null) {
switch (view) {
case 'cameras':
window.location.href = '/';
break;
case 'draft':
window.location.href = '/tournament/draft';
break;
case 'results':
window.location.href = '/results';
break;
case 'calculator':
window.location.href = '/results/calculator';
break;
case 'fullscreen':
if (fullscreenCamera) {
window.location.href = `/fullscreen/${fullscreenCamera}`;
}
break;
default:
console.log('Unknown view:', view);
}
}
// Show remote update notification - simplified for performance
function showRemoteUpdateNotification(message) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(40, 167, 69, 0.9);
color: white;
padding: 12px 20px;
border-radius: 8px;
font-weight: bold;
z-index: 1001;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
`;
notification.textContent = `📱 ${message}`;
document.body.appendChild(notification);
// Remove after 3 seconds without heavy animations
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
function updateRoundNavigation() {
if (tournamentActive) {
const prevBtn = document.getElementById('prevRoundBtn');
const nextBtn = document.getElementById('nextRoundBtn');
const currentRoundNum = document.getElementById('currentRoundNum');
if (prevBtn) prevBtn.disabled = currentRound <= 1;
if (nextBtn) nextBtn.disabled = currentRound >= totalRounds;
if (currentRoundNum) currentRoundNum.textContent = currentRound;
}
}
// Change round function
async function changeRound(direction) {
if (!tournamentActive) return;
const newRound = currentRound + direction;
if (newRound < 1 || newRound > totalRounds) return;
// Disable buttons during request
const prevBtn = document.getElementById('prevRoundBtn');
const nextBtn = document.getElementById('nextRoundBtn');
if (prevBtn) prevBtn.disabled = true;
if (nextBtn) nextBtn.disabled = true;
try {
const response = await fetch(`/api/tournament/round/${newRound}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
currentRound = newRound;
updateRoundNavigation();
// Show notification for remote users
if (remoteControlled) {
showRemoteUpdateNotification(`Round changed to ${newRound}`);
}
// Reload page to get new round players
setTimeout(() => {
window.location.reload();
}, 300);
} else {
console.error('Failed to change round');
alert('Failed to change round. Please try again.');
updateRoundNavigation();
}
} catch (error) {
console.error('Error changing round:', error);
alert('Error changing round. Please try again.');
updateRoundNavigation();
}
}
// TV view toggle (cameras ↔ draft)
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Save settings to server
async function saveSettings(settingsUpdate) {
try {
const response = await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settingsUpdate)
});
if (response.ok) {
const result = await response.json();
if (!tournamentActive) {
currentSettings = result.settings;
}
}
} catch (error) {
console.error('Error saving settings:', error);
}
}
const debouncedSaveSettings = debounce(saveSettings, 500);
// Handle stream loading success
function handleStreamLoad(img, cameraId) {
console.log(`✅ Camera ${cameraId} loaded successfully`);
img.style.opacity = '1';
// Hide placeholder when stream loads
const placeholder = document.getElementById(`placeholder${cameraId}`);
if (placeholder) {
placeholder.style.display = 'none';
}
}
// Handle stream loading error
function handleStreamError(img, cameraId) {
console.log(`❌ Camera ${cameraId} failed to load - showing placeholder`);
img.style.display = 'none';
// Show placeholder when stream fails
const placeholder = document.getElementById(`placeholder${cameraId}`);
if (placeholder) {
placeholder.style.display = 'flex';
}
}
// Update card title
function updateCardTitle(index) {
if (tournamentActive) return;
const input = document.getElementById(`titleInput${index}`);
const cardTitle = document.getElementById(`cardTitle${index}`);
if (input && cardTitle) {
const newTitle = input.value || `Camera ${index}`;
cardTitle.textContent = newTitle;
if (event && event.target === input) {
currentSettings.camera_titles[index.toString()] = newTitle;
debouncedSaveSettings({
camera_titles: {
[index.toString()]: newTitle
}
});
}
}
}
// 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;
for (let i = 1; i <= 6; i++) {
const cardTitle = document.getElementById(`cardTitle${i}`);
if (cardTitle) {
cardTitle.style.display = show ? 'block' : 'none';
}
}
if (event && event.target === document.getElementById('toggleTitles')) {
currentSettings.display_options.show_titles = show;
debouncedSaveSettings({
display_options: {
show_titles: show
}
});
}
}
// Adjust title size
function adjustTitleSize(size) {
document.getElementById('titleSizeValue').textContent = size;
for (let i = 1; i <= 6; i++) {
const cardTitle = document.getElementById(`cardTitle${i}`);
if (cardTitle) {
cardTitle.style.fontSize = `${size}rem`;
}
}
if (event && event.target === document.getElementById('titleSize')) {
currentSettings.display_options.title_size = parseFloat(size);
debouncedSaveSettings({
display_options: {
title_size: parseFloat(size)
}
});
}
}
function adjustTargetNumberSize(size) {
document.getElementById('targetNumberSizeValue').textContent = size;
for (let i = 1; i <= 6; i++) {
const targetNumber = document.querySelector(`[data-camera-id="${i}"] .target-number`);
if (targetNumber) {
targetNumber.style.fontSize = `${size}rem`;
}
}
if (event && event.target === document.getElementById('targetNumberSize')) {
if (!currentSettings.display_options) {
currentSettings.display_options = {};
}
currentSettings.display_options.target_number_size = parseFloat(size);
debouncedSaveSettings({
display_options: {
target_number_size: parseFloat(size)
}
});
}
}
// Apply current settings
function applyCurrentSettings() {
// Apply title visibility
const showTitles = currentSettings.display_options.show_titles;
const toggleTitlesEl = document.getElementById('toggleTitles');
if (toggleTitlesEl) {
toggleTitlesEl.checked = showTitles;
}
for (let i = 1; i <= 6; i++) {
const cardTitle = document.getElementById(`cardTitle${i}`);
if (cardTitle) {
cardTitle.style.display = showTitles ? 'block' : 'none';
}
}
// Apply title size
const titleSize = currentSettings.display_options.title_size;
const titleSizeValueEl = document.getElementById('titleSizeValue');
const titleSizeEl = document.getElementById('titleSize');
if (titleSizeValueEl) {
titleSizeValueEl.textContent = titleSize;
}
if (titleSizeEl) {
titleSizeEl.value = titleSize;
}
for (let i = 1; i <= 6; i++) {
const cardTitle = document.getElementById(`cardTitle${i}`);
if (cardTitle) {
cardTitle.style.fontSize = `${titleSize}rem`;
}
}
// Apply target number size separately
const targetNumberSize = currentSettings.display_options.target_number_size || titleSize;
const targetNumberSizeValueEl = document.getElementById('targetNumberSizeValue');
const targetNumberSizeEl = document.getElementById('targetNumberSize');
if (targetNumberSizeValueEl) {
targetNumberSizeValueEl.textContent = targetNumberSize;
}
if (targetNumberSizeEl) {
targetNumberSizeEl.value = targetNumberSize;
}
for (let i = 1; i <= 6; i++) {
const targetNumber = document.querySelector(`[data-camera-id="${i}"] .target-number`);
if (targetNumber) {
targetNumber.style.fontSize = `${targetNumberSize}rem`;
}
}
// Apply card titles
for (let i = 1; i <= 6; i++) {
const cardTitle = document.getElementById(`cardTitle${i}`);
const cameraCard = document.querySelector(`[data-camera-id="${i}"]`);
if (cardTitle) {
const title = currentSettings.camera_titles[i.toString()] || `Camera ${i}`;
cardTitle.textContent = title;
// Check if no player is assigned (title is Empty or Prazno)
if (title === 'Empty' || title === 'Prazno') {
if (cameraCard) {
cameraCard.classList.add('no-player');
}
} else {
if (cameraCard) {
cameraCard.classList.remove('no-player');
}
}
}
if (!tournamentActive) {
const input = document.getElementById(`titleInput${i}`);
if (input) {
input.value = currentSettings.camera_titles[i.toString()] || `Camera ${i}`;
}
}
}
}
// Time and date updates
function updateDateTime() {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const year = now.getFullYear();
const timeEl = document.getElementById('time');
const dateEl = document.getElementById('date');
if (timeEl) timeEl.textContent = `${hours}:${minutes}`;
if (dateEl) dateEl.textContent = `${day}.${month}.${year}`;
}
// Setup event listeners for menu
function setupEventListeners() {
console.log('Setting up event listeners...');
// Menu button click
if (menuButton) {
console.log('Adding menu button listener');
menuButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Menu button clicked');
openSettings();
});
} else {
console.error('Menu button not found!');
}
// Close button click
if (closeBtn) {
console.log('Adding close button listener');
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Close button clicked');
closeSettings();
});
}
// Overlay click
if (overlay) {
console.log('Adding overlay listener');
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
console.log('Overlay clicked');
closeSettings();
}
});
}
}
// Keyboard shortcuts
function setupKeyboardShortcuts() {
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && settingsPanel && settingsPanel.classList.contains('active')) {
console.log('Escape pressed, closing settings');
closeSettings();
} else if (tournamentActive) {
if (event.key === 'ArrowLeft') {
event.preventDefault();
changeRound(-1);
} else if (event.key === 'ArrowRight') {
event.preventDefault();
changeRound(1);
}
}
// Remote control shortcuts
if (remoteControlled) {
if (event.key === 'r' || event.key === 'R') {
if (event.ctrlKey || event.metaKey) {
// Allow normal refresh
return;
}
event.preventDefault();
window.location.reload();
}
}
});
}
// Add touch feedback for interactive elements on touch devices
function addTouchFeedback() {
if ('ontouchstart' in window) {
const touchElements = document.querySelectorAll('.camera-card, .nav-btn, .hamburger-menu');
touchElements.forEach(element => {
element.addEventListener('touchstart', function(e) {
if (!this.disabled) {
this.style.transform = 'scale(0.95) translateY(0)';
this.style.transition = 'transform 0.1s ease';
}
});
element.addEventListener('touchend', function(e) {
if (!this.disabled) {
setTimeout(() => {
this.style.transform = '';
this.style.transition = 'transform 0.2s ease';
}, 100);
}
});
element.addEventListener('touchcancel', function(e) {
if (!this.disabled) {
this.style.transform = '';
this.style.transition = 'transform 0.2s ease';
}
});
});
}
}
// Initialize everything
document.addEventListener('DOMContentLoaded', function() {
// Initialize DOM elements first
initializeDOMElements();
// Setup event listeners
setupEventListeners();
// Setup keyboard shortcuts
setupKeyboardShortcuts();
// Initialize other features
updateDateTime();
setInterval(updateDateTime, 1000);
updateRoundNavigation();
applyCurrentSettings();
reshapeGrid();
window.addEventListener('resize', reshapeGrid);
addTouchFeedback();
// Apply leaderboard display mode if enabled
initLeaderboardDisplay();
// Initialize language selector after i18n is ready
window.addEventListener('i18nReady', function() {
createLanguageSelector('languageSelectorContainer');
}, { once: true });
});
// TV Display Auto-Update Polling
// (Separate from remote control polling)
let lastDashboardStateHash = null;
let dashboardPollingInterval = null;
async function pollDashboardState() {
// Only poll when page is visible
if (document.visibilityState !== 'visible') {
return;
}
try {
const response = await fetch('/api/dashboard/state');
if (!response.ok) return;
const data = await response.json();
// If switched to draft view and tournament is active, follow it
if (data.tv_view === 'draft' && data.tournament_active) {
window.location.href = '/tournament/draft?tv=1';
return;
}
// If switched to results view, follow it (use explicit tv_url first, then fallback)
if (data.tv_view === 'results') {
if (data.tv_url) {
window.location.href = data.tv_url.includes('?') ? data.tv_url + '&tv=1' : data.tv_url + '?tv=1';
} else if (data.archive_filename) {
window.location.href = `/archive/tournament/${data.archive_filename}?tv=1`;
} else if (data.league_archive_filename) {
window.location.href = `/archive/league/${data.league_archive_filename}?tv=1`;
} else {
window.location.href = '/results?tv=1';
}
return;
}
// Create a hash of the state to detect changes
const stateHash = JSON.stringify(data);
// If structural state changed, reload the page
if (lastDashboardStateHash !== null && stateHash !== lastDashboardStateHash) {
console.log('🔄 Tournament state changed, reloading TV display...');
window.location.reload();
}
lastDashboardStateHash = stateHash;
} catch (error) {
console.error('Dashboard polling error:', error);
// Silent failure - keep polling
}
}
// Start dashboard polling (every 3 seconds)
dashboardPollingInterval = setInterval(pollDashboardState, 3000);
// Initial poll
pollDashboardState();
// Pause polling when page is hidden
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
pollDashboardState();
}
});
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (remotePollingInterval) {
clearInterval(remotePollingInterval);
}
if (dashboardPollingInterval) {
clearInterval(dashboardPollingInterval);
}
});
// ── Calculator leaderboard display mode ────────────────────────────────
let lbPollingInterval = null;
let lbDisplayEnabled = false;
function initLeaderboardDisplay() {
fetch('/api/calculator/display')
.then(r => r.json())
.then(d => applyLeaderboardMode(d.enabled, d.camera || 1))
.catch(() => {});
}
function applyLeaderboardMode(enabled, camera) {
lbDisplayEnabled = enabled;
const sidebar = document.getElementById('lbSidebar');
const grid = document.getElementById('cameraGrid');
if (!enabled) {
sidebar.style.display = 'none';
// restore camera visibility to match settings
for (let i = 1; i <= 6; i++) {
const card = document.querySelector(`.camera-card[data-camera-id="${i}"]`);
if (!card) continue;
const isDisabled = currentSettings.disabled_cameras && currentSettings.disabled_cameras.includes(i);
card.style.display = isDisabled ? 'none' : '';
}
grid.style.gridTemplateColumns = '';
if (lbPollingInterval) { clearInterval(lbPollingInterval); lbPollingInterval = null; }
reshapeGrid();
return;
}
// Show only selected camera
for (let i = 1; i <= 6; i++) {
const card = document.querySelector(`.camera-card[data-camera-id="${i}"]`);
if (!card) continue;
card.style.display = (i === camera) ? '' : 'none';
}
grid.style.gridTemplateColumns = '1fr';
grid.style.gridTemplateRows = '1fr';
sidebar.style.display = '';
reshapeGrid();
// Poll leaderboard
pollLeaderboard();
if (!lbPollingInterval) {
lbPollingInterval = setInterval(pollLeaderboard, 5000);
}
}
function pollLeaderboard() {
fetch('/api/calculator/leaderboard')
.then(r => r.json())
.then(data => { if (Array.isArray(data)) renderLbSidebar(data); })
.catch(() => {});
}
function renderLbSidebar(data) {
const list = document.getElementById('lbList');
const empty = document.getElementById('lbEmpty');
const count = document.getElementById('lbCount');
count.textContent = data.length;
if (data.length === 0) {
empty.style.display = '';
list.style.display = 'none';
return;
}
empty.style.display = 'none';
list.style.display = '';
list.innerHTML = data.map((e, idx) => {
const rank = data.filter(o =>
o.score > e.score || (o.score === e.score && o.tens > e.tens)
).length + 1;
const rc = rank <= 3 ? `r${rank}` : '';
const med = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : rank;
return `<div class="lb-row ${rc}">
<div class="lb-badge">${med}</div>
<div class="lb-name" title="${e.name}">${e.name}</div>
<div class="lb-tens">${e.tens}×10</div>
<div class="lb-pts">${e.score}</div>
</div>`;
}).join('');
}
// Poll display setting every 5s so enabling/disabling from calculator
// takes effect on the main screen automatically
setInterval(() => {
fetch('/api/calculator/display')
.then(r => r.json())
.then(d => {
const enabled = !!d.enabled;
const camera = d.camera || 1;
if (enabled !== lbDisplayEnabled) {
applyLeaderboardMode(enabled, camera);
}
})
.catch(() => {});
}, 5000);
</script>
</body>
</html>