Files
Sdk_TV_app/templates/index.html
T
2025-11-12 17:49:56 +01:00

1477 lines
46 KiB
HTML
Raw 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">
<style>
/* Dashboard-specific styles override and extensions */
html, body {
margin: 0;
padding: 0;
background: black;
height: 100vh;
overflow: hidden;
font-family: Arial, sans-serif;
}
/* Tournament Navigation */
.tournament-nav {
display: flex;
align-items: center;
gap: 8px;
}
.round-nav-btn {
background: #f8f9fa;
border: 2px solid #e9ecef;
cursor: pointer;
padding: 12px 20px;
border-radius: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #333;
font-weight: bold;
font-size: 0.9rem;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
height: 44px;
margin: 0;
flex-shrink: 0;
}
.round-nav-btn:hover {
background: #e9ecef;
border-color: #28a745;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
color: #28a745;
}
.round-nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
background: #f8f9fa !important;
border-color: #e9ecef !important;
color: #333 !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
.current-round-info {
text-align: center;
min-width: 100px;
margin: 0;
padding: 0;
}
.round-title {
font-size: 1.2rem;
font-weight: bold;
color: #333;
margin: 0;
padding: 0;
line-height: 1.2;
}
.round-progress {
font-size: 0.9rem;
color: #666;
padding: 2px 8px;
background: rgba(0, 123, 255, 0.1);
border-radius: 6px;
border: 1px solid rgba(0, 123, 255, 0.2);
margin: 2px 0 0 0;
}
/* Regular time/date display */
.datetime {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0;
margin: 0;
line-height: 1.2;
}
.datetime .time {
font-size: 1.6rem;
font-weight: bold;
color: #333;
padding: 0;
margin: 0;
line-height: 1.2;
}
.datetime .date {
font-size: 0.9rem;
color: #666;
padding: 0;
margin: 0;
line-height: 1.2;
}
.grid {
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;
}
.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;
color: white;
text-align: left;
pointer-events: none;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stream-wrapper {
position: relative;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
overflow: hidden;
pointer-events: none;
display: flex;
flex-direction: column;
max-width: 100%;
max-height: 100%;
width: fit-content;
height: fit-content;
gap: 0;
margin: 0;
padding: 0;
}
.image-container {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 0 0 12px 12px;
background: #1a1a1a;
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-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
z-index: 5;
}
.stream-placeholder svg {
width: 100%;
height: 100%;
display: block;
}
.stream {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: fill;
object-position: center;
display: block;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 10;
opacity: 1;
}
/* Camera Number Indicator */
.card-header {
background: linear-gradient(90deg, #28a745 0%, #28a745 60px, #1a1a1a 60px);
border-bottom: 2px solid #333;
padding: 12px 15px 12px 0;
display: flex;
align-items: center;
gap: 0;
transition: all 0.2s ease;
border-radius: 12px 12px 0 0;
flex-shrink: 0;
margin: 0;
line-height: 1;
}
.target-number {
background: transparent;
color: white;
width: 60px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: bold;
flex-shrink: 0;
margin-right: 15px;
transition: color 0.2s ease;
}
/* Style when no player is assigned */
.camera-card.no-player .card-header {
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 {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
#overlay.active {
opacity: 1;
visibility: visible;
}
.settings-panel {
position: fixed;
top: 0;
right: -500px;
width: 500px;
height: 100vh;
background: white;
z-index: 1000;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
transition: right 0.3s ease;
overflow-y: auto;
}
.settings-panel.active {
right: 0;
}
.settings-header {
background: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.settings-header h3 {
margin: 0;
font-size: 1.3rem;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 4px 8px;
border-radius: 4px;
}
.close-btn:hover {
background: #e9ecef;
color: #333;
}
.settings-content {
padding: 25px 30px;
}
.settings-group {
margin-bottom: 25px;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
border: 1px solid #e9ecef;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.settings-group h4 {
margin: -5px 0 20px 0;
font-size: 1.2rem;
color: #2c3e50;
border-bottom: 3px solid #28a745;
padding-bottom: 8px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.settings-group h5 {
margin: 20px 0 15px 0;
font-size: 1.05rem;
color: #495057;
border-bottom: 1px solid #dee2e6;
padding-bottom: 5px;
font-weight: 500;
}
/* System Section Styling */
.settings-group:has(h4[data-i18n="league.system"]) {
background: linear-gradient(135deg, #e8f4f8 0%, #f0f8ff 100%);
border-left: 4px solid #17a2b8;
}
.settings-group:has(h4[data-i18n="league.system"]) h4 {
color: #17a2b8;
border-bottom-color: #17a2b8;
}
/* Camera Section Styling */
.settings-group:has(h4[data-i18n="league.camera"]) {
background: linear-gradient(135deg, #e8f5e8 0%, #f0fff0 100%);
border-left: 4px solid #28a745;
}
.settings-group:has(h4[data-i18n="league.camera"]) h4 {
color: #28a745;
border-bottom-color: #28a745;
}
/* Tournaments Section Styling */
.settings-group:has(h4[data-i18n="league.tournaments"]) {
background: linear-gradient(135deg, #fff8e1 0%, #fffef7 100%);
border-left: 4px solid #ffc107;
}
.settings-group:has(h4[data-i18n="league.tournaments"]) h4 {
color: #e67e22;
border-bottom-color: #ffc107;
}
.nav-link {
display: block;
background: white;
border: 2px solid #dee2e6;
color: #495057;
padding: 14px 18px;
border-radius: 10px;
text-decoration: none;
font-weight: 500;
font-size: 0.95rem;
transition: all 0.3s ease;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 12px;
}
.nav-link:hover {
background: #f8f9fa;
border-color: #28a745;
color: #28a745;
transform: translateY(-2px);
box-shadow: 0 5px 12px rgba(0, 123, 255, 0.15);
text-decoration: none !important;
}
/* Yellow hollow style for first 3 tournament nav links */
.settings-group:has(h4[data-i18n="league.tournaments"]) .nav-link {
border: 2px solid #dee2e6;
background: white;
color: #333;
font-weight: 500;
}
.settings-group:has(h4[data-i18n="league.tournaments"]) .nav-link:hover {
background: #fffbf0;
border-color: #ffc107;
color: #ff9800;
box-shadow: 0 5px 12px rgba(255, 152, 0, 0.2);
}
.camera-input {
width: 100%;
padding: 14px 18px;
border: 2px solid #dee2e6;
border-radius: 10px;
background: white;
color: #495057;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
.camera-input:focus {
border-color: #28a745;
box-shadow: 0 5px 12px rgba(0, 123, 255, 0.15);
transform: translateY(-1px);
outline: none;
}
.camera-input:hover:not(:focus) {
border-color: #28a745;
box-shadow: 0 5px 12px rgba(0, 123, 255, 0.15);
transform: translateY(-1px);
}
.tournament-status-info {
background: #f0f8ff;
border: 2px solid #28a745;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
.tournament-status-info p {
margin: 5px 0;
color: #333;
}
.tournament-btn {
background: #28a745 !important;
border-color: #1e7e34 !important;
color: white !important;
text-align: center;
font-size: 0.95rem !important;
padding: 14px 18px !important;
font-weight: 500 !important;
}
.tournament-btn:hover {
background: #1e7e34 !important;
border-color: #004085 !important;
color: white !important;
text-decoration: none !important;
}
/* Desktop-only styles - mobile users are redirected */
/* Mobile responsive fallback for desktop users on small screens */
@media (max-width: 768px) {
.navbar {
padding: 10px 15px;
flex-direction: column;
gap: 15px;
}
.navbar-controls {
flex-wrap: wrap;
justify-content: center;
}
.grid {
height: calc(100vh - 100px);
grid-template-columns: 1fr;
grid-template-rows: repeat(6, 1fr);
gap: 15px;
padding: 20px;
}
.card-header {
background: linear-gradient(90deg, #28a745 0%, #28a745 50px, #1a1a1a 50px);
padding: 10px 12px 10px 0;
}
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 50px, #2d2d2d 50px);
}
.target-number {
width: 50px;
font-size: 1.1rem;
margin-right: 12px;
}
.card-title {
font-size: 1.2rem;
}
.settings-panel {
width: 100%;
right: -100%;
}
.tournament-nav {
gap: 10px;
}
.round-nav-btn {
width: 45px;
height: 45px;
padding: 10px;
font-size: 1.1rem;
}
.current-round-info {
min-width: 100px;
}
.round-title {
font-size: 1.2rem;
}
.round-progress {
font-size: 0.75rem;
}
.camera-number {
width: 35px;
height: 35px;
font-size: 1rem;
top: 10px;
right: 10px;
}
}
@media (max-width: 480px) {
.grid {
padding: 15px;
gap: 12px;
}
.card-header {
background: linear-gradient(90deg, #28a745 0%, #28a745 45px, #1a1a1a 45px);
padding: 8px 10px 8px 0;
}
.camera-card:hover .card-header {
background: linear-gradient(90deg, #1e7e34 0%, #1e7e34 45px, #2d2d2d 45px);
}
.target-number {
width: 45px;
font-size: 1rem;
margin-right: 10px;
}
.card-title {
font-size: 1rem;
}
}
</style>
<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;">
<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">📋 <span data-i18n="tournament.view_full_tournament_draft">Oglej si Celoten Žreb Turnirja</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 %}
{% if settings.tournament_active %}
<div class="settings-group">
<a href="/results/calculator" class="nav-link tournament-btn">🎯 <span data-i18n="scoring.results_calculator">Calculator</span></a>
</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>
<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>
</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>
<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>
{% 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>
<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.0
</div>
</div>
</div>
<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="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>
<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>
<style>
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
</style>
`;
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();
}
}
// 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 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)
}
});
}
}
// 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
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();
addTouchFeedback();
// Initialize language selector after i18n is ready
window.addEventListener('i18nReady', function() {
createLanguageSelector('languageSelectorContainer');
}, { once: true });
});
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (remotePollingInterval) {
clearInterval(remotePollingInterval);
}
});
</script>
</body>
</html>