Fix playlist controls: add to type-specific views (music, generic, match)

Controls were only added to show.blade.php, but music/generic/match videos
render their own complete layouts with their own sidebars. Added the
pl-controls-bar to all three type views and the global CSS to app.blade.php.

- music: full standalone JS with shuffle/loop/autoplay + _plOnTrackEnd hook
- generic/match: syncs with video-player's existing ytpShuffleRow/ytpAutoplayRow toggles
- audio-player: ended handler now calls window._plOnTrackEnd if defined
- video-player: exposes window._ytpNav.next/prev for sidebar prev/next buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ghassan 2026-05-16 11:45:33 +03:00
parent 05db0e128a
commit 99f71c54e5
6 changed files with 217 additions and 4 deletions

View File

@ -1175,6 +1175,7 @@ function navigatePrev() {
const url = shuffleOn ? getShuffledPrevUrl() : PREV_URL;
if (url) window.location.href = url;
}
window._ytpNav = { next: navigateNext, prev: navigatePrev };
video.addEventListener('ended', () => {
updatePlayIcon();

View File

@ -789,6 +789,39 @@
border-color: #ef4444 !important;
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
/* ── Playlist controls bar (sidebar, all video types) ── */
.pl-controls-bar {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 10px;
padding: 6px 8px;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-color);
flex-shrink: 0;
}
.pl-ctrl-btn {
display: flex;
align-items: center;
gap: 5px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 5px 8px;
border-radius: 6px;
font-size: 15px;
transition: color .15s, background .15s;
white-space: nowrap;
}
.pl-ctrl-btn:hover:not(:disabled) { color: var(--text-primary); background: rgba(255,255,255,.07); }
.pl-ctrl-btn:disabled { opacity: .35; cursor: default; }
.pl-ctrl-btn.pl-ctrl-active { color: var(--brand-red); }
.pl-ctrl-divider { width: 1px; height: 18px; background: var(--border-color); margin: 0 2px; flex-shrink: 0; }
.pl-ctrl-autoplay { margin-left: auto; }
.pl-autoplay-label { font-size: 12px; font-weight: 500; }
</style>
@yield('extra_styles')

View File

@ -558,7 +558,7 @@ audio.addEventListener('seeked', () => {
});
audio.addEventListener('timeupdate', updateProgress);
audio.addEventListener('durationchange', () => { timeDur.textContent = fmt(audio.duration); });
audio.addEventListener('ended', () => { releaseWakeLock(); if (NEXT_URL) window.location.href = NEXT_URL; });
audio.addEventListener('ended', () => { releaseWakeLock(); if (window._plOnTrackEnd) { window._plOnTrackEnd(); } else if (NEXT_URL) { window.location.href = NEXT_URL; } });
audio.addEventListener('volumechange', updateVolumeIcon);
// ── Loop ─────────────────────────────────────────────────────

View File

@ -441,13 +441,57 @@
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container" style="display: flex; flex-direction: column; gap: 16px;">
@if ($playlist && $playlistVideos && $playlistVideos->count() > 0)
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 8px;">
<i class="bi bi-collection-play" style="margin-right: 8px;"></i>
{{ $playlist->name }}
<span
style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }}
videos)</span>
</h3>
{{-- Playlist controls --}}
<div class="pl-controls-bar">
<button class="pl-ctrl-btn" id="plPrevBtn" title="Previous"
@if(!($previousVideo ?? null)) disabled @endif
onclick="window._ytpNav?.prev()">
<i class="bi bi-skip-start-fill"></i>
</button>
<button class="pl-ctrl-btn" id="plNextBtn" title="Next"
onclick="window._ytpNav?.next()">
<i class="bi bi-skip-end-fill"></i>
</button>
<div class="pl-ctrl-divider"></div>
<button class="pl-ctrl-btn" id="plShuffleBtn" title="Shuffle"
onclick="document.getElementById('ytpShuffleRow')?.click(); plSyncGeneric()">
<i class="bi bi-shuffle"></i>
</button>
<button class="pl-ctrl-btn" id="plLoopBtn" title="Loop"
onclick="document.getElementById('ytpLoopRow')?.click(); plSyncGeneric()">
<i class="bi bi-repeat"></i>
</button>
<button class="pl-ctrl-btn pl-ctrl-autoplay" id="plAutoplayBtn" title="Autoplay"
onclick="document.getElementById('ytpAutoplayRow')?.click(); plSyncGeneric()">
<i class="bi bi-play-circle"></i>
<span class="pl-autoplay-label">Autoplay</span>
</button>
</div>
<script>
function plSyncGeneric(){
var sb = document.getElementById('plShuffleBtn');
var lb = document.getElementById('plLoopBtn');
var ab = document.getElementById('plAutoplayBtn');
var sr = document.getElementById('ytpShuffleRow');
var lr = document.getElementById('ytpLoopRow');
var ar = document.getElementById('ytpAutoplayRow');
if(sb && sr) sb.classList.toggle('pl-ctrl-active', sr.classList.contains('is-on'));
if(lb && lr) lb.classList.toggle('pl-ctrl-active', lr.classList.contains('is-on'));
if(ab && ar) ab.classList.toggle('pl-ctrl-active', ar.classList.contains('is-on'));
}
document.addEventListener('DOMContentLoaded', function(){ setTimeout(plSyncGeneric, 100); });
if(document.readyState !== 'loading') setTimeout(plSyncGeneric, 100);
</script>
<div class="recommended-videos-list">
@foreach ($playlistVideos as $index => $playlistVideo)
@if ($playlistVideo->id !== $video->id)

