From 46c68813ec720c199c7d8925edf972da508befa3 Mon Sep 17 00:00:00 2001 From: Yuri Date: Thu, 1 Jan 2026 11:07:07 -0500 Subject: [PATCH] add video controls --- public/css/main.css | 83 +++++++++++++++++++ public/index.html | 27 +++++- public/js/components/VideoPlayer.js | 124 ++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 1 deletion(-) diff --git a/public/css/main.css b/public/css/main.css index 918e57b..f51b7bf 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -616,6 +616,89 @@ h4 { } } +/* Custom Video Controls */ +.video-controls { + position: absolute; + top: var(--space-md); + right: var(--space-md); + display: none; + flex-direction: column; + gap: var(--space-sm); + z-index: 50; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.video-controls.show { + pointer-events: auto; +} + +.video-container:hover .video-controls, +.video-controls.show { + opacity: 1; +} + +.video-control-btn { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--glass-bg); + backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + color: var(--color-text-primary); + cursor: pointer; + transition: all var(--transition-fast); + box-shadow: var(--shadow-md); +} + +.video-control-btn:hover { + background: var(--color-accent); + border-color: var(--color-accent); + transform: scale(1.05); +} + +.video-control-btn:active { + transform: scale(0.95); +} + +.video-control-btn .icon { + width: 24px; + height: 24px; +} + +.video-control-btn.muted .icon { + color: var(--color-error); +} + +/* Show custom controls on mobile as supplementary */ +@media (max-width: 768px) { + .video-controls { + display: flex; + } + + /* Make control buttons larger on mobile */ + .video-control-btn { + width: 56px; + height: 56px; + } + + .video-control-btn .icon { + width: 28px; + height: 28px; + } +} + +/* Touch devices */ +@media (hover: none) and (pointer: coarse) { + .video-controls { + display: flex; + } +} + /* Closed Caption styling */ video::cue { background-color: rgba(0, 0, 0, 0.8); diff --git a/public/index.html b/public/index.html index e57b42a..1f59960 100644 --- a/public/index.html +++ b/public/index.html @@ -123,7 +123,32 @@
- + + + +
+ + + + +
+

Select a channel to start watching

diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js index bec50f3..1fafab2 100644 --- a/public/js/components/VideoPlayer.js +++ b/public/js/components/VideoPlayer.js @@ -133,6 +133,127 @@ class VideoPlayer { }; } + /** + * Initialize custom video controls for mobile + */ + initCustomControls() { + 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'); + + if (!btnPlay || !btnMute || !btnPip || !btnFullscreen || !controls) return; + + // Always use native controls + this.video.controls = true; + + // Play/Pause button + btnPlay.addEventListener('click', () => { + if (this.video.paused) { + this.video.play(); + } else { + this.video.pause(); + } + }); + + // Update play/pause icon + const updatePlayIcon = () => { + const icon = btnPlay.querySelector('.icon'); + if (this.video.paused) { + icon.innerHTML = ''; + btnPlay.title = 'Play'; + } else { + icon.innerHTML = ''; + btnPlay.title = 'Pause'; + } + }; + + this.video.addEventListener('play', updatePlayIcon); + this.video.addEventListener('pause', updatePlayIcon); + + // Mute/Unmute button + btnMute.addEventListener('click', () => { + this.video.muted = !this.video.muted; + btnMute.classList.toggle('muted', this.video.muted); + + // Update icon + const icon = btnMute.querySelector('.icon'); + if (this.video.muted) { + icon.innerHTML = ''; + } else { + icon.innerHTML = ''; + } + }); + + // 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); + }); + } + }); + + // 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); + }; + + 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 = ''; + } else { + icon.innerHTML = ''; + } + }); + } + init() { // Apply default/remembered volume const volume = this.settings.rememberVolume ? this.settings.lastVolume : this.settings.defaultVolume; @@ -146,6 +267,9 @@ class VideoPlayer { } }); + // Setup custom video controls + this.initCustomControls(); + // Initialize HLS.js if supported if (Hls.isSupported()) { this.hls = new Hls(this.getHlsConfig());