V1.0.0 - release and included language
This commit is contained in:
@@ -1202,25 +1202,8 @@ def index():
|
||||
# MOBILE ROUTES
|
||||
@app.route('/mobile')
|
||||
def mobile_menu():
|
||||
"""Mobile main menu page"""
|
||||
tournament_state = load_tournament_state()
|
||||
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())
|
||||
"""Mobile redirect to streams"""
|
||||
return redirect('/mobile/streams')
|
||||
|
||||
@app.route('/mobile/streams')
|
||||
def mobile_streams():
|
||||
|
||||
@@ -230,7 +230,8 @@
|
||||
color: white;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.7;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stream-loaded .loading {
|
||||
@@ -763,11 +764,10 @@
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
// Check if user came from mobile by looking at referrer or user agent
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
// Check if user came specifically from a mobile page by looking at referrer
|
||||
const referrer = document.referrer;
|
||||
|
||||
if (isMobile || referrer.includes('/mobile/')) {
|
||||
if (referrer.includes('/mobile/')) {
|
||||
window.location.href = '/mobile/streams';
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
|
||||
+106
-31
@@ -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>
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
* {
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user