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:
parent
05db0e128a
commit
99f71c54e5
@ -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();
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user