Update live tv player styling to match VOD player
This commit is contained in:
+92
-1
@@ -3293,6 +3293,35 @@ kbd {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.watch-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: none;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.watch-loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.watch-loading .loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Bottom Bar */
|
||||
.watch-bottom-bar {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
@@ -3382,6 +3411,68 @@ kbd {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Captions Menu */
|
||||
.watch-captions-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.watch-captions-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
margin-bottom: var(--space-sm);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
min-width: 160px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.watch-captions-menu.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.captions-menu-title {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.captions-menu-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.captions-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-primary);
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.captions-option:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.captions-option.active {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.captions-option.active::before {
|
||||
content: "✓ ";
|
||||
}
|
||||
|
||||
.watch-btn-lg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
@@ -3433,7 +3524,7 @@ kbd {
|
||||
/* Scroll Hint */
|
||||
.watch-scroll-hint {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
|
||||
+74
-22
@@ -126,33 +126,65 @@
|
||||
|
||||
<!-- Video Player Area -->
|
||||
<section class="player-section">
|
||||
<div class="video-container">
|
||||
<video id="video-player" controls playsinline></video>
|
||||
<div class="video-container" id="video-container">
|
||||
<video id="video-player" playsinline></video>
|
||||
|
||||
<!-- Custom Mobile Controls -->
|
||||
<div class="video-controls" id="video-controls">
|
||||
<button class="video-control-btn" id="btn-play" title="Play/Pause">
|
||||
<!-- Custom Controls Overlay -->
|
||||
<div class="watch-overlay hidden" id="player-controls-overlay">
|
||||
<!-- Top Bar -->
|
||||
<div class="watch-top-bar">
|
||||
<h2 class="watch-title-overlay" id="player-channel-name"></h2>
|
||||
</div>
|
||||
|
||||
<!-- Center: Large Play Button -->
|
||||
<button class="watch-center-play" id="player-center-play">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="video-control-btn" id="btn-mute" title="Mute/Unmute">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path
|
||||
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="video-control-btn" id="btn-pip" title="Picture in Picture">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path
|
||||
d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="video-control-btn" id="btn-fullscreen" title="Fullscreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div class="watch-loading" id="player-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar: Controls -->
|
||||
<div class="watch-bottom-bar">
|
||||
<div class="watch-controls">
|
||||
<button class="watch-btn watch-btn-lg" id="btn-play" title="Play/Pause">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="icon icon-play">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="icon icon-pause hidden">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="watch-spacer"></div>
|
||||
|
||||
<button class="watch-btn" id="btn-mute" title="Mute">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="icon icon-vol">
|
||||
<path
|
||||
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="icon icon-muted hidden">
|
||||
<path
|
||||
d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" />
|
||||
</svg>
|
||||
</button>
|
||||
<input type="range" class="watch-volume" id="player-volume" min="0" max="100" value="80">
|
||||
|
||||
<button class="watch-btn" id="btn-fullscreen" title="Fullscreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="player-overlay" id="player-overlay">
|
||||
@@ -569,6 +601,11 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div class="watch-loading" id="watch-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar: Controls -->
|
||||
<div class="watch-bottom-bar">
|
||||
<div class="watch-progress-container">
|
||||
@@ -611,6 +648,21 @@
|
||||
</svg>
|
||||
</button>
|
||||
<input type="range" class="watch-volume" id="watch-volume" min="0" max="100" value="80">
|
||||
<!-- Captions Button -->
|
||||
<div class="watch-captions-wrapper">
|
||||
<button class="watch-btn" id="watch-captions-btn" title="Captions">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path
|
||||
d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="watch-captions-menu hidden" id="watch-captions-menu">
|
||||
<div class="captions-menu-title">Subtitles</div>
|
||||
<div class="captions-menu-list" id="watch-captions-list">
|
||||
<button class="captions-option active" data-index="-1">Off</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="watch-btn" id="watch-fullscreen" title="Fullscreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
|
||||
|
||||
@@ -141,124 +141,186 @@ class VideoPlayer {
|
||||
/**
|
||||
* Initialize custom video controls for mobile
|
||||
*/
|
||||
/**
|
||||
* Initialize custom video controls
|
||||
*/
|
||||
initCustomControls() {
|
||||
// Elements
|
||||
this.controlsOverlay = document.getElementById('player-controls-overlay');
|
||||
this.centerPlayBtn = document.getElementById('player-center-play');
|
||||
this.loadingSpinner = document.getElementById('player-loading');
|
||||
|
||||
const btnPlay = document.getElementById('btn-play');
|
||||
const btnMute = document.getElementById('btn-mute');
|
||||
const btnPip = document.getElementById('btn-pip');
|
||||
const btnFullscreen = document.getElementById('btn-fullscreen');
|
||||
const controls = document.getElementById('video-controls');
|
||||
const volumeSlider = document.getElementById('player-volume');
|
||||
const channelNameEl = document.getElementById('player-channel-name');
|
||||
|
||||
if (!btnPlay || !btnMute || !btnPip || !btnFullscreen || !controls) return;
|
||||
if (!this.controlsOverlay) return;
|
||||
|
||||
// Always use native controls
|
||||
this.video.controls = true;
|
||||
// Disable native controls
|
||||
this.video.controls = false;
|
||||
|
||||
// Play/Pause button
|
||||
btnPlay.addEventListener('click', () => {
|
||||
// Initial State: Hide all overlay elements until content is loaded
|
||||
this.loadingSpinner?.classList.remove('show');
|
||||
this.centerPlayBtn?.classList.remove('show');
|
||||
this.controlsOverlay?.classList.add('hidden');
|
||||
|
||||
// Play/Pause toggle
|
||||
const togglePlay = () => {
|
||||
if (this.video.paused) {
|
||||
this.video.play();
|
||||
} else {
|
||||
this.video.pause();
|
||||
}
|
||||
};
|
||||
|
||||
btnPlay?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
togglePlay();
|
||||
});
|
||||
|
||||
// Update play/pause icon
|
||||
const updatePlayIcon = () => {
|
||||
const icon = btnPlay.querySelector('.icon');
|
||||
if (this.video.paused) {
|
||||
icon.innerHTML = '<path d="M8 5v14l11-7z"/>';
|
||||
btnPlay.title = 'Play';
|
||||
} else {
|
||||
icon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
||||
btnPlay.title = 'Pause';
|
||||
this.centerPlayBtn?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
togglePlay();
|
||||
});
|
||||
|
||||
// Update play/pause UI
|
||||
const updatePlayUI = () => {
|
||||
const isPaused = this.video.paused;
|
||||
|
||||
// Center button
|
||||
this.centerPlayBtn?.classList.toggle('show', isPaused);
|
||||
|
||||
// Bottom bar button
|
||||
const iconPlay = btnPlay?.querySelector('.icon-play');
|
||||
const iconPause = btnPlay?.querySelector('.icon-pause');
|
||||
|
||||
if (iconPlay && iconPause) {
|
||||
iconPlay.classList.toggle('hidden', !isPaused);
|
||||
iconPause.classList.toggle('hidden', isPaused);
|
||||
}
|
||||
};
|
||||
|
||||
this.video.addEventListener('play', updatePlayIcon);
|
||||
this.video.addEventListener('pause', updatePlayIcon);
|
||||
this.video.addEventListener('play', updatePlayUI);
|
||||
this.video.addEventListener('pause', updatePlayUI);
|
||||
|
||||
// Mute/Unmute button
|
||||
btnMute.addEventListener('click', () => {
|
||||
this.video.muted = !this.video.muted;
|
||||
btnMute.classList.toggle('muted', this.video.muted);
|
||||
// Loading spinner
|
||||
this.video.addEventListener('waiting', () => {
|
||||
this.loadingSpinner?.classList.add('show');
|
||||
this.centerPlayBtn?.classList.remove('show');
|
||||
});
|
||||
|
||||
// Update icon
|
||||
const icon = btnMute.querySelector('.icon');
|
||||
this.video.addEventListener('canplay', () => {
|
||||
this.loadingSpinner?.classList.remove('show');
|
||||
if (this.video.paused) {
|
||||
this.centerPlayBtn?.classList.add('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Mute/Volume
|
||||
const updateVolumeUI = () => {
|
||||
const isMuted = this.video.muted || this.video.volume === 0;
|
||||
const iconVol = btnMute?.querySelector('.icon-vol');
|
||||
const iconMuted = btnMute?.querySelector('.icon-muted');
|
||||
|
||||
if (iconVol && iconMuted) {
|
||||
iconVol.classList.toggle('hidden', isMuted);
|
||||
iconMuted.classList.toggle('hidden', !isMuted);
|
||||
}
|
||||
|
||||
if (volumeSlider) {
|
||||
volumeSlider.value = this.video.muted ? 0 : Math.round(this.video.volume * 100);
|
||||
}
|
||||
};
|
||||
|
||||
btnMute?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.video.muted) {
|
||||
icon.innerHTML = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
|
||||
this.video.muted = false;
|
||||
this.video.volume = (parseInt(volumeSlider?.value || 80) / 100) || 0.8;
|
||||
} else {
|
||||
icon.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>';
|
||||
this.video.muted = true;
|
||||
}
|
||||
updateVolumeUI();
|
||||
});
|
||||
|
||||
// Picture-in-Picture button
|
||||
if (document.pictureInPictureEnabled) {
|
||||
btnPip.addEventListener('click', async () => {
|
||||
try {
|
||||
if (document.pictureInPictureElement) {
|
||||
await document.exitPictureInPicture();
|
||||
} else {
|
||||
await this.video.requestPictureInPicture();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('PiP error:', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
btnPip.style.display = 'none';
|
||||
}
|
||||
|
||||
// Fullscreen button
|
||||
btnFullscreen.addEventListener('click', () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
this.container.requestFullscreen().catch(err => {
|
||||
console.error('Fullscreen error:', err);
|
||||
});
|
||||
}
|
||||
volumeSlider?.addEventListener('input', (e) => {
|
||||
e.stopPropagation();
|
||||
const val = parseInt(e.target.value);
|
||||
this.video.volume = val / 100;
|
||||
this.video.muted = val === 0;
|
||||
updateVolumeUI();
|
||||
});
|
||||
|
||||
// Show controls on tap/touch for mobile (only hide after interaction)
|
||||
if (isMobile) {
|
||||
let controlsTimeout;
|
||||
const hideControls = () => {
|
||||
clearTimeout(controlsTimeout);
|
||||
controlsTimeout = setTimeout(() => {
|
||||
controls.classList.remove('show');
|
||||
}, 4000);
|
||||
};
|
||||
this.video.addEventListener('volumechange', updateVolumeUI);
|
||||
|
||||
const showControls = () => {
|
||||
controls.classList.add('show');
|
||||
hideControls();
|
||||
};
|
||||
|
||||
this.video.addEventListener('click', showControls);
|
||||
this.video.addEventListener('touchstart', showControls);
|
||||
|
||||
// Keep controls visible when video is paused
|
||||
this.video.addEventListener('pause', () => {
|
||||
controls.classList.add('show');
|
||||
clearTimeout(controlsTimeout);
|
||||
});
|
||||
|
||||
this.video.addEventListener('play', () => {
|
||||
hideControls();
|
||||
});
|
||||
}
|
||||
|
||||
// Update fullscreen icon
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
const icon = btnFullscreen.querySelector('.icon');
|
||||
if (document.fullscreenElement) {
|
||||
icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
|
||||
} else {
|
||||
icon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
|
||||
}
|
||||
// Fullscreen
|
||||
btnFullscreen?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleFullscreen();
|
||||
});
|
||||
|
||||
this.container.addEventListener('dblclick', () => this.toggleFullscreen());
|
||||
|
||||
// Overlay Auto-hide Logic
|
||||
let overlayTimeout;
|
||||
|
||||
const showOverlay = () => {
|
||||
this.controlsOverlay.classList.remove('hidden');
|
||||
this.container.style.cursor = 'default';
|
||||
resetOverlayTimer();
|
||||
};
|
||||
|
||||
const hideOverlay = () => {
|
||||
if (!this.video.paused) {
|
||||
this.controlsOverlay.classList.add('hidden');
|
||||
this.container.style.cursor = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
const resetOverlayTimer = () => {
|
||||
clearTimeout(overlayTimeout);
|
||||
if (!this.video.paused) {
|
||||
overlayTimeout = setTimeout(hideOverlay, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
this.container.addEventListener('mousemove', showOverlay);
|
||||
this.container.addEventListener('click', showOverlay);
|
||||
this.container.addEventListener('touchstart', showOverlay);
|
||||
|
||||
this.video.addEventListener('play', resetOverlayTimer);
|
||||
this.video.addEventListener('pause', showOverlay);
|
||||
|
||||
// Update Title when channel changes
|
||||
window.addEventListener('channelChanged', (e) => {
|
||||
if (channelNameEl && e.detail) {
|
||||
channelNameEl.textContent = e.detail.name || e.detail.tvgName || 'Live TV';
|
||||
}
|
||||
showOverlay();
|
||||
});
|
||||
|
||||
// Initial state
|
||||
updatePlayUI();
|
||||
updateVolumeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle fullscreen mode
|
||||
*/
|
||||
toggleFullscreen() {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
this.container.requestFullscreen().catch(err => {
|
||||
console.error('Fullscreen error:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
init() {
|
||||
// Apply default/remembered volume
|
||||
const volume = this.settings.rememberVolume ? this.settings.lastVolume : this.settings.defaultVolume;
|
||||
@@ -462,6 +524,10 @@ class VideoPlayer {
|
||||
// Hide "select a channel" overlay
|
||||
this.overlay.classList.add('hidden');
|
||||
|
||||
// Show custom controls overlay
|
||||
this.controlsOverlay?.classList.remove('hidden');
|
||||
this.loadingSpinner?.classList.add('show');
|
||||
|
||||
// Determine if HLS or direct stream
|
||||
this.currentUrl = streamUrl;
|
||||
|
||||
@@ -766,6 +832,13 @@ class VideoPlayer {
|
||||
this.video.pause();
|
||||
this.video.src = '';
|
||||
this.video.load();
|
||||
|
||||
// Reset UI to idle state
|
||||
this.overlay.classList.remove('hidden'); // Show "Select a channel"
|
||||
this.controlsOverlay?.classList.add('hidden'); // Hide controls
|
||||
this.loadingSpinner?.classList.remove('show');
|
||||
this.centerPlayBtn?.classList.remove('show');
|
||||
this.nowPlaying.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ class WatchPage {
|
||||
this.timeCurrent = document.getElementById('watch-time-current');
|
||||
this.timeTotal = document.getElementById('watch-time-total');
|
||||
this.scrollHint = document.getElementById('watch-scroll-hint');
|
||||
this.loadingSpinner = document.getElementById('watch-loading');
|
||||
|
||||
// Next episode
|
||||
this.nextEpisodePanel = document.getElementById('watch-next-episode');
|
||||
@@ -53,6 +54,11 @@ class WatchPage {
|
||||
this.episodesSection = document.getElementById('watch-episodes');
|
||||
this.seasonsContainer = document.getElementById('watch-seasons');
|
||||
|
||||
// Captions
|
||||
this.captionsBtn = document.getElementById('watch-captions-btn');
|
||||
this.captionsMenu = document.getElementById('watch-captions-menu');
|
||||
this.captionsList = document.getElementById('watch-captions-list');
|
||||
|
||||
// State
|
||||
this.hls = null;
|
||||
this.content = null;
|
||||
@@ -62,6 +68,7 @@ class WatchPage {
|
||||
this.currentEpisode = null;
|
||||
this.isFavorite = false;
|
||||
this.returnPage = null;
|
||||
this.captionsMenuOpen = false;
|
||||
|
||||
// Overlay timer
|
||||
this.overlayTimeout = null;
|
||||
@@ -105,6 +112,8 @@ class WatchPage {
|
||||
this.video?.addEventListener('pause', () => this.onPause());
|
||||
this.video?.addEventListener('ended', () => this.onEnded());
|
||||
this.video?.addEventListener('error', (e) => this.onError(e));
|
||||
this.video?.addEventListener('waiting', () => this.showLoading());
|
||||
this.video?.addEventListener('canplay', () => this.hideLoading());
|
||||
|
||||
// Overlay auto-hide
|
||||
const watchSection = document.querySelector('.watch-video-section');
|
||||
@@ -122,6 +131,19 @@ class WatchPage {
|
||||
this.nextPlayNowBtn?.addEventListener('click', () => this.playNextEpisode());
|
||||
this.nextCancelBtn?.addEventListener('click', () => this.cancelNextEpisode());
|
||||
|
||||
// Captions toggle
|
||||
this.captionsBtn?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleCaptionsMenu();
|
||||
});
|
||||
|
||||
// Close captions menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (this.captionsMenuOpen && !this.captionsMenu?.contains(e.target) && e.target !== this.captionsBtn) {
|
||||
this.closeCaptionsMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide scroll hint after scrolling
|
||||
const watchPage = document.getElementById('page-watch');
|
||||
watchPage?.addEventListener('scroll', () => {
|
||||
@@ -187,6 +209,9 @@ class WatchPage {
|
||||
// Stop any existing playback
|
||||
this.stop();
|
||||
|
||||
// Show loading spinner
|
||||
this.showLoading();
|
||||
|
||||
// Get settings for proxy/transcode
|
||||
let settings = {};
|
||||
try {
|
||||
@@ -383,7 +408,11 @@ class WatchPage {
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
console.error('Video error:', e);
|
||||
// Only log actual fatal errors, not benign stream recovery events
|
||||
const error = this.video?.error;
|
||||
if (error && error.code) {
|
||||
console.error('[WatchPage] Video error:', error.code, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
updateVolumeUI() {
|
||||
@@ -403,6 +432,87 @@ class WatchPage {
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// === Loading Spinner ===
|
||||
|
||||
showLoading() {
|
||||
this.loadingSpinner?.classList.add('show');
|
||||
this.centerPlayBtn?.classList.remove('show');
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
this.loadingSpinner?.classList.remove('show');
|
||||
}
|
||||
|
||||
// === Captions ===
|
||||
|
||||
toggleCaptionsMenu() {
|
||||
if (this.captionsMenuOpen) {
|
||||
this.closeCaptionsMenu();
|
||||
} else {
|
||||
this.updateCaptionsTracks();
|
||||
this.captionsMenu?.classList.remove('hidden');
|
||||
this.captionsMenuOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
closeCaptionsMenu() {
|
||||
this.captionsMenu?.classList.add('hidden');
|
||||
this.captionsMenuOpen = false;
|
||||
}
|
||||
|
||||
updateCaptionsTracks() {
|
||||
if (!this.captionsList || !this.video) return;
|
||||
|
||||
// Build list of available text tracks
|
||||
const tracks = this.video.textTracks;
|
||||
let html = '<button class="captions-option" data-index="-1">Off</button>';
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
if (track.kind === 'subtitles' || track.kind === 'captions') {
|
||||
const label = track.label || track.language || `Track ${i + 1}`;
|
||||
const isActive = track.mode === 'showing';
|
||||
html += `<button class="captions-option ${isActive ? 'active' : ''}" data-index="${i}">${label}</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any track is active, if not mark "Off" as active
|
||||
let anyActive = false;
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i].mode === 'showing') anyActive = true;
|
||||
}
|
||||
if (!anyActive) {
|
||||
html = html.replace('class="captions-option"', 'class="captions-option active"');
|
||||
}
|
||||
|
||||
this.captionsList.innerHTML = html;
|
||||
|
||||
// Add click handlers
|
||||
this.captionsList.querySelectorAll('.captions-option').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.selectCaptionTrack(parseInt(btn.dataset.index)));
|
||||
});
|
||||
}
|
||||
|
||||
selectCaptionTrack(index) {
|
||||
if (!this.video) return;
|
||||
|
||||
const tracks = this.video.textTracks;
|
||||
|
||||
// Disable all tracks
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
tracks[i].mode = 'hidden';
|
||||
}
|
||||
|
||||
// Enable selected track
|
||||
if (index >= 0 && index < tracks.length) {
|
||||
tracks[index].mode = 'showing';
|
||||
}
|
||||
|
||||
// Update UI
|
||||
this.updateCaptionsTracks();
|
||||
this.closeCaptionsMenu();
|
||||
}
|
||||
|
||||
// === Overlay Auto-Hide ===
|
||||
|
||||
showOverlay() {
|
||||
|
||||
Reference in New Issue
Block a user