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

783 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">
<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>
/* Fullscreen-specific 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: #28a745 !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
transform: translateY(-1px) !important;
color: #28a745 !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: #28a745 !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
transform: translateY(-1px) !important;
color: #28a745 !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: 2;
pointer-events: none;
}
.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 specifically from a mobile page by looking at referrer
const referrer = document.referrer;
if (referrer.includes('/mobile/')) {
window.location.href = '/mobile/streams';
} else {
window.location.href = '/';
}
}
</script>
</body>
</html>