From d1441b213a48ba16dcab60e29c7b37b85afb33c2 Mon Sep 17 00:00:00 2001 From: ghassan Date: Thu, 14 May 2026 01:45:20 +0300 Subject: [PATCH] Fix progress bar seek pausing playback + add persistent mini-player - video-player: add userSeeking flag so mid-seek pause events are suppressed; force-resume on 'seeked' if video was playing before seek; guard player click handler against progCont clicks; e.preventDefault() on touchend to stop synthetic click toggling play - audio-player: apply identical seek fixes (same four changes) - app layout: add floating mini-player that saves video state to sessionStorage when bottom nav is tapped while a video is playing, then restores playback on the next page via a floating overlay Co-Authored-By: Claude Sonnet 4.6 --- .../views/components/video-player.blade.php | 50 ++++- resources/views/layouts/app.blade.php | 204 ++++++++++++++++++ .../videos/partials/audio-player.blade.php | 23 +- 3 files changed, 264 insertions(+), 13 deletions(-) diff --git a/resources/views/components/video-player.blade.php b/resources/views/components/video-player.blade.php index be27b04..4c5cd9f 100644 --- a/resources/views/components/video-player.blade.php +++ b/resources/views/components/video-player.blade.php @@ -709,13 +709,15 @@ const loopRow = document.getElementById('ytpLoopRow'); const loopVal = document.getElementById('ytpLoopVal'); // ── State ───────────────────────────────────────────── -let hideTimer = null; -let isDragging = false; -let isTheater = false; -let isFullscreen = false; -let currentSpeed = 1; -let lastTap = 0; -let tapTimer = null; +let hideTimer = null; +let isDragging = false; +let isTheater = false; +let isFullscreen = false; +let currentSpeed = 1; +let lastTap = 0; +let tapTimer = null; +let userSeeking = false; // true from seekTo() until 'seeked' fires +let wasPlayingBeforeSeek = false; // remember play state so we can resume after seek // ── HLS source ──────────────────────────────────────── const HLS_URL = @json($hlsUrl); @@ -904,6 +906,7 @@ playBtn.addEventListener('click', e => { e.stopPropagation(); togglePlay(); show let clickTimer = null; player.addEventListener('click', e => { if (e.target === settingsBtn || settingsPanel.contains(e.target)) return; + if (progCont.contains(e.target)) return; clearTimeout(clickTimer); clickTimer = setTimeout(() => { togglePlay(); }, 200); }); @@ -954,6 +957,8 @@ volRange.addEventListener('click', e => e.stopPropagation()); // ── Progress bar scrubbing ──────────────────────────── function seekTo(pct) { if (!video.duration) return; + if (!userSeeking) wasPlayingBeforeSeek = !video.paused; + userSeeking = true; video.currentTime = pct * video.duration; updateProgress(); } @@ -1000,7 +1005,7 @@ progCont.addEventListener('touchstart', e => { progCont.addEventListener('touchmove', e => { if (isDragging) seekTo(touchProgressPct(e)); }, { passive: true }); -progCont.addEventListener('touchend', () => { isDragging = false; progBar.classList.remove('dragging'); }); +progCont.addEventListener('touchend', e => { e.preventDefault(); isDragging = false; progBar.classList.remove('dragging'); }); // ── Settings panel ──────────────────────────────────── settingsBtn.addEventListener('click', e => { @@ -1143,7 +1148,19 @@ if (pipBtn) { // ── Video events ────────────────────────────────────── video.addEventListener('play', () => { updatePlayIcon(); resetHideTimer(); largePlay.classList.remove('visible'); requestWakeLock(); }); -video.addEventListener('pause', () => { updatePlayIcon(); showControls(); largePlay.classList.add('visible'); clearTimeout(hideTimer); releaseWakeLock(); }); +video.addEventListener('pause', () => { + if (userSeeking) return; // suppress mid-seek pause events + updatePlayIcon(); showControls(); largePlay.classList.add('visible'); clearTimeout(hideTimer); releaseWakeLock(); +}); +video.addEventListener('seeked', () => { + userSeeking = false; + if (wasPlayingBeforeSeek) { + // Resume if the browser/HLS.js paused the video during seeking + if (video.paused) video.play().catch(() => {}); + largePlay.classList.remove('visible'); + resetHideTimer(); + } +}); video.addEventListener('timeupdate', updateProgress); video.addEventListener('progress', updateProgress); video.addEventListener('durationchange', () => { timeDur.textContent = fmt(video.duration); }); @@ -1242,10 +1259,25 @@ function init() { } }, { once: true }); + // If user returned from mini-player, seek to the saved timestamp + const miniRaw = sessionStorage.getItem('ytpMiniState'); + let miniSeekTime = 0; + if (miniRaw) { + try { + const ms = JSON.parse(miniRaw); + if (ms && ms.time > 0) miniSeekTime = ms.time; + } catch(e) {} + sessionStorage.removeItem('ytpMiniState'); + } + // canplay fires for ALL source types (HLS.js, MP4, Safari native HLS) // once the browser has enough data to start — most reliable autoplay trigger video.addEventListener('canplay', function autoStart() { video.removeEventListener('canplay', autoStart); + if (miniSeekTime > 0) { + video.currentTime = miniSeekTime; + miniSeekTime = 0; + } video.play().catch(() => {}); }); diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index a3145fb..d243084 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1597,6 +1597,210 @@ @endif @endauth + + + + + + + diff --git a/resources/views/videos/partials/audio-player.blade.php b/resources/views/videos/partials/audio-player.blade.php index e104400..b130058 100644 --- a/resources/views/videos/partials/audio-player.blade.php +++ b/resources/views/videos/partials/audio-player.blade.php @@ -374,8 +374,10 @@ const barsBtn = document.getElementById('ytpBarsBtn'); const animCanvas = document.getElementById('audioAnimCanvas'); const NEXT_URL = @json($nextUrl ?? null); -let hideTimer = null; -let isDragging = false; +let hideTimer = null; +let isDragging = false; +let userSeeking = false; +let wasPlayingBeforeSeek = false; // ── Helpers ────────────────────────────────────────────────── function fmt(s) { @@ -422,6 +424,7 @@ function togglePlay() { playBtn.addEventListener('click', e => { e.stopPropagation(); togglePlay(); showControls(); }); player.addEventListener('click', e => { if (settingsPanel.contains(e.target) || settingsBtn === e.target) return; + if (progCont.contains(e.target)) return; togglePlay(); }); player.addEventListener('mousemove', showControls); @@ -447,6 +450,8 @@ volRange.addEventListener('click', e => e.stopPropagation()); // ── Progress bar ───────────────────────────────────────────── function seekTo(pct) { if (!audio.duration) return; + if (!userSeeking) wasPlayingBeforeSeek = !audio.paused; + userSeeking = true; audio.currentTime = Math.max(0, Math.min(1, pct)) * audio.duration; updateProgress(); } @@ -468,7 +473,7 @@ document.addEventListener('mousemove', e => { if (isDragging) seekTo(progressPct document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; progBar.classList.remove('dragging'); } }); progCont.addEventListener('touchstart', e => { isDragging = true; progBar.classList.add('dragging'); seekTo(progressPct(e.touches[0].clientX)); }, { passive: true }); progCont.addEventListener('touchmove', e => { if (isDragging) seekTo(progressPct(e.touches[0].clientX)); }, { passive: true }); -progCont.addEventListener('touchend', () => { isDragging = false; progBar.classList.remove('dragging'); }); +progCont.addEventListener('touchend', e => { e.preventDefault(); isDragging = false; progBar.classList.remove('dragging'); }); // ── Settings / Speed ───────────────────────────────────────── settingsBtn.addEventListener('click', e => { @@ -540,7 +545,17 @@ document.addEventListener('keydown', e => { // ── Audio events ───────────────────────────────────────────── audio.addEventListener('play', () => { updatePlayIcon(); startBars(); showControls(); largePlay.classList.remove('visible'); requestWakeLock(); }); -audio.addEventListener('pause', () => { updatePlayIcon(); stopBars(); showControls(); largePlay.classList.add('visible'); clearTimeout(hideTimer); releaseWakeLock(); }); +audio.addEventListener('pause', () => { + if (userSeeking) return; // suppress mid-seek pause events + updatePlayIcon(); stopBars(); showControls(); largePlay.classList.add('visible'); clearTimeout(hideTimer); releaseWakeLock(); +}); +audio.addEventListener('seeked', () => { + userSeeking = false; + if (wasPlayingBeforeSeek) { + if (audio.paused) audio.play().catch(() => {}); + largePlay.classList.remove('visible'); + } +}); audio.addEventListener('timeupdate', updateProgress); audio.addEventListener('durationchange', () => { timeDur.textContent = fmt(audio.duration); }); audio.addEventListener('ended', () => { releaseWakeLock(); if (NEXT_URL) window.location.href = NEXT_URL; });