V1.0.0 - release and included language

This commit is contained in:
2025-10-10 16:59:23 +02:00
parent c61c1448e4
commit f8d8548244
4 changed files with 119 additions and 61 deletions
+106 -31
View File
@@ -152,21 +152,26 @@
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 20px;
padding: 20px;
gap: 15px;
padding: 15px;
box-sizing: border-box;
align-items: stretch;
justify-items: stretch;
}
.camera-card {
background: #2a2a2a;
background: transparent;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
position: relative;
width: 100%;
height: 100%;
}
.camera-card:hover {
@@ -189,22 +194,55 @@
.stream-wrapper {
position: relative;
width: 100%;
flex: 1;
background: #222;
background: #2a2a2a;
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%;
}
.image-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.stream-placeholder {
width: 640px;
height: 480px;
max-width: 100%;
max-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: cover;
object-fit: contain;
object-position: center;
display: block;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 10;
}
/* Camera Number Indicator */
.card-header {
background: linear-gradient(90deg, #007bff 0%, #007bff 60px, #1a1a1a 60px);
@@ -214,7 +252,8 @@
align-items: center;
gap: 0;
transition: all 0.2s ease;
position: relative;
border-radius: 12px 12px 0 0;
flex-shrink: 0;
}
.target-number {
@@ -711,12 +750,29 @@
<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="card-header">
<div class="target-number">{{ i }}</div>
<div class="card-title" id="cardTitle{{ i }}">{{ settings.camera_titles[i|string] }}</div>
</div>
<div class="stream-wrapper">
<img src="{{ streams[i-1].url }}" alt="Camera Stream {{ i }}" class="stream" onerror="this.style.opacity='0.1'">
<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="640" height="480" viewBox="0 0 640 480" 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 %}
@@ -1040,14 +1096,32 @@
const debouncedSaveSettings = debounce(saveSettings, 500);
// Mobile device detection
function isMobileDevice() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(window.innerWidth <= 1024 && window.innerHeight <= 1366) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0);
// 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;
@@ -1255,7 +1329,7 @@
// Add touch feedback for interactive elements on touch devices
function addTouchFeedback() {
if (isMobileDevice()) {
if ('ontouchstart' in window) {
const touchElements = document.querySelectorAll('.camera-card, .nav-btn, .hamburger-menu');
touchElements.forEach(element => {
@@ -1317,16 +1391,17 @@
<script src="/static/js/i18n.js"></script>
<script>
// Initialize translations with server data
if (typeof {{ translations|tojson }} !== 'undefined') {
currentTranslations = {{ translations|tojson }};
currentLanguage = '{{ current_language }}';
// Apply translations and create language selector when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
translatePage();
createLanguageSelector('languageSelectorContainer');
});
}
document.addEventListener('DOMContentLoaded', function() {
try {
// Only try to initialize translations if variables are available
if (typeof currentTranslations !== 'undefined' && typeof currentLanguage !== 'undefined') {
translatePage();
createLanguageSelector('languageSelectorContainer');
}
} catch (e) {
console.log('Translations not available:', e);
}
});
</script>
</body>
</html>