View File

@ -2529,13 +2529,53 @@
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container">
@if ($playlist && $playlistVideos && $playlistVideos->count() > 0)
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 8px;">
<i class="bi bi-collection-play" style="margin-right: 8px;"></i>
{{ $playlist->name }}
<span
style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }}
videos)</span>
</h3>
{{-- Playlist controls --}}
<div class="pl-controls-bar">
<button class="pl-ctrl-btn" id="plPrevBtn" title="Previous"
@if(!($previousVideo ?? null)) disabled @endif
onclick="window._ytpNav?.prev()">
<i class="bi bi-skip-start-fill"></i>
</button>
<button class="pl-ctrl-btn" id="plNextBtn" title="Next"
onclick="window._ytpNav?.next()">
<i class="bi bi-skip-end-fill"></i>
</button>
<div class="pl-ctrl-divider"></div>
<button class="pl-ctrl-btn" id="plShuffleBtn" title="Shuffle"
onclick="document.getElementById('ytpShuffleRow')?.click(); plSyncGeneric()">
<i class="bi bi-shuffle"></i>
</button>
<button class="pl-ctrl-btn" id="plLoopBtn" title="Loop"
onclick="document.getElementById('ytpLoopRow')?.click(); plSyncGeneric()">
<i class="bi bi-repeat"></i>
</button>
<button class="pl-ctrl-btn pl-ctrl-autoplay" id="plAutoplayBtn" title="Autoplay"
onclick="document.getElementById('ytpAutoplayRow')?.click(); plSyncGeneric()">
<i class="bi bi-play-circle"></i>
<span class="pl-autoplay-label">Autoplay</span>
</button>
</div>
<script>
function plSyncGeneric(){
var sb=document.getElementById('plShuffleBtn'), lb=document.getElementById('plLoopBtn'), ab=document.getElementById('plAutoplayBtn');
var sr=document.getElementById('ytpShuffleRow'), lr=document.getElementById('ytpLoopRow'), ar=document.getElementById('ytpAutoplayRow');
if(sb&&sr) sb.classList.toggle('pl-ctrl-active',sr.classList.contains('is-on'));
if(lb&&lr) lb.classList.toggle('pl-ctrl-active',lr.classList.contains('is-on'));
if(ab&&ar) ab.classList.toggle('pl-ctrl-active',ar.classList.contains('is-on'));
}
document.addEventListener('DOMContentLoaded',function(){ setTimeout(plSyncGeneric,100); });
if(document.readyState!=='loading') setTimeout(plSyncGeneric,100);
</script>
<div class="recommended-videos-list">
@foreach ($playlistVideos as $index => $playlistVideo)
@if ($playlistVideo->id !== $video->id)

View File

