Files
Sdk_TV_app/templates/fullscreen.html
T
bl3kunja c61c1448e4 Enhance print layouts with branded headers and fix navigation consistency
- Replace plain print headers with full branded headers including logo
  - Add dynamic tournament-type styling (🎯 4-target,  20-target, 💪 40-target)
  - Remove border lines and optimize spacing for clean print appearance
  - Fix emoji positioning in league championship headers
  - Standardize navigation with proper active button indicators
  - Add missing translation keys for calculator instructions
  - Update print media queries for professional document output

  Print improvements:
  - Logo and branding now appear on printed results
  - Consistent 20px spacing between header and table
  - Clean white background with subtle borders
  - Optimized typography for print readability

  Navigation fixes:
  - Added active button highlighting across all PC pages
  - Consistent navigation order: Dashboard → Tournament → Player Analysis → Archive → Draft →
  Calculator
  - Fixed draft page active indicator

  🤖 Generated with Claude Code

  Co-Authored-By: Claude <noreply@anthropic.com>

  This commit message covers all the major improvements we made:
  - Print layout enhancements with branded headers
  - Navigation standardization and active indicators
  - Translation fixes
  - Visual styling improvements
  - Professional document output optimization
2025-09-20 20:03:44 +02:00

778 lines
21 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>{{ title }} - Fullscreen</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* Reset and base styles */
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
background: black;
height: 100vh;
overflow: hidden;
font-family: Arial, sans-serif;
}
/* Navigation bar */
.fullscreen-navbar {
background: white;
color: black;
padding: 15px 25px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid #ccc;
height: 70px;
box-sizing: border-box;
position: relative;
z-index: 1000;
}
.navbar-title {
font-size: 1.8rem;
font-weight: bold;
color: #333;
text-align: left;
flex: 1;
margin-right: auto;
}
.navbar-controls {
display: flex;
gap: 12px;
align-items: center;
z-index: 10;
}
/* Control buttons */
.control-btn {
background: #f8f9fa !important;
border: 2px solid #e9ecef !important;
cursor: pointer;
padding: 12px;
border-radius: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #333 !important;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
font-size: 1.2rem;
font-weight: bold;
text-decoration: none;
}
.control-btn:hover {
background: #e9ecef !important;
border-color: #007bff !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
transform: translateY(-1px) !important;
color: #007bff !important;
}
.control-btn:active {
transform: translateY(0) !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
.control-btn:disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
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;
}
.control-btn.close-btn:hover {
border-color: #dc3545 !important;
color: #dc3545 !important;
}
.control-btn svg {
transition: all 0.2s ease;
}
/* Stream container */
.stream-container {
height: calc(100vh - 70px);
width: 100%;
background: #111;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.stream-viewport {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
}
.stream-viewport.dragging {
cursor: grabbing !important;
}
.stream-viewport.zoomed {
cursor: grab !important;
}
.fullscreen-stream {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
display: block;
transition: transform 0.2s ease;
transform-origin: center center;
}
.fullscreen-stream.instant-transition {
transition: none;
}
/* Zoom controls */
.zoom-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
background: rgba(248, 249, 250, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
z-index: 100;
}
.zoom-btn {
background: #f8f9fa !important;
border: 2px solid #e9ecef !important;
cursor: pointer;
padding: 10px;
border-radius: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #333 !important;
font-weight: bold;
font-size: 1rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.zoom-btn:hover {
background: #e9ecef !important;
border-color: #007bff !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
transform: translateY(-1px) !important;
color: #007bff !important;
}
.zoom-btn:active {
transform: translateY(0) !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
.zoom-btn:disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
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;
}
.zoom-level {
padding: 10px 12px;
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 0.9rem;
font-weight: bold;
color: #333;
min-width: 50px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Loading indicator */
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 1.1rem;
opacity: 0.7;
z-index: 1;
}
.stream-loaded .loading {
display: none;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.fullscreen-navbar {
padding: 10px 15px;
height: 60px;
}
.navbar-title {
font-size: 1.4rem;
}
.navbar-controls {
gap: 8px;
}
.control-btn {
padding: 10px;
width: 45px;
height: 45px;
font-size: 1.1rem;
}
.stream-container {
height: calc(100vh - 60px);
}
.zoom-controls {
bottom: 15px;
padding: 8px;
gap: 8px;
}
.zoom-btn {
padding: 8px;
width: 36px;
height: 36px;
font-size: 0.9rem;
}
.zoom-level {
padding: 8px 10px;
min-width: 45px;
font-size: 0.85rem;
}
}
/* Very small mobile devices */
@media (max-width: 480px) {
.fullscreen-navbar {
padding: 8px 12px;
height: 55px;
}
.navbar-title {
font-size: 1.2rem;
}
.navbar-controls {
gap: 6px;
}
.control-btn {
padding: 8px;
width: 40px;
height: 40px;
font-size: 1rem;
}
.stream-container {
height: calc(100vh - 55px);
}
.zoom-controls {
bottom: 12px;
gap: 6px;
padding: 6px;
}
.zoom-btn {
padding: 6px;
width: 32px;
height: 32px;
font-size: 0.85rem;
}
.zoom-level {
padding: 6px 8px;
min-width: 40px;
font-size: 0.8rem;
}
}
/* Landscape mobile optimization */
@media (max-height: 500px) and (orientation: landscape) {
.fullscreen-navbar {
padding: 8px 15px;
height: 50px;
}
.navbar-title {
font-size: 1.3rem;
}
.navbar-controls {
gap: 6px;
}
.control-btn {
padding: 6px;
width: 38px;
height: 38px;
font-size: 0.9rem;
}
.stream-container {
height: calc(100vh - 50px);
}
.zoom-controls {
bottom: 10px;
gap: 4px;
padding: 4px;
}
.zoom-btn {
padding: 4px;
width: 30px;
height: 30px;
font-size: 0.8rem;
}
.zoom-level {
padding: 4px 6px;
min-width: 35px;
font-size: 0.75rem;
}
}
</style>
</head>
<body>
<div class="fullscreen-navbar">
<div class="navbar-title">{{ title }}</div>
<div class="navbar-controls">
<button class="control-btn" id="fullscreenToggleBtn" title="Toggle Browser Fullscreen (F)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
</svg>
</button>
<a href="#" onclick="goBack()" class="control-btn close-btn" title="Close Fullscreen"></a>
</div>
</div>
<div class="stream-container" id="streamContainer">
<div class="loading" id="loadingIndicator">Loading stream...</div>
<div class="stream-viewport" id="streamViewport">
<img
src="{{ stream.url }}"
alt="{{ title }}"
class="fullscreen-stream"
id="fullscreenStream"
onerror="handleStreamError()"
onload="handleStreamLoad()"
>
</div>
<div class="zoom-controls" id="zoomControls">
<button class="zoom-btn" id="zoomOutBtnBottom" title="Zoom Out (-)"></button>
<div class="zoom-level" id="zoomLevel">1.0x</div>
<button class="zoom-btn" id="zoomInBtnBottom" title="Zoom In (+)">+</button>
<button class="zoom-btn" id="resetZoomBtn" title="Reset Zoom (R)"></button>
</div>
</div>
<script>
let retryCount = 0;
const maxRetries = 3;
// Zoom functionality
let currentZoom = 1;
const minZoom = 1;
const maxZoom = 5;
const zoomStep = 0.1;
// Pan functionality
let isPanning = false;
let panStartX = 0;
let panStartY = 0;
let panOffsetX = 0;
let panOffsetY = 0;
let lastTouchDistance = 0;
function handleStreamLoad() {
document.getElementById('streamContainer').classList.add('stream-loaded');
retryCount = 0;
resetZoom();
}
function handleStreamError() {
const streamImg = document.getElementById('fullscreenStream');
const loadingIndicator = document.getElementById('loadingIndicator');
if (retryCount < maxRetries) {
retryCount++;
loadingIndicator.textContent = `Retrying... (${retryCount}/${maxRetries})`;
setTimeout(() => {
streamImg.src = streamImg.src + '?retry=' + Date.now();
}, 2000);
} else {
loadingIndicator.textContent = 'Stream unavailable';
streamImg.style.display = 'none';
}
}
// Zoom functions
function updateZoom(smoothTransition = true) {
const streamImg = document.getElementById('fullscreenStream');
const zoomLevel = document.getElementById('zoomLevel');
const zoomInBtnBottom = document.getElementById('zoomInBtnBottom');
const zoomOutBtnBottom = document.getElementById('zoomOutBtnBottom');
const viewport = document.getElementById('streamViewport');
if (smoothTransition) {
streamImg.classList.remove('instant-transition');
} else {
streamImg.classList.add('instant-transition');
}
if (currentZoom <= 1) {
panOffsetX = 0;
panOffsetY = 0;
currentZoom = 1;
streamImg.style.transform = '';
} else {
streamImg.style.transform = `scale(${currentZoom}) translate(${panOffsetX}px, ${panOffsetY}px)`;
}
zoomLevel.textContent = `${currentZoom.toFixed(1)}x`;
const atMinZoom = currentZoom <= minZoom;
const atMaxZoom = currentZoom >= maxZoom;
if (zoomOutBtnBottom) zoomOutBtnBottom.disabled = atMinZoom;
if (zoomInBtnBottom) zoomInBtnBottom.disabled = atMaxZoom;
if (currentZoom > 1) {
viewport.classList.add('zoomed');
} else {
viewport.classList.remove('zoomed');
panOffsetX = 0;
panOffsetY = 0;
}
}
function zoomIn() {
if (currentZoom < maxZoom) {
currentZoom = Math.min(maxZoom, currentZoom + zoomStep);
updateZoom(false);
}
}
function zoomOut() {
if (currentZoom > minZoom) {
const newZoom = Math.max(minZoom, currentZoom - zoomStep);
if (newZoom <= minZoom) {
currentZoom = minZoom;
panOffsetX = 0;
panOffsetY = 0;
const streamImg = document.getElementById('fullscreenStream');
streamImg.style.transform = '';
updateZoom(true);
} else {
currentZoom = newZoom;
const zoomRatio = currentZoom / (currentZoom + zoomStep);
panOffsetX *= zoomRatio;
panOffsetY *= zoomRatio;
updateZoom(false);
}
}
}
function resetZoom() {
currentZoom = 1;
panOffsetX = 0;
panOffsetY = 0;
const streamImg = document.getElementById('fullscreenStream');
streamImg.style.transform = '';
updateZoom(true);
}
// Pan functions
function startPan(clientX, clientY) {
if (currentZoom > 1) {
isPanning = true;
panStartX = clientX - panOffsetX;
panStartY = clientY - panOffsetY;
document.getElementById('streamViewport').classList.add('dragging');
}
}
function updatePan(clientX, clientY) {
if (isPanning && currentZoom > 1) {
const viewport = document.getElementById('streamViewport');
const streamImg = document.getElementById('fullscreenStream');
const newOffsetX = clientX - panStartX;
const newOffsetY = clientY - panStartY;
const imgRect = streamImg.getBoundingClientRect();
const viewportRect = viewport.getBoundingClientRect();
const scaledWidth = imgRect.width / currentZoom;
const scaledHeight = imgRect.height / currentZoom;
const maxOffsetX = Math.max(0, (scaledWidth * currentZoom - viewportRect.width) / (2 * currentZoom));
const maxOffsetY = Math.max(0, (scaledHeight * currentZoom - viewportRect.height) / (2 * currentZoom));
panOffsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, newOffsetX));
panOffsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, newOffsetY));
updateZoom(false);
}
}
function endPan() {
isPanning = false;
document.getElementById('streamViewport').classList.remove('dragging');
if (currentZoom <= 1) {
panOffsetX = 0;
panOffsetY = 0;
updateZoom();
}
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
const zoomInBtnBottom = document.getElementById('zoomInBtnBottom');
const zoomOutBtnBottom = document.getElementById('zoomOutBtnBottom');
const resetZoomBtn = document.getElementById('resetZoomBtn');
const fullscreenToggleBtn = document.getElementById('fullscreenToggleBtn');
const viewport = document.getElementById('streamViewport');
if (zoomInBtnBottom) zoomInBtnBottom.addEventListener('click', zoomIn);
if (zoomOutBtnBottom) zoomOutBtnBottom.addEventListener('click', zoomOut);
if (resetZoomBtn) resetZoomBtn.addEventListener('click', resetZoom);
if (fullscreenToggleBtn) {
fullscreenToggleBtn.addEventListener('click', toggleBrowserFullscreen);
}
// Mouse events for panning
viewport.addEventListener('mousedown', function(e) {
e.preventDefault();
startPan(e.clientX, e.clientY);
});
document.addEventListener('mousemove', function(e) {
if (isPanning) {
e.preventDefault();
updatePan(e.clientX, e.clientY);
}
});
document.addEventListener('mouseup', endPan);
// Mouse wheel zoom
viewport.addEventListener('wheel', function(e) {
e.preventDefault();
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
});
// Touch events for mobile
viewport.addEventListener('touchstart', function(e) {
e.preventDefault();
if (e.touches.length === 1) {
startPan(e.touches[0].clientX, e.touches[0].clientY);
} else if (e.touches.length === 2) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
lastTouchDistance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
}
});
viewport.addEventListener('touchmove', function(e) {
e.preventDefault();
if (e.touches.length === 1 && isPanning) {
updatePan(e.touches[0].clientX, e.touches[0].clientY);
} else if (e.touches.length === 2) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
if (lastTouchDistance > 0) {
const distanceRatio = currentDistance / lastTouchDistance;
if (distanceRatio > 1.02) {
zoomIn();
} else if (distanceRatio < 0.98) {
zoomOut();
}
}
lastTouchDistance = currentDistance;
}
});
viewport.addEventListener('touchend', function(e) {
endPan();
if (e.touches.length < 2) {
lastTouchDistance = 0;
}
});
updateZoom(false);
});
// Handle keyboard shortcuts
document.addEventListener('keydown', function(event) {
switch(event.key) {
case 'Escape':
goBack();
break;
case 'f':
case 'F':
toggleBrowserFullscreen();
break;
case 'r':
case 'R':
event.preventDefault();
resetZoom();
break;
case '=':
case '+':
event.preventDefault();
zoomIn();
break;
case '-':
event.preventDefault();
zoomOut();
break;
}
});
function toggleBrowserFullscreen() {
if (!document.fullscreenElement && !document.webkitFullscreenElement &&
!document.mozFullScreenElement && !document.msFullscreenElement) {
const docEl = document.documentElement;
if (docEl.requestFullscreen) {
docEl.requestFullscreen().catch(err => console.log('Fullscreen error:', err));
} else if (docEl.webkitRequestFullscreen) {
docEl.webkitRequestFullscreen();
} else if (docEl.mozRequestFullScreen) {
docEl.mozRequestFullScreen();
} else if (docEl.msRequestFullscreen) {
docEl.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen().catch(err => console.log('Exit fullscreen error:', err));
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
}
function handleFullscreenChange() {
const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement ||
document.mozFullScreenElement || document.msFullscreenElement;
const fullscreenBtn = document.getElementById('fullscreenToggleBtn');
if (fullscreenBtn) {
if (isFullscreen) {
fullscreenBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
</svg>
`;
fullscreenBtn.title = "Exit Browser Fullscreen (F)";
} else {
fullscreenBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
</svg>
`;
fullscreenBtn.title = "Toggle Browser Fullscreen (F)";
}
}
}
// Listen for fullscreen changes
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
// Auto-refresh stream every 30 seconds
setInterval(() => {
const streamImg = document.getElementById('fullscreenStream');
if (streamImg && !streamImg.complete) {
streamImg.src = streamImg.src.split('?')[0] + '?refresh=' + Date.now();
}
}, 30000);
// Prevent context menu and dragging on stream
document.getElementById('fullscreenStream').addEventListener('contextmenu', function(e) {
e.preventDefault();
});
document.getElementById('fullscreenStream').addEventListener('dragstart', function(e) {
e.preventDefault();
});
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);
const referrer = document.referrer;
if (isMobile || referrer.includes('/mobile/')) {
window.location.href = '/mobile/streams';
} else {
window.location.href = '/';
}
}
</script>
</body>
</html>