vivaplusdl/static/index.html
2025-06-17 18:53:05 +02:00

599 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Viva++</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<style>
body {
background-color: #000;
background-image: radial-gradient(circle at 50% 200%, rgba(255, 0, 0, 0.4) 0%, transparent 70%),
radial-gradient(circle at 50% -100%, rgba(0, 0, 255, 0.4) 0%, transparent 70%);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden; /* To prevent any unexpected scrollbars */
}
.loader {
position: fixed; /* Changed for global overlay */
left: 50%;
top: 50%;
transform: translate(-50%, -50%); /* Center on screen */
z-index: 10001; /* Above video overlay */
border: 1.5vw solid #333;
border-top: 1.5vw solid #3498db;
border-bottom: 1.5vw solid #3498db;
border-radius: 50%;
width: 10vw;
height: 10vw;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#video-container {
display: none;
width: 100%;
justify-content: center;
align-items: center;
gap: 2vw;
}
.video-thumbnail {
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
background-color: #444;
aspect-ratio: 16/9;
width: 20vw;
transition: width 0.3s ease, box-shadow 0.3s ease, left 0.3s ease;
transform: translate(-50%, -50%);
position: fixed;
}
.video-thumbnail.focussed {
outline: 3px solid #557; /* Existing focus outline */
box-shadow: 0 0 400px -10px #446; /* Existing focus shadow */
width: 50vw;
z-index: 3; /* Ensure focused thumbnail is on top */
}
.side-video {
width: 20vw;
opacity: 0.7;
z-index: 1;
}
.play-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px; /* Adjust size as needed */
height: 80px; /* Adjust size as needed */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E");
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
background-repeat: no-repeat;
background-position: center;
background-size: 50%; /* Adjust icon size within button */
border-radius: 50%; /* Circular button */
cursor: pointer;
z-index: 3; /* Ensure it's above the video thumbnails */
border: 2px solid white;
box-shadow: 0 0 15px rgba(0,0,0,0.7);
}
#video-overlay-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
z-index: 10000; /* Above other page content */
display: none; /* Initially hidden, JS will change to flex */
align-items: center;
justify-content: center;
}
#video-overlay-container .video-player {
width: 100%;
height: 100%;
display: block;
position: fixed;
}
#video-overlay-container #back-button { /* Back button is now inside the container */
position: absolute;
top: 20px;
left: 20px;
z-index: 1; /* z-index relative to siblings in container */
padding: 10px 20px;
font-size: 16px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: 1px solid white;
border-radius: 5px;
cursor: pointer;
opacity: 1; /* Visible by default */
transition: opacity 0.3s ease-in-out; /* Smooth transition */
}
#video-overlay-container #back-button.button-hidden {
opacity: 0; /* Class to hide the button */
}
#custom-video-controls {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
z-index: 1; /* Above video, but below back button if it were in the same direct stacking context */
opacity: 1;
transition: opacity 0.3s ease-in-out;
box-sizing: border-box;
}
#custom-video-controls.controls-hidden {
opacity: 0;
}
#custom-play-pause-button {
background: none;
border: none;
color: white;
font-size: 1.5em; /* Adjust size as needed */
cursor: pointer;
padding: 5px 10px;
margin-right: 10px;
}
#custom-seek-bar {
flex-grow: 1;
cursor: pointer;
accent-color: #3498db; /* Optional: color for the seek bar thumb and progress */
}
/* Styles for TV remote focus indication */
#back-button:focus {
outline: 3px solid #3498db !important; /* A clear blue outline */
box-shadow: 0 0 15px #3498db; /* A glow effect */
}
.nofocus:focus {
outline: none !important; /* Remove focus outline for elements with nofocus class */
}
</style>
<script>
function hideSpinner() { // Was hideLoader, now only hides spinner
$(".loader").hide();
}
function showSpinner() { // Was showLoader
$(".loader").show();
}
function showVideoContainer() {
$("#video-container").css("display", "flex");
}
let currentVideo = null;
let videoElement = null;
let overlayContainer = null;
let inactivityTimer = null;
let backButton = null;
let customControls = null;
let playPauseButton = null;
let seekBar = null;
function exitVideoPlayback() {
if (videoElement && videoElement.pause) {
videoElement.pause();
}
clearTimeout(inactivityTimer);
if (overlayContainer) {
overlayContainer.css('cursor', 'default');
overlayContainer.off('mousemove.inactivityControls'); // Remove session-specific listener
overlayContainer.hide(); // Hide instead of remove
}
$(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
if (videoElement) {
$(videoElement).off('.currentVideo'); // Remove all namespaced video events
videoElement.src = ""; // Clear the source
videoElement.load(); // Important to ensure the video unloads
}
if (seekBar) { // seekBar is a jQuery object
seekBar.val(0);
seekBar.css('background', ''); // Reset seek bar style
}
updatePlayPauseButtonState(); // Update button to show play icon usually
// Attempt to exit fullscreen
if (document.fullscreenElement || document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement) {
if (document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
// FOCUS: Return focus to the main view's play button
if ($("#video-container").is(":visible") && $('#play-button').length) {
$('#play-button').focus();
}
}
function showControlsAndResetTimer() {
if (backButton) backButton.removeClass('button-hidden');
if (customControls) customControls.removeClass('controls-hidden');
if (overlayContainer) overlayContainer.css('cursor', 'default');
clearTimeout(inactivityTimer);
inactivityTimer = setTimeout(function() {
if (backButton) backButton.addClass('button-hidden');
if (customControls) customControls.addClass('controls-hidden');
if (overlayContainer) overlayContainer.css('cursor', 'none');
}, 3000);
}
function handlePlayPauseClick() {
if (videoElement) {
if (videoElement.paused) {
videoElement.play();
} else {
videoElement.pause();
}
}
updatePlayPauseButtonState();
}
function updatePlayPauseButtonState() {
if (!playPauseButton || !videoElement) return;
if (videoElement.paused) {
playPauseButton.text('►');
} else {
playPauseButton.text('❚❚');
}
}
function handleVideoLoadedMetadata() {
if (seekBar && videoElement) {
seekBar.attr('max', videoElement.duration);
}
}
function handleVideoTimeUpdate() {
if (seekBar && videoElement && videoElement.duration) { // Check videoElement.duration to prevent NaN
seekBar.val(videoElement.currentTime);
const percentage = (videoElement.currentTime / videoElement.duration) * 100;
seekBar.css('background', `linear-gradient(to right, #3498db ${percentage}%, #555 ${percentage}%)`);
} else if (seekBar) {
seekBar.val(0); // Reset if duration is not available
}
}
function handleSeekBarInput() {
if (videoElement && seekBar) {
videoElement.currentTime = seekBar.val();
}
}
function handleFullscreenChange() {
const isActuallyFullscreen = document.fullscreenElement || document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement;
if (!isActuallyFullscreen && overlayContainer && overlayContainer.is(':visible')) {
// If fullscreen exited and overlay was visible, treat as exiting video playback
// This helps if user presses Esc.
exitVideoPlayback();
}
}
let videoInfo = [];
let selectedThumbnailIndex = 0;
/**
* Creates a new video thumbnail element.
*
* @param data The video data returned by the server.
* @param selected Set to true if this is the currently selected video thumbnail.
* @param append Set to true if this thumbnail should be appended to the video container, false if it should be prepended.
*/
function createVideoThumbnailElement(data, selected, append) {
let container = $(`<span></span>`);
let thumbnail = $(`<img src="${data.thumbnail}" class="video-thumbnail" alt="Video Thumbnail" tabindex="0">`)[0];
container.append(thumbnail)
let index;
if (append) {
$("#video-container").append(container);
index = videoInfo.length;
videoInfo.push({
thumbnail,
data
})
} else {
$("#video-container").prepend(container);
index = 0;
videoInfo.unshift({
thumbnail,
data
});
selectedThumbnailIndex++;
}
if (selected) {
thumbnail.classList.add("focussed");
selectedThumbnailIndex = index;
}
}
/**
* Selects a video thumbnail by its index into the videoThumbnails array.
* @param index The index of the thumbnail to select.
*/
function selectThumbnail(index) {
if (index < 0 || index >= videoInfo.length) return; // Out of bounds check
if (selectedThumbnailIndex !== -1 && videoInfo[selectedThumbnailIndex]) {
videoInfo[selectedThumbnailIndex].thumbnail.classList.remove("focussed");
}
selectedThumbnailIndex = index;
videoInfo[selectedThumbnailIndex].thumbnail.classList.add("focussed");
const offset = 12;
for (let i = 0; i < videoInfo.length; i++) {
if (i < selectedThumbnailIndex - 1) {
videoInfo[i].thumbnail.style.left = "-20%";
} else if (i === selectedThumbnailIndex - 1) {
videoInfo[i].thumbnail.style.left = offset + "%";
} else if (i === selectedThumbnailIndex) {
videoInfo[i].thumbnail.style.left = "50%";
} else if (i === selectedThumbnailIndex + 1) {
videoInfo[i].thumbnail.style.left = (100 - offset) + "%";
} else {
videoInfo[i].thumbnail.style.left = "120%";
}
}
}
/**
* Starts the currently selected video.
*/
function startVideo() {
showSpinner();
let currentVideo = videoInfo[selectedThumbnailIndex].data
// Clear previous handlers and set new ones for this specific load attempt
videoElement.oncanplaythrough = null;
videoElement.onerror = null;
videoElement.oncanplaythrough = function() {
hideSpinner();
videoElement.oncanplaythrough = null; // Clean up handler
videoElement.onerror = null; // Clean up handler
videoElement.play().catch(err => {
console.error("Error attempting to play video:", err);
exitVideoPlayback(); // Exit overlay on play error
});
};
videoElement.onerror = function() {
console.error("Error loading video.");
hideSpinner();
videoElement.oncanplaythrough = null; // Clean up handler
videoElement.onerror = null; // Clean up handler
exitVideoPlayback(); // Exit overlay on load error
};
videoElement.src = currentVideo.url;
videoElement.load();
// --- Setup Inactivity Controls (session-specific) ---
showControlsAndResetTimer(); // Initial call
updatePlayPauseButtonState(); // Initial state for the button
// Show overlay and request fullscreen
overlayContainer.css('display', 'flex');
const containerEl = overlayContainer[0];
if (containerEl.requestFullscreen) {
containerEl.requestFullscreen().catch(err => console.error("Error attempting to enable full-screen mode:", err));
} else if (containerEl.mozRequestFullScreen) {
containerEl.mozRequestFullScreen();
} else if (containerEl.webkitRequestFullscreen) {
containerEl.webkitRequestFullscreen();
} else if (containerEl.msRequestFullscreen) {
containerEl.msRequestFullscreen();
}
// FOCUS: Set focus in the video overlay
if (playPauseButton && playPauseButton.length) {
playPauseButton.focus();
}
updatePlayPauseButtonState()
}
$(document).on("mousemove", function () {
showControlsAndResetTimer()
})
$(document).ready(function() {
// Initialize global selectors for static elements
overlayContainer = $("#video-overlay-container");
videoElement = overlayContainer.find(".video-player")[0];
backButton = overlayContainer.find("#back-button");
customControls = overlayContainer.find("#custom-video-controls");
playPauseButton = customControls.find("#custom-play-pause-button");
seekBar = customControls.find("#custom-seek-bar");
// Spinner is visible by default (CSS). Video container is hidden (CSS).
// Pre-set all thumbnails to a placeholder state
$(".video-thumbnail").attr("src", "data:image/svg+xml,%3Csvg xmlns=\\\'http://www.w3.org/2000/svg\\\' width=\\\'100%25\\\' height=\\\'100%25\\\' viewBox=\\\'0 0 16 9\\\'%3E%3C/svg%3E");
$.getJSON("/api/homepage", function(data) {
createVideoThumbnailElement(data.currentVideo, true, true);
for (let i = 0; i < data.nextVideos.length; i++)
createVideoThumbnailElement(data.nextVideos[i], false, true);
// currentVideo = data.currentVideo;
// nextVideo = data.nextVideo;
// $("#current-thumb").attr("src", currentVideo.thumbnail);
// $("#next-thumb").attr("src", nextVideo.thumbnail);
// Consider populating prev-thumb and next-thumb here if data provides them
hideSpinner();
showVideoContainer();
selectThumbnail(selectedThumbnailIndex);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.error("Failed to load homepage data:", textStatus, errorThrown);
hideSpinner();
// Display a user-friendly error message
if (!$('#init-error-msg').length) { // Prevent multiple error messages
$('body').append('<div id="init-error-msg" style="color:white; text-align:center; padding:20px; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background-color:rgba(0,0,0,0.8); border-radius:8px; z-index: 10002;">Failed to load content. Please try again later.</div>');
}
});
// Attach event listeners that can be set up once for static controls
if (playPauseButton) playPauseButton.click(handlePlayPauseClick);
if (seekBar) seekBar.on('input', handleSeekBarInput);
if (backButton) backButton.click(exitVideoPlayback);
// --- Setup Custom Video Controls Events (session-specific for video element) ---
$(videoElement).on('play.currentVideo', updatePlayPauseButtonState);
$(videoElement).on('pause.currentVideo', updatePlayPauseButtonState);
$(videoElement).on('loadedmetadata.currentVideo', handleVideoLoadedMetadata);
$(videoElement).on('ended.currentVideo', exitVideoPlayback);
setInterval(handleVideoTimeUpdate, 1000/30);
// --- Setup Fullscreen Change Listener (session-specific) ---
$(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
$(document).on('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
overlayContainer.on('mousemove.inactivityControls', showControlsAndResetTimer);
// --- TV Remote/Keyboard Navigation ---
const getOverlayViewFocusableElements = () => [backButton, playPauseButton, seekBar].filter(el => el && el.length > 0 && el.is(':visible'));
$(document).on('keydown', function(e) {
let isOverlayVisible = overlayContainer.is(':visible');
let activeFocusableSet;
if (isOverlayVisible) {
activeFocusableSet = getOverlayViewFocusableElements();
const focusedElement = $(document.activeElement);
let currentIndex = -1;
for (let i = 0; i < activeFocusableSet.length; i++) {
if (focusedElement.is(activeFocusableSet[i])) {
currentIndex = i;
break;
}
}
}
// Global key handling for overlay
if (isOverlayVisible) {
switch (e.key || e.keyCode) {
case 'Backspace':
case 8:
e.preventDefault();
if (backButton && backButton.length) backButton.click();
return;
case 'Enter':
case 13:
case 179: // MediaPlayPause
e.preventDefault();
if (playPauseButton && playPauseButton.length) playPauseButton.click();
return;
case 178: // MediaStop
e.preventDefault();
exitVideoPlayback();
return;
case 'ArrowLeft':
case 37: // ArrowLeft
videoElement.currentTime = Math.max(0, videoElement.currentTime - 5); // Seek back 5s
handleVideoTimeUpdate();
showControlsAndResetTimer();
e.preventDefault();
return;
case 'ArrowRight':
case 39: // ArrowRight
videoElement.currentTime = Math.min(videoElement.duration, videoElement.currentTime + 5); // Seek forward 5s
handleVideoTimeUpdate();
showControlsAndResetTimer();
e.preventDefault();
return;
}
}
let keyHandled = false;
let nextIndex;
switch (e.key || e.keyCode) {
case 'ArrowLeft':
case 37: // ArrowLeft
nextIndex = Math.max(selectedThumbnailIndex - 1, 0);
selectThumbnail(nextIndex);
keyHandled = true;
break;
case 'ArrowRight':
case 39: // ArrowRight
nextIndex = Math.min(selectedThumbnailIndex + 1, videoInfo.length - 1);
selectThumbnail(nextIndex);
keyHandled = true;
break;
case 'Enter':
case 13: // Enter
startVideo();
keyHandled = true;
break;
case 179: // MediaPlayPause (if not handled by global overlay section)
if (!isOverlayVisible && $('#play-button').length) {
$('#play-button').click();
keyHandled = true;
}
break;
}
if (keyHandled) {
e.preventDefault();
}
});
})
</script>
</head>
<body>
<div class="loader" id="loader"></div>
<div id="video-container">
<!-- <img id="prev-thumb" class="video-thumbnail side-video" alt="Previous Video" tabindex="0">-->
<!-- <img id="current-thumb" class="video-thumbnail current-video" alt="Current Video" tabindex="0">-->
<!-- <img id="next-thumb" class="video-thumbnail side-video" alt="Next Video" tabindex="0">-->
<!-- <div class="play-button" id="play-button" tabindex="0" role="button" aria-label="Play video"></div>-->
</div>
<!-- STATIC VIDEO OVERLAY STRUCTURE -->
<div id="video-overlay-container">
<video class="video-player"></video> <!-- no src, no autoplay, no controls initially -->
<button id="back-button" tabindex="0">Back</button>
<div id="custom-video-controls">
<button id="custom-play-pause-button" class="nofocus" tabindex="0"></button> <!-- Default to Play icon -->
<input type="range" id="custom-seek-bar" class="nofocus" value="0" step="any" tabindex="0" aria-label="Video seek bar" />
</div>
</div>
</body>
</html>