@ -469,13 +469,108 @@
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container" style="display: flex; flex-direction: column; gap: 16px;">
@if ($playlist && $playlistVideos && $playlistVideos->count() > 0)
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 8px;">
<i class="bi bi-collection-play" style="margin-right: 8px;"></i>
{{ $playlist->name }}
<span
style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }}
videos)</span>
</h3>
{{-- Playlist controls --}}
<div class="pl-controls-bar">
<button class="pl-ctrl-btn" id="plPrevBtn" title="Previous"
@if(!($previousVideo ?? null)) disabled @endif
onclick="plGoTo('{{ ($previousVideo ?? null) ? route('videos.show', $previousVideo).'?playlist='.$playlist->share_token : '' }}')">
<i class="bi bi-skip-start-fill"></i>
</button>
<button class="pl-ctrl-btn" id="plNextBtn" title="Next" onclick="plNext()">
<i class="bi bi-skip-end-fill"></i>
</button>
<div class="pl-ctrl-divider"></div>
<button class="pl-ctrl-btn" id="plShuffleBtn" title="Shuffle" onclick="plToggleShuffle()">
<i class="bi bi-shuffle"></i>
</button>
<button class="pl-ctrl-btn" id="plLoopBtn" title="Loop" onclick="plToggleLoop()">
<i class="bi bi-repeat"></i>
</button>
<button class="pl-ctrl-btn pl-ctrl-autoplay" id="plAutoplayBtn" title="Autoplay" onclick="plToggleAutoplay()">
<i class="bi bi-play-circle"></i>
<span class="pl-autoplay-label">Autoplay</span>
</button>
</div>
<script>
(function () {
var PL_ID = '{{ $playlist->share_token }}';
var PL_CURRENT = {{ $video->id }};
var PL_NEXT_URL = '{{ ($nextVideo ?? null) ? route("videos.show", $nextVideo)."?playlist=".$playlist->share_token : "" }}';
var PL_PREV_URL = '{{ ($previousVideo ?? null) ? route("videos.show", $previousVideo)."?playlist=".$playlist->share_token : "" }}';
var PL_FIRST_URL= '{{ $playlistVideos->count() ? route("videos.show", $playlistVideos->first())."?playlist=".$playlist->share_token : "" }}';
var PL_VIDEOS = {!! $playlistVideos->map(fn($v) => ['id' => $v->id, 'url' => route('videos.show', $v).'?playlist='.$playlist->share_token])->values()->toJson() !!};
function plGet(k,d){ var v=localStorage.getItem('pl_'+k+'_'+PL_ID); return v!==null?v:d; }
function plSet(k,v){ localStorage.setItem('pl_'+k+'_'+PL_ID,v); }
var plAutoplay = plGet('autoplay','1');
var plLoop = plGet('loop','off');
var plShuffle = plGet('shuffle','0');
function plShuffleOrder(){ var o=PL_VIDEOS.map(function(_,i){return i;}); for(var i=o.length-1;i>0;i--){var j=Math.floor(Math.random()*(i+1));var t=o[i];o[i]=o[j];o[j]=t;} localStorage.setItem('pl_shuffleOrder_'+PL_ID,JSON.stringify(o)); return o; }
function plGetOrder(){ var r=localStorage.getItem('pl_shuffleOrder_'+PL_ID); if(r){try{return JSON.parse(r);}catch(e){}} return null; }
window.plGoTo = function(url){ if(url) window.location.href=url; };
window.plNext = function(){
if(plShuffle==='1'){
var order=plGetOrder()||plShuffleOrder();
var curIdx=PL_VIDEOS.findIndex(function(v){return v.id===PL_CURRENT;});
var pos=order.indexOf(curIdx);
var nxt=(pos+1)%order.length;
if(pos===order.length-1&&plLoop!=='all') return;
window.location.href=PL_VIDEOS[order[nxt]].url;
} else {
if(PL_NEXT_URL) window.location.href=PL_NEXT_URL;
else if(plLoop==='all'&&PL_FIRST_URL) window.location.href=PL_FIRST_URL;
}
};
window._plOnTrackEnd = function(){
if(plLoop==='one') return; // native loop handles it
if(plAutoplay==='1') window.plNext();
};
function plRender(){
var sb=document.getElementById('plShuffleBtn');
var lb=document.getElementById('plLoopBtn');
var ab=document.getElementById('plAutoplayBtn');
var nb=document.getElementById('plNextBtn');
var pb=document.getElementById('plPrevBtn');
if(sb){ sb.classList.toggle('pl-ctrl-active',plShuffle==='1'); sb.title='Shuffle: '+(plShuffle==='1'?'On':'Off'); }
if(lb){
lb.classList.remove('pl-ctrl-active');
if(plLoop==='all'){ lb.classList.add('pl-ctrl-active'); lb.innerHTML='<i class="bi bi-repeat"></i>'; lb.title='Loop: All'; }
else if(plLoop==='one'){ lb.classList.add('pl-ctrl-active'); lb.innerHTML='<i class="bi bi-repeat-1"></i>'; lb.title='Loop: One'; }
else { lb.innerHTML='<i class="bi bi-repeat"></i>'; lb.title='Loop: Off'; }
}
if(ab){ ab.classList.toggle('pl-ctrl-active',plAutoplay==='1'); ab.title='Autoplay: '+(plAutoplay==='1'?'On':'Off'); }
if(nb) nb.disabled=!PL_NEXT_URL&&plLoop!=='all'&&plShuffle!=='1';
if(pb) pb.disabled=!PL_PREV_URL;
}
window.plToggleShuffle = function(){ plShuffle=plShuffle==='1'?'0':'1'; plSet('shuffle',plShuffle); if(plShuffle==='1') plShuffleOrder(); else localStorage.removeItem('pl_shuffleOrder_'+PL_ID); plRender(); };
window.plToggleLoop = function(){ var s=['off','all','one']; var i=s.indexOf(plLoop); plLoop=s[(i+1)%s.length]; plSet('loop',plLoop); plRender(); var a=document.getElementById('audioEl'); if(a) a.loop=(plLoop==='one'); };
window.plToggleAutoplay= function(){ plAutoplay=plAutoplay==='1'?'0':'1'; plSet('autoplay',plAutoplay); plRender(); };
// Apply state after audio player is ready
document.addEventListener('DOMContentLoaded', function(){
var a=document.getElementById('audioEl');
if(a&&plLoop==='one') a.loop=true;
plRender();
});
if(document.readyState!=='loading'){ var a=document.getElementById('audioEl'); if(a&&plLoop==='one') a.loop=true; plRender(); }
})();
</script>
<div class="recommended-videos-list">
@foreach ($playlistVideos as $index => $playlistVideo)
@if ($playlistVideo->id !== $video->id)