Three bugs in the Up Next / playlist SPA transition for music-type videos: 1. Title selector was '.audio-title' (doesn't exist) instead of '.video-title span' 2. Cover image only updated #audioCoverImg — missed when video has a slideshow 3. Slideshow SLIDE_URLS lived in a closed IIFE and couldn't be updated cross-video Fix: always render both #audioCoverImg and #slideshowWrap in the DOM (toggle display via inline style), hoist slideshow state variables outside the if-block, and expose window._audioPlayerUpdate(d) that both recTransitionTo and plTransitionTo call. The hook handles all cases: cover-to-cover, cover-to-slideshow, slideshow-to-cover, slideshow-to-slideshow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
868 lines
40 KiB
PHP
868 lines
40 KiB
PHP
@php
|
|
$audioUrl = route('videos.stream', $video);
|
|
$coverUrl = $video->thumbnail ? route('media.thumbnail', $video->thumbnail) : asset('storage/images/logo.png');
|
|
$nextUrl = isset($nextVideo, $playlist) ? route('videos.show', $nextVideo).'?playlist='.$playlist->share_token : null;
|
|
$prevUrl = isset($previousVideo, $playlist) ? route('videos.show', $previousVideo).'?playlist='.$playlist->share_token : null;
|
|
$slideUrls = $video->slides->count() > 1
|
|
? $video->slides->map(fn($s) => route('media.thumbnail', $s->filename))->values()->all()
|
|
: [];
|
|
@endphp
|
|
|
|
<div class="ytp-wrap" id="ytpWrap">
|
|
<div class="ytp audio-ytp" id="audioContainer" tabindex="0">
|
|
|
|
{{-- Cover art / slideshow — both always in DOM so SPA transitions can switch between them --}}
|
|
<img src="{{ $coverUrl }}" alt="{{ $video->title }}" class="audio-cover-img" id="audioCoverImg"@if(count($slideUrls) > 1) style="display:none"@endif>
|
|
<div class="slideshow-wrap" id="slideshowWrap"@if(count($slideUrls) <= 1) style="display:none"@endif>
|
|
<img src="{{ $slideUrls[0] ?? $coverUrl }}" alt="" class="slide-img slide-a" id="slideA">
|
|
<img src="{{ count($slideUrls) > 1 ? $slideUrls[1] : ($slideUrls[0] ?? $coverUrl) }}" alt="" class="slide-img slide-b" id="slideB">
|
|
</div>
|
|
|
|
{{-- Bars canvas overlay --}}
|
|
<canvas id="audioAnimCanvas" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:none;z-index:3;"></canvas>
|
|
|
|
{{-- Gradient --}}
|
|
<div class="ytp-gradient-bottom"></div>
|
|
|
|
{{-- Large play overlay --}}
|
|
<div class="ytp-large-play-btn" id="ytpLargePlay">
|
|
<i class="bi bi-play-fill"></i>
|
|
</div>
|
|
|
|
{{-- Controls --}}
|
|
<div class="ytp-chrome-bottom" id="ytpControls">
|
|
|
|
<div class="ytp-progress-bar-container" id="ytpProgressContainer">
|
|
<div class="ytp-progress-bar" id="ytpProgressBar">
|
|
<div class="ytp-play-progress" id="ytpPlayed"></div>
|
|
<div class="ytp-scrubber-container">
|
|
<div class="ytp-scrubber-button" id="ytpScrubber"></div>
|
|
</div>
|
|
<div class="ytp-hover-time" id="ytpHoverTime"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ytp-chrome-controls">
|
|
|
|
<div class="ytp-left-controls">
|
|
|
|
<button class="ytp-button ytp-play-btn" id="ytpPlayBtn" title="Play (k)">
|
|
<svg class="ytp-svg-play" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
|
<svg class="ytp-svg-pause" viewBox="0 0 24 24" style="display:none"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
|
</button>
|
|
|
|
@if(isset($previousVideo) && $prevUrl)
|
|
<button class="ytp-button" title="Previous" onclick="window.location.href='{{ $prevUrl }}'">
|
|
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg>
|
|
</button>
|
|
@endif
|
|
|
|
@if(isset($nextVideo) && $nextUrl)
|
|
<button class="ytp-button" title="Next" onclick="window.location.href='{{ $nextUrl }}'">
|
|
<svg viewBox="0 0 24 24"><path d="m6 18 8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
|
</button>
|
|
@endif
|
|
|
|
<div class="ytp-volume-area">
|
|
<button class="ytp-button ytp-mute-btn" id="ytpMuteBtn" title="Mute (m)">
|
|
<svg class="ytp-svg-vol3" viewBox="0 0 24 24"><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 class="ytp-svg-vol0" viewBox="0 0 24 24" style="display:none"><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 3 3 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 4 9.91 6.09 12 8.18V4z"/></svg>
|
|
</button>
|
|
<div class="ytp-volume-slider-wrap">
|
|
<input type="range" class="ytp-volume-range" id="ytpVolume" min="0" max="100" step="1" value="50">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ytp-time-display">
|
|
<span id="ytpCurrent">0:00</span>
|
|
<span class="ytp-time-sep"> / </span>
|
|
<span id="ytpDuration">0:00</span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="ytp-right-controls">
|
|
|
|
<div class="ytp-settings-wrap" id="ytpSettingsWrap">
|
|
<button class="ytp-button ytp-settings-btn" id="ytpSettingsBtn" title="Settings">
|
|
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
|
</button>
|
|
<div class="ytp-settings-panel" id="ytpSettingsPanel">
|
|
<div class="ytp-settings-item" id="ytpSpeedRow">
|
|
<svg viewBox="0 0 24 24"><path d="M10 8v8l6-4-6-4zm6.5 4A6.5 6.5 0 1 1 9 6.04V4.02A8.5 8.5 0 1 0 18.5 12H16.5z"/></svg>
|
|
<span>Playback speed</span>
|
|
<span class="ytp-settings-val" id="ytpSpeedLabel">Normal</span>
|
|
<svg class="ytp-chevron" viewBox="0 0 24 24"><path d="M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
|
</div>
|
|
<div class="ytp-speed-panel" id="ytpSpeedPanel">
|
|
<div class="ytp-speed-back" id="ytpSpeedBack">
|
|
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
|
Playback speed
|
|
</div>
|
|
@foreach([['0.25','0.25'],['0.5','0.5'],['0.75','0.75'],['1','Normal'],['1.25','1.25'],['1.5','1.5'],['1.75','1.75'],['2','2']] as [$val,$label])
|
|
<div class="ytp-speed-option {{ $val === '1' ? 'active' : '' }}" data-speed="{{ $val }}">
|
|
<svg class="ytp-speed-check" viewBox="0 0 24 24"><path d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
|
{{ $label }}
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
{{-- Loop toggle --}}
|
|
<div class="ytp-settings-item ytp-toggle-row" id="ytpLoopRow">
|
|
<svg viewBox="0 0 24 24"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>
|
|
<span>Loop</span>
|
|
<span class="ytp-settings-val ytp-toggle-val" id="ytpLoopVal">Off</span>
|
|
<div class="ytp-toggle-switch"><div class="ytp-toggle-thumb"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Bars visualiser toggle --}}
|
|
<button class="ytp-button audio-bars-btn" id="ytpBarsBtn" title="Visualiser: Off">
|
|
<svg viewBox="0 0 24 24">
|
|
<rect x="2" y="14" width="4" height="8" rx="1" fill="white"/>
|
|
<rect x="8" y="8" width="4" height="14" rx="1" fill="white"/>
|
|
<rect x="14" y="11" width="4" height="11" rx="1" fill="white"/>
|
|
<rect x="20" y="5" width="2" height="17" rx="1" fill="white"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<button class="ytp-button ytp-fs-btn" id="ytpFsBtn" title="Full screen (f)">
|
|
<svg class="ytp-svg-fs-enter" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>
|
|
<svg class="ytp-svg-fs-exit" viewBox="0 0 24 24" style="display:none"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>
|
|
</button>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Hidden audio element --}}
|
|
<audio id="audioEl" src="{{ $audioUrl }}" preload="metadata"></audio>
|
|
|
|
{{-- ══ CSS ══ --}}
|
|
<style>
|
|
.audio-ytp { cursor: default; }
|
|
.audio-cover-img {
|
|
position: absolute;
|
|
inset: 0; width: 100%; height: 100%;
|
|
object-fit: cover; display: block;
|
|
}
|
|
|
|
/* Slideshow */
|
|
.slideshow-wrap { position: absolute; inset: 0; background: #000; }
|
|
.slide-img {
|
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
object-fit: cover; display: block;
|
|
transition: opacity 1s ease-in-out;
|
|
}
|
|
.slide-img.portrait { object-fit: contain; }
|
|
.slide-a { opacity: 1; z-index: 1; }
|
|
.slide-b { opacity: 0; z-index: 0; }
|
|
|
|
.audio-ytp .ytp-gradient-bottom,
|
|
.audio-ytp .ytp-chrome-bottom,
|
|
.audio-ytp .ytp-large-play-btn { z-index: 4; }
|
|
|
|
/* Bars button: dim when off, red when on */
|
|
.audio-bars-btn svg { opacity: .45; transition: opacity .15s; }
|
|
.audio-bars-btn.bars-on svg { opacity: 1; fill: #f00 !important; }
|
|
.audio-bars-btn.bars-on rect { fill: #f00 !important; }
|
|
|
|
/* ══ Full ytp styles ══ */
|
|
.ytp-wrap {
|
|
position: relative; width: 100%; background: #000;
|
|
border-radius: 12px; overflow: hidden;
|
|
aspect-ratio: 16/9; max-height: 70vh;
|
|
}
|
|
.ytp {
|
|
position: relative; width: 100%; height: 100%;
|
|
background: #000; outline: none;
|
|
user-select: none; overflow: hidden;
|
|
font-family: Roboto, Arial, sans-serif;
|
|
}
|
|
.ytp:focus { outline: none; }
|
|
.ytp-gradient-bottom {
|
|
position: absolute; bottom: 0; left: 0; right: 0;
|
|
height: 98px;
|
|
background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,.75));
|
|
pointer-events: none; transition: opacity .25s;
|
|
}
|
|
.ytp-large-play-btn {
|
|
position: absolute; inset: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
pointer-events: none; opacity: 0; transition: opacity .2s;
|
|
}
|
|
.ytp-large-play-btn i { font-size: 72px; color: rgba(255,255,255,.9); text-shadow: 0 0 30px rgba(0,0,0,.6); }
|
|
.ytp-large-play-btn.visible { opacity: 1; }
|
|
|
|
.ytp-chrome-bottom {
|
|
position: absolute; bottom: 0; left: 0; right: 0;
|
|
padding: 0 12px 8px;
|
|
transition: opacity .25s, transform .25s;
|
|
}
|
|
.ytp.controls-hidden .ytp-chrome-bottom { opacity: 0; transform: translateY(4px); pointer-events: none; }
|
|
.ytp.controls-hidden .ytp-gradient-bottom { opacity: 0; }
|
|
|
|
.ytp-progress-bar-container { padding: 4px 0; cursor: pointer; margin-bottom: 4px; }
|
|
.ytp-progress-bar {
|
|
position: relative; height: 3px;
|
|
background: rgba(255,255,255,.2); border-radius: 2px; transition: height .1s;
|
|
}
|
|
.ytp-progress-bar-container:hover .ytp-progress-bar,
|
|
.ytp-progress-bar.dragging { height: 5px; }
|
|
.ytp-play-progress {
|
|
position: absolute; top: 0; left: 0; bottom: 0;
|
|
background: #f00; border-radius: 2px; width: 0; pointer-events: none;
|
|
}
|
|
.ytp-scrubber-container {
|
|
position: absolute; top: 50%; transform: translateY(-50%);
|
|
width: 0; pointer-events: none;
|
|
}
|
|
.ytp-scrubber-button {
|
|
width: 13px; height: 13px; border-radius: 50%; background: #f00;
|
|
transform: translate(-50%, 0) scale(0); transition: transform .1s; margin-top: -4px;
|
|
}
|
|
.ytp-progress-bar-container:hover .ytp-scrubber-button,
|
|
.ytp-progress-bar.dragging .ytp-scrubber-button { transform: translate(-50%, 0) scale(1); }
|
|
.ytp-hover-time {
|
|
position: absolute; bottom: 16px;
|
|
background: rgba(28,28,28,.9); color: #fff;
|
|
font-size: 12px; padding: 3px 6px; border-radius: 4px;
|
|
pointer-events: none; opacity: 0;
|
|
transform: translateX(-50%); white-space: nowrap; transition: opacity .1s;
|
|
}
|
|
.ytp-progress-bar-container:hover .ytp-hover-time { opacity: 1; }
|
|
|
|
.ytp-chrome-controls {
|
|
display: flex; align-items: center; justify-content: space-between; height: 36px;
|
|
}
|
|
.ytp-left-controls, .ytp-right-controls { display: flex; align-items: center; gap: 4px; }
|
|
|
|
.ytp-button {
|
|
background: none; border: none; color: #fff; cursor: pointer;
|
|
padding: 0; width: 36px; height: 36px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
border-radius: 50%; transition: background .15s; flex-shrink: 0;
|
|
}
|
|
.ytp-button:hover { background: rgba(255,255,255,.1); }
|
|
.ytp-button svg { width: 22px; height: 22px; fill: #fff; pointer-events: none; }
|
|
.ytp-button:focus { outline: none; }
|
|
.ytp-play-btn svg { width: 26px; height: 26px; }
|
|
|
|
.ytp-volume-area { display: flex; align-items: center; }
|
|
.ytp-volume-slider-wrap {
|
|
overflow: hidden; width: 0; transition: width .2s;
|
|
display: flex; align-items: center;
|
|
}
|
|
.ytp-volume-area:hover .ytp-volume-slider-wrap,
|
|
.ytp-volume-area:focus-within .ytp-volume-slider-wrap { width: 60px; }
|
|
.ytp-volume-range {
|
|
-webkit-appearance: none; appearance: none;
|
|
width: 52px; height: 3px; border-radius: 2px;
|
|
background: linear-gradient(to right, #fff var(--vol,50%), rgba(255,255,255,.3) var(--vol,50%));
|
|
outline: none; cursor: pointer; margin: 0 4px;
|
|
}
|
|
.ytp-volume-range::-webkit-slider-thumb { -webkit-appearance: none; width: 13px; height: 13px; border-radius: 50%; background: #fff; cursor: pointer; }
|
|
.ytp-volume-range::-moz-range-thumb { width: 13px; height: 13px; border-radius: 50%; background: #fff; border: none; cursor: pointer; }
|
|
|
|
.ytp-time-display { font-size: 13px; color: #fff; white-space: nowrap; padding: 0 6px; line-height: 36px; }
|
|
.ytp-time-sep { opacity: .6; margin: 0 2px; }
|
|
|
|
.ytp-settings-wrap { position: relative; }
|
|
.ytp-settings-panel {
|
|
display: none; position: absolute; bottom: 44px; right: 0;
|
|
background: rgba(28,28,28,.95); border-radius: 12px;
|
|
min-width: 200px; overflow: hidden;
|
|
box-shadow: 0 4px 24px rgba(0,0,0,.6); z-index: 100;
|
|
}
|
|
.ytp-settings-panel.open { display: block; }
|
|
.ytp-settings-item {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 12px 16px; color: #fff; font-size: 13px;
|
|
cursor: pointer; transition: background .15s; white-space: nowrap;
|
|
}
|
|
.ytp-settings-item:hover { background: rgba(255,255,255,.1); }
|
|
.ytp-settings-item svg { width: 20px; height: 20px; fill: #fff; flex-shrink: 0; }
|
|
.ytp-settings-item .ytp-settings-val { margin-left: auto; color: rgba(255,255,255,.7); font-size: 12px; margin-right: 4px; }
|
|
.ytp-chevron { width: 18px; height: 18px; flex-shrink: 0; }
|
|
|
|
.ytp-speed-panel { display: none; }
|
|
.ytp-speed-panel.open { display: block; }
|
|
.ytp-speed-back {
|
|
display: flex; align-items: center; gap: 12px;
|
|
padding: 12px 16px; color: #fff; font-size: 13px; font-weight: 600;
|
|
cursor: pointer; border-bottom: 1px solid rgba(255,255,255,.15);
|
|
}
|
|
.ytp-speed-back:hover { background: rgba(255,255,255,.1); }
|
|
.ytp-speed-back svg { width: 20px; height: 20px; fill: #fff; }
|
|
.ytp-speed-option {
|
|
display: flex; align-items: center; gap: 12px;
|
|
padding: 10px 16px; color: #fff; font-size: 13px;
|
|
cursor: pointer; transition: background .15s;
|
|
}
|
|
.ytp-speed-option:hover { background: rgba(255,255,255,.1); }
|
|
.ytp-speed-check { width: 18px; height: 18px; fill: #fff; opacity: 0; flex-shrink: 0; }
|
|
.ytp-speed-option.active .ytp-speed-check { opacity: 1; }
|
|
|
|
.ytp-wrap.ytp-fullscreen {
|
|
position: fixed !important; inset: 0; z-index: 99999;
|
|
max-height: 100vh; height: 100vh; width: 100vw;
|
|
border-radius: 0; aspect-ratio: unset; margin: 0 !important;
|
|
}
|
|
|
|
.ytp-toggle-row { border-top: 1px solid rgba(255,255,255,.08); }
|
|
.ytp-toggle-row svg { opacity: .75; }
|
|
.ytp-toggle-val { font-size: 11px !important; opacity: .6; transition: color .15s, opacity .15s; }
|
|
.ytp-toggle-row.is-on .ytp-toggle-val { color: #f00; opacity: 1; }
|
|
.ytp-toggle-row.is-on svg { fill: #f00; opacity: 1; }
|
|
.ytp-toggle-switch {
|
|
width: 28px; height: 16px; background: rgba(255,255,255,.2);
|
|
border-radius: 8px; position: relative; flex-shrink: 0;
|
|
transition: background .2s; margin-left: 6px;
|
|
}
|
|
.ytp-toggle-row.is-on .ytp-toggle-switch { background: #e61e1e; }
|
|
.ytp-toggle-thumb {
|
|
position: absolute; top: 2px; left: 2px;
|
|
width: 12px; height: 12px; background: #fff;
|
|
border-radius: 50%; transition: transform .2s;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,.4);
|
|
}
|
|
.ytp-toggle-row.is-on .ytp-toggle-thumb { transform: translateX(12px); }
|
|
|
|
@media (max-width: 576px) {
|
|
.ytp-wrap { border-radius: 0; max-height: 56vw; }
|
|
.ytp-button { width: 32px; height: 32px; }
|
|
.ytp-button svg { width: 18px; height: 18px; }
|
|
.ytp-time-display { font-size: 11px; padding: 0 4px; }
|
|
}
|
|
</style>
|
|
|
|
{{-- ══ JS ══ --}}
|
|
<script>
|
|
(function () {
|
|
|
|
const wrap = document.getElementById('ytpWrap');
|
|
const player = document.getElementById('audioContainer');
|
|
const audio = document.getElementById('audioEl');
|
|
const playBtn = document.getElementById('ytpPlayBtn');
|
|
const muteBtn = document.getElementById('ytpMuteBtn');
|
|
const volRange = document.getElementById('ytpVolume');
|
|
const timeCur = document.getElementById('ytpCurrent');
|
|
const timeDur = document.getElementById('ytpDuration');
|
|
const progCont = document.getElementById('ytpProgressContainer');
|
|
const progBar = document.getElementById('ytpProgressBar');
|
|
const played = document.getElementById('ytpPlayed');
|
|
const scrubber = document.getElementById('ytpScrubber');
|
|
const hoverTime = document.getElementById('ytpHoverTime');
|
|
const largePlay = document.getElementById('ytpLargePlay');
|
|
const settingsBtn = document.getElementById('ytpSettingsBtn');
|
|
const settingsPanel = document.getElementById('ytpSettingsPanel');
|
|
const speedRow = document.getElementById('ytpSpeedRow');
|
|
const speedPanel = document.getElementById('ytpSpeedPanel');
|
|
const speedBack = document.getElementById('ytpSpeedBack');
|
|
const speedLabel = document.getElementById('ytpSpeedLabel');
|
|
const speedOpts = document.querySelectorAll('.ytp-speed-option');
|
|
const fsBtn = document.getElementById('ytpFsBtn');
|
|
const loopRow = document.getElementById('ytpLoopRow');
|
|
const loopVal = document.getElementById('ytpLoopVal');
|
|
const barsBtn = document.getElementById('ytpBarsBtn');
|
|
const animCanvas = document.getElementById('audioAnimCanvas');
|
|
|
|
const NEXT_URL = @json($nextUrl ?? null);
|
|
let hideTimer = null;
|
|
let isDragging = false;
|
|
let userSeeking = false;
|
|
let wasPlayingBeforeSeek = false;
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────
|
|
function fmt(s) {
|
|
if (!isFinite(s) || isNaN(s)) return '0:00';
|
|
s = Math.floor(s);
|
|
const m = Math.floor(s / 60), sec = s % 60;
|
|
return m + ':' + String(sec).padStart(2, '0');
|
|
}
|
|
|
|
function updatePlayIcon() {
|
|
playBtn.querySelector('.ytp-svg-play').style.display = audio.paused ? '' : 'none';
|
|
playBtn.querySelector('.ytp-svg-pause').style.display = audio.paused ? 'none' : '';
|
|
player.classList.toggle('playing', !audio.paused);
|
|
}
|
|
|
|
function updateVolumeIcon() {
|
|
const muted = audio.muted || audio.volume === 0;
|
|
muteBtn.querySelector('.ytp-svg-vol3').style.display = muted ? 'none' : '';
|
|
muteBtn.querySelector('.ytp-svg-vol0').style.display = muted ? '' : 'none';
|
|
const v = muted ? 0 : Math.round(audio.volume * 100);
|
|
volRange.value = v;
|
|
volRange.style.setProperty('--vol', v + '%');
|
|
}
|
|
|
|
function updateProgress() {
|
|
if (!audio.duration) return;
|
|
const pct = (audio.currentTime / audio.duration) * 100;
|
|
played.style.width = pct + '%';
|
|
scrubber.parentElement.style.left = pct + '%';
|
|
timeCur.textContent = fmt(audio.currentTime);
|
|
}
|
|
|
|
// ── Controls visibility ──────────────────────────────────────
|
|
function showControls() {
|
|
player.classList.remove('controls-hidden');
|
|
clearTimeout(hideTimer);
|
|
if (!audio.paused) hideTimer = setTimeout(() => player.classList.add('controls-hidden'), 3000);
|
|
}
|
|
|
|
// ── Play / Pause ─────────────────────────────────────────────
|
|
function togglePlay() {
|
|
if (audio.paused) { audio.play(); } else { audio.pause(); }
|
|
}
|
|
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);
|
|
|
|
// ── Volume ───────────────────────────────────────────────────
|
|
muteBtn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
audio.muted = !audio.muted;
|
|
if (!audio.muted && audio.volume === 0) audio.volume = 0.5;
|
|
updateVolumeIcon();
|
|
localStorage.setItem('ytpMuted', audio.muted ? '1' : '0');
|
|
});
|
|
volRange.addEventListener('input', e => {
|
|
e.stopPropagation();
|
|
const v = parseInt(e.target.value) / 100;
|
|
audio.volume = v;
|
|
audio.muted = v === 0;
|
|
updateVolumeIcon();
|
|
localStorage.setItem('ytpVolume', e.target.value);
|
|
});
|
|
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();
|
|
}
|
|
function progressPct(clientX) {
|
|
const rect = progBar.getBoundingClientRect();
|
|
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
}
|
|
|
|
progCont.addEventListener('mousemove', e => {
|
|
hoverTime.textContent = fmt(progressPct(e.clientX) * (audio.duration || 0));
|
|
hoverTime.style.left = (progressPct(e.clientX) * 100) + '%';
|
|
if (isDragging) seekTo(progressPct(e.clientX));
|
|
});
|
|
progCont.addEventListener('mousedown', e => {
|
|
e.preventDefault(); isDragging = true;
|
|
progBar.classList.add('dragging'); seekTo(progressPct(e.clientX));
|
|
});
|
|
document.addEventListener('mousemove', e => { if (isDragging) seekTo(progressPct(e.clientX)); });
|
|
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', e => { e.preventDefault(); isDragging = false; progBar.classList.remove('dragging'); });
|
|
|
|
// ── Settings / Speed ─────────────────────────────────────────
|
|
settingsBtn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
const open = settingsPanel.classList.toggle('open');
|
|
if (!open) { speedPanel.classList.remove('open'); speedRow.style.display = ''; }
|
|
clearTimeout(hideTimer);
|
|
});
|
|
document.addEventListener('click', e => {
|
|
if (!document.getElementById('ytpSettingsWrap').contains(e.target)) {
|
|
settingsPanel.classList.remove('open');
|
|
speedPanel.classList.remove('open');
|
|
speedRow.style.display = '';
|
|
}
|
|
});
|
|
speedRow.addEventListener('click', e => { e.stopPropagation(); speedRow.style.display = 'none'; speedPanel.classList.add('open'); });
|
|
speedBack.addEventListener('click', e => { e.stopPropagation(); speedPanel.classList.remove('open'); speedRow.style.display = ''; });
|
|
speedOpts.forEach(opt => {
|
|
opt.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
const s = parseFloat(opt.dataset.speed);
|
|
audio.playbackRate = s;
|
|
speedOpts.forEach(o => o.classList.remove('active'));
|
|
opt.classList.add('active');
|
|
speedLabel.textContent = s === 1 ? 'Normal' : s;
|
|
settingsPanel.classList.remove('open');
|
|
speedPanel.classList.remove('open');
|
|
speedRow.style.display = '';
|
|
});
|
|
});
|
|
|
|
// ── Fullscreen ───────────────────────────────────────────────
|
|
fsBtn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
document.fullscreenElement ? document.exitFullscreen() : wrap.requestFullscreen();
|
|
showControls();
|
|
});
|
|
document.addEventListener('fullscreenchange', () => {
|
|
const fs = !!document.fullscreenElement;
|
|
wrap.classList.toggle('ytp-fullscreen', fs);
|
|
fsBtn.querySelector('.ytp-svg-fs-enter').style.display = fs ? 'none' : '';
|
|
fsBtn.querySelector('.ytp-svg-fs-exit').style.display = fs ? '' : 'none';
|
|
});
|
|
|
|
// ── Keyboard shortcuts ────────────────────────────────────────
|
|
document.addEventListener('keydown', e => {
|
|
const tag = document.activeElement.tagName;
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || document.activeElement.isContentEditable) return;
|
|
switch (e.key) {
|
|
case ' ': case 'k': case 'K': e.preventDefault(); togglePlay(); showControls(); break;
|
|
case 'm': case 'M':
|
|
e.preventDefault();
|
|
audio.muted = !audio.muted;
|
|
if (!audio.muted && audio.volume === 0) audio.volume = 0.5;
|
|
updateVolumeIcon(); showControls(); break;
|
|
case 'f': case 'F': e.preventDefault(); fsBtn.click(); break;
|
|
case 'ArrowLeft': e.preventDefault(); if (audio.duration) audio.currentTime = Math.max(0, audio.currentTime - 5); showControls(); break;
|
|
case 'ArrowRight': e.preventDefault(); if (audio.duration) audio.currentTime = Math.min(audio.duration, audio.currentTime + 5); showControls(); break;
|
|
case 'ArrowUp': e.preventDefault(); audio.volume = Math.min(1, audio.volume + 0.05); audio.muted = false; updateVolumeIcon(); showControls(); break;
|
|
case 'ArrowDown': e.preventDefault(); audio.volume = Math.max(0, audio.volume - 0.05); updateVolumeIcon(); showControls(); break;
|
|
default:
|
|
if (e.key >= '0' && e.key <= '9') {
|
|
e.preventDefault();
|
|
if (audio.duration) audio.currentTime = (parseInt(e.key) / 10) * audio.duration;
|
|
showControls();
|
|
}
|
|
}
|
|
});
|
|
|
|
// ── Audio events ─────────────────────────────────────────────
|
|
audio.addEventListener('play', () => { updatePlayIcon(); startBars(); showControls(); largePlay.classList.remove('visible'); requestWakeLock(); });
|
|
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 (window._plOnTrackEnd) { window._plOnTrackEnd(); } else if (NEXT_URL) { window.location.href = NEXT_URL; } });
|
|
audio.addEventListener('volumechange', updateVolumeIcon);
|
|
|
|
// ── Loop ─────────────────────────────────────────────────────
|
|
let isLooping = localStorage.getItem('ytpLoop') === '1';
|
|
audio.loop = isLooping;
|
|
function applyLoopState() {
|
|
if (!loopRow) return;
|
|
loopRow.classList.toggle('is-on', isLooping);
|
|
if (loopVal) loopVal.textContent = isLooping ? 'On' : 'Off';
|
|
}
|
|
applyLoopState();
|
|
if (loopRow) {
|
|
loopRow.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
isLooping = !isLooping;
|
|
audio.loop = isLooping;
|
|
localStorage.setItem('ytpLoop', isLooping ? '1' : '0');
|
|
applyLoopState();
|
|
});
|
|
}
|
|
|
|
// ── Wake Lock ─────────────────────────────────────────────────
|
|
let wakeLock = null;
|
|
async function requestWakeLock() {
|
|
if (!('wakeLock' in navigator) || wakeLock) return;
|
|
try { wakeLock = await navigator.wakeLock.request('screen'); } catch(e) {}
|
|
}
|
|
function releaseWakeLock() {
|
|
if (wakeLock) { wakeLock.release(); wakeLock = null; }
|
|
}
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible' && !audio.paused) requestWakeLock();
|
|
});
|
|
|
|
// ── Bars visualiser ───────────────────────────────────────────
|
|
let audioCtx = null, analyser = null, dataArray = null, rafId = null;
|
|
let barsOn = localStorage.getItem('audioBarsOn') === '1';
|
|
|
|
// Extract dominant colors from cover art for bar gradients
|
|
let imgColors = ['rgb(255,255,255)', 'rgb(200,200,200)', 'rgb(170,170,170)'];
|
|
|
|
function colorWithAlpha(col, a) {
|
|
return col.replace('rgb(', 'rgba(').replace(')', ',' + a + ')');
|
|
}
|
|
|
|
function extractColors(img) {
|
|
try {
|
|
const cv = document.createElement('canvas');
|
|
cv.width = cv.height = 24;
|
|
const cx = cv.getContext('2d');
|
|
cx.drawImage(img, 0, 0, 24, 24);
|
|
const px = cx.getImageData(0, 0, 24, 24).data;
|
|
const buckets = {};
|
|
for (let i = 0; i < px.length; i += 4) {
|
|
const r = px[i], g = px[i+1], b = px[i+2];
|
|
const bright = (r + g + b) / 3;
|
|
if (bright < 25 || bright > 230) continue;
|
|
const max = Math.max(r,g,b), min = Math.min(r,g,b);
|
|
if (max === 0 || (max - min) / max < 0.25) continue;
|
|
const key = `${r>>2},${g>>2},${b>>2}`;
|
|
buckets[key] = (buckets[key] || { r:0, g:0, b:0, n:0 });
|
|
buckets[key].r += r; buckets[key].g += g; buckets[key].b += b; buckets[key].n++;
|
|
}
|
|
const sorted = Object.values(buckets).sort((a,b) => b.n - a.n);
|
|
if (!sorted.length) return;
|
|
const chosen = [sorted[0]];
|
|
for (let i = 1; i < sorted.length && chosen.length < 3; i++) {
|
|
const c = sorted[i];
|
|
const far = chosen.every(e => {
|
|
const dr = e.r/e.n - c.r/c.n, dg = e.g/e.n - c.g/c.n, db = e.b/e.n - c.b/c.n;
|
|
return Math.sqrt(dr*dr + dg*dg + db*db) > 60;
|
|
});
|
|
if (far) chosen.push(c);
|
|
}
|
|
imgColors = chosen.map(c => `rgb(${Math.round(c.r/c.n)},${Math.round(c.g/c.n)},${Math.round(c.b/c.n)})`);
|
|
while (imgColors.length < 3) imgColors.push(imgColors[0]);
|
|
} catch(e) {}
|
|
}
|
|
|
|
const coverImg = document.getElementById('audioCoverImg');
|
|
if (coverImg) {
|
|
if (coverImg.complete && coverImg.naturalWidth) {
|
|
extractColors(coverImg);
|
|
if (coverImg.naturalHeight > coverImg.naturalWidth) coverImg.style.objectFit = 'contain';
|
|
} else {
|
|
coverImg.addEventListener('load', () => {
|
|
extractColors(coverImg);
|
|
if (coverImg.naturalHeight > coverImg.naturalWidth) coverImg.style.objectFit = 'contain';
|
|
}, { once: true });
|
|
}
|
|
}
|
|
|
|
function applyBarsState() {
|
|
barsBtn.classList.toggle('bars-on', barsOn);
|
|
barsBtn.title = barsOn ? 'Visualiser: On' : 'Visualiser: Off';
|
|
animCanvas.style.display = barsOn ? 'block' : 'none';
|
|
}
|
|
applyBarsState();
|
|
|
|
barsBtn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
barsOn = !barsOn;
|
|
localStorage.setItem('audioBarsOn', barsOn ? '1' : '0');
|
|
applyBarsState();
|
|
if (barsOn && !audio.paused) startBars(); else stopBars();
|
|
showControls();
|
|
});
|
|
|
|
function initAnalyser() {
|
|
if (audioCtx) { audioCtx.resume(); return; }
|
|
try {
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
const src = audioCtx.createMediaElementSource(audio);
|
|
analyser = audioCtx.createAnalyser();
|
|
analyser.fftSize = 256;
|
|
analyser.smoothingTimeConstant = 0.75;
|
|
src.connect(analyser);
|
|
analyser.connect(audioCtx.destination);
|
|
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
} catch (e) { console.warn('Web Audio unavailable:', e); }
|
|
}
|
|
|
|
function drawBars() {
|
|
if (!analyser) return;
|
|
analyser.getByteFrequencyData(dataArray);
|
|
const ctx = animCanvas.getContext('2d');
|
|
const w = animCanvas.offsetWidth;
|
|
const h = animCanvas.offsetHeight;
|
|
if (animCanvas.width !== w) animCanvas.width = w;
|
|
if (animCanvas.height !== h) animCanvas.height = h;
|
|
ctx.clearRect(0, 0, w, h);
|
|
const bins = 48;
|
|
const barW = (w / bins) * 0.7;
|
|
const gap = (w / bins) * 0.3;
|
|
const maxH = h * 0.25;
|
|
for (let i = 0; i < bins; i++) {
|
|
const val = dataArray[i + 2] / 255;
|
|
const barH = Math.max(3, val * maxH);
|
|
const x = i * (barW + gap) + gap / 2;
|
|
const y = h - barH;
|
|
const col = imgColors[Math.floor((i / bins) * imgColors.length)];
|
|
const grad = ctx.createLinearGradient(0, y, 0, h);
|
|
grad.addColorStop(0, colorWithAlpha(col, (0.5 + val * 0.5).toFixed(2)));
|
|
grad.addColorStop(1, colorWithAlpha(col, 0.12));
|
|
ctx.fillStyle = grad;
|
|
ctx.beginPath();
|
|
ctx.roundRect(x, y, barW, barH, [3, 3, 0, 0]);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
function barsLoop() {
|
|
rafId = requestAnimationFrame(barsLoop);
|
|
drawBars();
|
|
}
|
|
|
|
function startBars() {
|
|
if (!barsOn) return;
|
|
initAnalyser();
|
|
if (!rafId) rafId = requestAnimationFrame(barsLoop);
|
|
}
|
|
function stopBars() {
|
|
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
|
|
if (animCanvas.getContext) animCanvas.getContext('2d').clearRect(0, 0, animCanvas.width, animCanvas.height);
|
|
}
|
|
|
|
// ── Crossfade slideshow ───────────────────────────────────────
|
|
// Variables hoisted outside the if-block so the SPA update hook can access them
|
|
const SLIDE_URLS = @json($slideUrls);
|
|
const slideA = document.getElementById('slideA');
|
|
const slideB = document.getElementById('slideB');
|
|
let currentSlide = 0;
|
|
let aIsTop = true;
|
|
let slideshowTimer = null;
|
|
const slideOrientations = new Array(Math.max(SLIDE_URLS.length, 1)).fill(false);
|
|
|
|
function applyOrientation(el, idx) {
|
|
if (!el) return;
|
|
el.classList.toggle('portrait', !!slideOrientations[idx]);
|
|
}
|
|
function getSlideInterval() {
|
|
return audio.duration ? (audio.duration / SLIDE_URLS.length) * 1000 : 8000;
|
|
}
|
|
function advanceSlide() {
|
|
if (!slideA || !slideB || SLIDE_URLS.length <= 1) return;
|
|
currentSlide = (currentSlide + 1) % SLIDE_URLS.length;
|
|
const next = SLIDE_URLS[currentSlide];
|
|
if (aIsTop) {
|
|
slideB.src = next; applyOrientation(slideB, currentSlide);
|
|
slideB.style.zIndex = '2'; slideB.style.opacity = '1';
|
|
slideA.style.opacity = '0'; slideA.style.zIndex = '1';
|
|
} else {
|
|
slideA.src = next; applyOrientation(slideA, currentSlide);
|
|
slideA.style.zIndex = '2'; slideA.style.opacity = '1';
|
|
slideB.style.opacity = '0'; slideB.style.zIndex = '1';
|
|
}
|
|
aIsTop = !aIsTop;
|
|
}
|
|
function startSlideshow() {
|
|
if (slideshowTimer || SLIDE_URLS.length <= 1) return;
|
|
slideshowTimer = setInterval(advanceSlide, getSlideInterval());
|
|
}
|
|
function stopSlideshow() {
|
|
clearInterval(slideshowTimer);
|
|
slideshowTimer = null;
|
|
}
|
|
|
|
// Always attach listeners; functions guard themselves with SLIDE_URLS.length check
|
|
audio.addEventListener('play', startSlideshow);
|
|
audio.addEventListener('pause', stopSlideshow);
|
|
audio.addEventListener('ended', stopSlideshow);
|
|
|
|
audio.addEventListener('seeked', () => {
|
|
if (SLIDE_URLS.length <= 1 || !slideA || !slideB || !audio.duration) return;
|
|
stopSlideshow();
|
|
const idx = Math.min(Math.floor((audio.currentTime / audio.duration) * SLIDE_URLS.length), SLIDE_URLS.length - 1);
|
|
currentSlide = idx;
|
|
slideA.style.transition = 'none'; slideB.style.transition = 'none';
|
|
slideA.src = SLIDE_URLS[idx]; applyOrientation(slideA, idx);
|
|
slideA.style.opacity = '1'; slideA.style.zIndex = '2';
|
|
slideB.style.opacity = '0'; slideB.style.zIndex = '1';
|
|
const nextIdx = (idx + 1) % SLIDE_URLS.length;
|
|
slideB.src = SLIDE_URLS[nextIdx]; applyOrientation(slideB, nextIdx);
|
|
aIsTop = true;
|
|
requestAnimationFrame(() => { slideA.style.transition = ''; slideB.style.transition = ''; });
|
|
if (!audio.paused) startSlideshow();
|
|
});
|
|
|
|
// Preload orientations
|
|
SLIDE_URLS.forEach((url, idx) => {
|
|
const img = new Image();
|
|
img.onload = () => { slideOrientations[idx] = img.naturalHeight > img.naturalWidth; };
|
|
img.src = url;
|
|
});
|
|
|
|
if (SLIDE_URLS.length > 1 && slideA) {
|
|
function initSlideA() {
|
|
extractColors(slideA);
|
|
const portrait = slideA.naturalHeight > slideA.naturalWidth;
|
|
slideA.classList.toggle('portrait', portrait);
|
|
slideOrientations[0] = portrait;
|
|
}
|
|
if (slideA.complete && slideA.naturalWidth) initSlideA();
|
|
else slideA.addEventListener('load', initSlideA, { once: true });
|
|
}
|
|
|
|
// ── SPA update hook — called by recTransitionTo / plTransitionTo ──────────
|
|
window._audioPlayerUpdate = function(d) {
|
|
var newSlides = (d.slides && d.slides.length > 1) ? d.slides : [];
|
|
var coverEl = document.getElementById('audioCoverImg');
|
|
var slideshowEl = document.getElementById('slideshowWrap');
|
|
|
|
stopSlideshow();
|
|
SLIDE_URLS.length = 0;
|
|
|
|
if (newSlides.length > 1) {
|
|
newSlides.forEach(function(s) { SLIDE_URLS.push(s); });
|
|
if (coverEl) coverEl.style.display = 'none';
|
|
if (slideshowEl) slideshowEl.style.display = '';
|
|
currentSlide = 0; aIsTop = true;
|
|
if (slideA) { slideA.style.transition='none'; slideA.src=newSlides[0]; slideA.style.opacity='1'; slideA.style.zIndex='2'; }
|
|
if (slideB) { slideB.style.transition='none'; slideB.src=newSlides[1]||newSlides[0]; slideB.style.opacity='0'; slideB.style.zIndex='1'; }
|
|
requestAnimationFrame(function() { if(slideA) slideA.style.transition=''; if(slideB) slideB.style.transition=''; });
|
|
} else {
|
|
if (slideshowEl) slideshowEl.style.display = 'none';
|
|
if (coverEl) { coverEl.style.display = ''; coverEl.src = d.cover_url || ''; }
|
|
}
|
|
|
|
// Update page title
|
|
var titleEl = document.querySelector('.video-title span');
|
|
if (titleEl) titleEl.textContent = d.title || '';
|
|
document.title = (d.title || '') + ' | {{ config("app.name") }}';
|
|
|
|
// Reset progress bar
|
|
if (played) played.style.width = '0%';
|
|
if (scrubber) scrubber.parentElement.style.left = '0%';
|
|
if (timeCur) timeCur.textContent = '0:00';
|
|
if (timeDur && d.duration) timeDur.textContent = fmt(d.duration);
|
|
};
|
|
|
|
// ── Init ─────────────────────────────────────────────────────
|
|
const savedVol = localStorage.getItem('ytpVolume');
|
|
const savedMuted = localStorage.getItem('ytpMuted');
|
|
audio.volume = savedVol ? parseInt(savedVol) / 100 : 0.8;
|
|
audio.muted = true; // start muted so autoplay always works
|
|
updateVolumeIcon();
|
|
largePlay.classList.add('visible');
|
|
showControls();
|
|
|
|
// Once playing, restore user's mute preference
|
|
audio.addEventListener('playing', function restoreSound() {
|
|
audio.removeEventListener('playing', restoreSound);
|
|
if (savedMuted !== '1') {
|
|
audio.muted = false;
|
|
updateVolumeIcon();
|
|
}
|
|
}, { once: true });
|
|
|
|
audio.addEventListener('loadedmetadata', () => {
|
|
timeDur.textContent = fmt(audio.duration);
|
|
const p = audio.play();
|
|
if (p) p.catch(() => {
|
|
audio.muted = true;
|
|
audio.play().catch(() => {});
|
|
});
|
|
});
|
|
|
|
})();
|
|
</script>
|