Add files via upload

This commit is contained in:
Bl3kiiie
2025-07-30 17:53:24 +02:00
committed by GitHub
commit 75ac46c23c
22 changed files with 16175 additions and 0 deletions
+766
View File
@@ -0,0 +1,766 @@
<!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="/" 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':
window.location.href = '/';
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();
});
</script>
</body>
</html>