V1.0.0 - release and included language
This commit is contained in:
@@ -1202,25 +1202,8 @@ def index():
|
|||||||
# MOBILE ROUTES
|
# MOBILE ROUTES
|
||||||
@app.route('/mobile')
|
@app.route('/mobile')
|
||||||
def mobile_menu():
|
def mobile_menu():
|
||||||
"""Mobile main menu page"""
|
"""Mobile redirect to streams"""
|
||||||
tournament_state = load_tournament_state()
|
return redirect('/mobile/streams')
|
||||||
league_state = load_league_state()
|
|
||||||
results = load_results()
|
|
||||||
|
|
||||||
tournament_active = tournament_state is not None
|
|
||||||
league_active = league_state is not None and not league_state.get('league_finished', False)
|
|
||||||
results_available = results is not None and results.get('tournament_finished', False)
|
|
||||||
league_results_available = league_state is not None and league_state.get('league_finished', False)
|
|
||||||
|
|
||||||
return render_template('mobile_menu.html',
|
|
||||||
tournament_active=tournament_active,
|
|
||||||
league_active=league_active,
|
|
||||||
tournament_state=tournament_state,
|
|
||||||
league_state=league_state,
|
|
||||||
results_available=results_available,
|
|
||||||
league_results_available=league_results_available,
|
|
||||||
translations=get_translations(),
|
|
||||||
current_language=get_current_language())
|
|
||||||
|
|
||||||
@app.route('/mobile/streams')
|
@app.route('/mobile/streams')
|
||||||
def mobile_streams():
|
def mobile_streams():
|
||||||
|
|||||||
+10
-10
@@ -230,7 +230,8 @@
|
|||||||
color: white;
|
color: white;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-loaded .loading {
|
.stream-loaded .loading {
|
||||||
@@ -392,10 +393,10 @@
|
|||||||
<div class="stream-container" id="streamContainer">
|
<div class="stream-container" id="streamContainer">
|
||||||
<div class="loading" id="loadingIndicator">Loading stream...</div>
|
<div class="loading" id="loadingIndicator">Loading stream...</div>
|
||||||
<div class="stream-viewport" id="streamViewport">
|
<div class="stream-viewport" id="streamViewport">
|
||||||
<img
|
<img
|
||||||
src="{{ stream.url }}"
|
src="{{ stream.url }}"
|
||||||
alt="{{ title }}"
|
alt="{{ title }}"
|
||||||
class="fullscreen-stream"
|
class="fullscreen-stream"
|
||||||
id="fullscreenStream"
|
id="fullscreenStream"
|
||||||
onerror="handleStreamError()"
|
onerror="handleStreamError()"
|
||||||
onload="handleStreamLoad()"
|
onload="handleStreamLoad()"
|
||||||
@@ -437,11 +438,11 @@
|
|||||||
function handleStreamError() {
|
function handleStreamError() {
|
||||||
const streamImg = document.getElementById('fullscreenStream');
|
const streamImg = document.getElementById('fullscreenStream');
|
||||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||||
|
|
||||||
if (retryCount < maxRetries) {
|
if (retryCount < maxRetries) {
|
||||||
retryCount++;
|
retryCount++;
|
||||||
loadingIndicator.textContent = `Retrying... (${retryCount}/${maxRetries})`;
|
loadingIndicator.textContent = `Retrying... (${retryCount}/${maxRetries})`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
streamImg.src = streamImg.src + '?retry=' + Date.now();
|
streamImg.src = streamImg.src + '?retry=' + Date.now();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -763,11 +764,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
// Check if user came from mobile by looking at referrer or user agent
|
// Check if user came specifically from a mobile page by looking at referrer
|
||||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
||||||
const referrer = document.referrer;
|
const referrer = document.referrer;
|
||||||
|
|
||||||
if (isMobile || referrer.includes('/mobile/')) {
|
if (referrer.includes('/mobile/')) {
|
||||||
window.location.href = '/mobile/streams';
|
window.location.href = '/mobile/streams';
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
|
|||||||
+106
-31
@@ -152,21 +152,26 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
grid-template-rows: repeat(2, 1fr);
|
grid-template-rows: repeat(2, 1fr);
|
||||||
gap: 20px;
|
gap: 15px;
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-card {
|
.camera-card {
|
||||||
background: #2a2a2a;
|
background: transparent;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-card:hover {
|
.camera-card:hover {
|
||||||
@@ -189,22 +194,55 @@
|
|||||||
|
|
||||||
.stream-wrapper {
|
.stream-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
background: #2a2a2a;
|
||||||
flex: 1;
|
border-radius: 12px;
|
||||||
background: #222;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
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 {
|
.stream {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
|
object-position: center;
|
||||||
display: block;
|
display: block;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Camera Number Indicator */
|
/* Camera Number Indicator */
|
||||||
.card-header {
|
.card-header {
|
||||||
background: linear-gradient(90deg, #007bff 0%, #007bff 60px, #1a1a1a 60px);
|
background: linear-gradient(90deg, #007bff 0%, #007bff 60px, #1a1a1a 60px);
|
||||||
@@ -214,7 +252,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
position: relative;
|
border-radius: 12px 12px 0 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.target-number {
|
.target-number {
|
||||||
@@ -711,12 +750,29 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
{% for i in range(1, 7) %}
|
{% 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" 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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -1040,14 +1096,32 @@
|
|||||||
|
|
||||||
const debouncedSaveSettings = debounce(saveSettings, 500);
|
const debouncedSaveSettings = debounce(saveSettings, 500);
|
||||||
|
|
||||||
// Mobile device detection
|
// Handle stream loading success
|
||||||
function isMobileDevice() {
|
function handleStreamLoad(img, cameraId) {
|
||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
console.log(`✅ Camera ${cameraId} loaded successfully`);
|
||||||
(window.innerWidth <= 1024 && window.innerHeight <= 1366) ||
|
img.style.opacity = '1';
|
||||||
('ontouchstart' in window) ||
|
|
||||||
(navigator.maxTouchPoints > 0);
|
// 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
|
// Update card title
|
||||||
function updateCardTitle(index) {
|
function updateCardTitle(index) {
|
||||||
if (tournamentActive) return;
|
if (tournamentActive) return;
|
||||||
@@ -1255,7 +1329,7 @@
|
|||||||
|
|
||||||
// Add touch feedback for interactive elements on touch devices
|
// Add touch feedback for interactive elements on touch devices
|
||||||
function addTouchFeedback() {
|
function addTouchFeedback() {
|
||||||
if (isMobileDevice()) {
|
if ('ontouchstart' in window) {
|
||||||
const touchElements = document.querySelectorAll('.camera-card, .nav-btn, .hamburger-menu');
|
const touchElements = document.querySelectorAll('.camera-card, .nav-btn, .hamburger-menu');
|
||||||
|
|
||||||
touchElements.forEach(element => {
|
touchElements.forEach(element => {
|
||||||
@@ -1317,16 +1391,17 @@
|
|||||||
<script src="/static/js/i18n.js"></script>
|
<script src="/static/js/i18n.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Initialize translations with server data
|
// Initialize translations with server data
|
||||||
if (typeof {{ translations|tojson }} !== 'undefined') {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
currentTranslations = {{ translations|tojson }};
|
try {
|
||||||
currentLanguage = '{{ current_language }}';
|
// Only try to initialize translations if variables are available
|
||||||
|
if (typeof currentTranslations !== 'undefined' && typeof currentLanguage !== 'undefined') {
|
||||||
// Apply translations and create language selector when DOM is ready
|
translatePage();
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
createLanguageSelector('languageSelectorContainer');
|
||||||
translatePage();
|
}
|
||||||
createLanguageSelector('languageSelectorContainer');
|
} catch (e) {
|
||||||
});
|
console.log('Translations not available:', e);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{ player.name }} - Player Stats</title>
|
<title>{{ player.name }} - Player Stats</title>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
<script src="/static/js/chart.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user