Add sports-match type, device tracking, profile visits, and share refactor
- New SportsMatch model/controller and sports UI components/modal - Move share-modal to a reusable x-share-modal/x-share-button component - Add VideoSharedWithUser notification and share-to-members flow - Device/user-agent tracking on views, downloads, share accesses - ProfileVisit model + migration; subscription source tracking - Email thumbnail support; remove stale TODO files
This commit is contained in:
1 parent
6aae6f86b6
commit
73527f3781
61 files changed
+4014
-636
No files matched your search
@@ -243,7 +243,7 @@
|
||||
|
||||
@endif
|
||||
|
||||
@include('layouts.partials.share-modal')
|
||||
<x-share-modal />
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
|
||||
@@ -77,14 +77,11 @@
|
||||
<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 }}'">
|
||||
@if(isset($playlist) && $playlist)
|
||||
<button class="ytp-button ytp-prev-btn" title="Previous" onclick="if(window.plPrev)window.plPrev();">
|
||||
<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 }}'">
|
||||
<button class="ytp-button ytp-next-btn" title="Next" onclick="if(window.plNext)window.plNext();">
|
||||
<svg viewBox="0 0 24 24"><path d="m6 18 8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
||||
</button>
|
||||
@endif
|
||||
@@ -224,12 +221,16 @@
|
||||
|
||||
/* ══ Full ytp styles ══ */
|
||||
.ytp-wrap {
|
||||
position: relative; width: 100%; background: #000;
|
||||
position: relative; width: 100% !important; background: #000;
|
||||
border-radius: 12px; overflow: hidden;
|
||||
aspect-ratio: 16/9;
|
||||
aspect-ratio: 16/9 !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
.ytp {
|
||||
.ytp, .audio-ytp {
|
||||
position: relative; width: 100%; height: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000; outline: none;
|
||||
user-select: none; overflow: hidden;
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
@@ -1063,6 +1064,9 @@ window._audioPlayerUpdate = function(d) {
|
||||
var langPopup = document.getElementById('ytpLangPopup');
|
||||
if (!langWrap || !langPopup || !langBtn) return;
|
||||
|
||||
// New song loaded → back to its primary track, so shares don't carry a stale track id.
|
||||
window._ytpTrackId = 0;
|
||||
|
||||
function flagHtml(flag, size) {
|
||||
var s = size === 'sm' ? 'width:22px;height:16px;border-radius:2px;display:inline-block;flex-shrink:0;'
|
||||
: 'width:22px;height:16px;border-radius:2px;display:inline-block;';
|
||||
@@ -1143,6 +1147,8 @@ window._audioPlayerUpdate = function(d) {
|
||||
var url = opt.dataset.langUrl;
|
||||
var flag = opt.dataset.langFlag;
|
||||
var curTime = audio.currentTime;
|
||||
// Track the selected version so "Share" links to this exact language (0 = primary).
|
||||
window._ytpTrackId = parseInt(opt.dataset.langId, 10) || 0;
|
||||
langPopup.querySelectorAll('.ytp-lang-option').forEach(function(o) { o.classList.remove('active'); });
|
||||
opt.classList.add('active');
|
||||
langPopup.classList.remove('open');
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
.vdb-desc-text a.action-btn { display:inline-flex; margin:4px 6px 4px 0; color:inherit; text-decoration:none; vertical-align:middle; }
|
||||
.vdb-desc-text.vdb-clamp { max-height:130px; overflow:hidden; -webkit-mask-image:linear-gradient(180deg,#000 70%,transparent); mask-image:linear-gradient(180deg,#000 70%,transparent); }
|
||||
.vdb-desc-text.vdb-clamp.vdb-expanded { max-height:none; -webkit-mask-image:none; mask-image:none; }
|
||||
.vdb-show-more { background:none; border:none; color:var(--text-primary); font-weight:700; font-size:13px; cursor:pointer; padding:6px 0 0; }
|
||||
.vdb-show-more { display:flex; align-items:center; justify-content:center; gap:6px; margin:12px auto 0; background:var(--bg-secondary); border:1px solid var(--border-color); color:var(--text-primary); font-weight:700; font-size:13px; cursor:pointer; padding:7px 16px; border-radius:18px; transition:background .15s ease, border-color .15s ease; }
|
||||
.vdb-show-more:hover { background:var(--bg-hover, rgba(127,127,127,.12)); }
|
||||
.vdb-show-more i { font-size:14px; transition:transform .2s ease; }
|
||||
.vdb-show-more.expanded i { transform:rotate(180deg); }
|
||||
</style>
|
||||
|
||||
<div class="vdb-wrap" id="vdbWrap">
|
||||
@@ -58,7 +61,7 @@
|
||||
{!! $descriptionSlot !!}
|
||||
@elseif($renderedDescription !== '')
|
||||
<div id="vdbDescShort" class="vdb-desc-text vdb-clamp">{!! $renderedDescription !!}</div>
|
||||
<button id="vdbShowMore" class="vdb-show-more" style="display:none;" onclick="toggleVdbDesc(this)">Show more</button>
|
||||
<button id="vdbShowMore" class="vdb-show-more" style="display:none;" onclick="toggleVdbDesc(this)"><span>Show more</span> <i class="bi bi-chevron-down"></i></button>
|
||||
@else
|
||||
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No description added.</p>
|
||||
@endif
|
||||
@@ -70,8 +73,24 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Remember which tab the user opened so SPA navigation between videos keeps it active.
|
||||
window._vdbActiveTab = window._vdbActiveTab || 'vdb-about';
|
||||
|
||||
// Scroll back up to the video player on every SPA video-to-video swap.
|
||||
// On mobile the window is locked (see CLAUDE.md mobile scroll model) and `.yt-main`
|
||||
// (id="main") is the real scroll container, so we scroll that instead.
|
||||
window._spaScrollToVideo = window._spaScrollToVideo || function () {
|
||||
const main = document.getElementById('main');
|
||||
if (window.innerWidth <= 768 && main) {
|
||||
main.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
// ── Tab switching ──────────────────────────────────────
|
||||
function switchVdbTab(panelId, btn) {
|
||||
window._vdbActiveTab = panelId;
|
||||
document.querySelectorAll('.vdb-tab').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.vdb-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
@@ -82,11 +101,44 @@ function switchVdbTab(panelId, btn) {
|
||||
if (currentUrl && currentUrl !== window._insLoadedUrl) loadInsights();
|
||||
}
|
||||
}
|
||||
|
||||
// Re-apply the remembered tab after an SPA swap replaces #vdbWrap's contents.
|
||||
// If the remembered tab doesn't exist on the new video (e.g. Insights when the
|
||||
// viewer isn't the owner), fall back to About so nothing is left blank.
|
||||
function _vdbApplyActiveTab() {
|
||||
const wrap = document.getElementById('vdbWrap');
|
||||
if (!wrap) return;
|
||||
let target = window._vdbActiveTab || 'vdb-about';
|
||||
let btn = wrap.querySelector('.vdb-tab[data-panel="' + target + '"]');
|
||||
if (!btn) { target = 'vdb-about'; btn = wrap.querySelector('.vdb-tab[data-panel="vdb-about"]'); }
|
||||
if (!btn) return;
|
||||
wrap.querySelectorAll('.vdb-tab').forEach(b => b.classList.remove('active'));
|
||||
wrap.querySelectorAll('.vdb-panel').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const panel = document.getElementById(target);
|
||||
if (panel) panel.classList.add('active');
|
||||
if (target === 'vdb-insights') {
|
||||
const ip = document.getElementById('vdb-insights');
|
||||
const currentUrl = ip && ip.dataset.insightsBase;
|
||||
if (currentUrl && currentUrl !== window._insLoadedUrl && typeof loadInsights === 'function') loadInsights();
|
||||
}
|
||||
}
|
||||
// Observe #vdbWrap so the active tab is re-applied whenever the SPA layer
|
||||
// rewrites its innerHTML during a video-to-video transition.
|
||||
(function _vdbWatchSwaps() {
|
||||
const wrap = document.getElementById('vdbWrap');
|
||||
if (!wrap || wrap._vdbTabObserver) return;
|
||||
const obs = new MutationObserver(() => _vdbApplyActiveTab());
|
||||
obs.observe(wrap, { childList: true, subtree: false });
|
||||
wrap._vdbTabObserver = obs;
|
||||
})();
|
||||
function toggleVdbDesc(btn) {
|
||||
const d = document.getElementById('vdbDescShort');
|
||||
if (!d) return;
|
||||
const expanded = d.classList.toggle('vdb-expanded');
|
||||
btn.textContent = expanded ? 'Show less' : 'Show more';
|
||||
btn.classList.toggle('expanded', expanded);
|
||||
const label = btn.querySelector('span');
|
||||
if (label) label.textContent = expanded ? 'Show less' : 'Show more';
|
||||
}
|
||||
// Reveal "Show more" only when the description overflows the clamp. Compare the
|
||||
// natural content height to the clamp limit (130px) rather than clientHeight,
|
||||
@@ -94,8 +146,8 @@ function toggleVdbDesc(btn) {
|
||||
function _vdbCheckOverflow() {
|
||||
const d = document.getElementById('vdbDescShort'), b = document.getElementById('vdbShowMore');
|
||||
if (!d || !b) return;
|
||||
if (d.classList.contains('vdb-expanded')) { b.style.display = 'block'; return; }
|
||||
b.style.display = (d.scrollHeight > 138) ? 'block' : 'none';
|
||||
if (d.classList.contains('vdb-expanded')) { b.style.display = 'flex'; return; }
|
||||
b.style.display = (d.scrollHeight > 138) ? 'flex' : 'none';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', _vdbCheckOverflow);
|
||||
window.addEventListener('load', _vdbCheckOverflow);
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 6px 0; flex-wrap: wrap; gap: 12px;">
|
||||
{{-- Left: Channel Info --}}
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<a href="{{ $video->user ? route('channel', $video->user->channel) : '#' }}" class="channel-info text-decoration-none" style="color: inherit; display: flex; align-items: center; gap: 12px;">
|
||||
<a href="{{ $video->user ? route('channel', $video->user->channel) : '#' }}" class="channel-info text-decoration-none"
|
||||
@if($video->user) data-profile-visit-url="{{ route('profile.visit', $video->user) }}" data-source-video-id="{{ $video->id }}" @endif
|
||||
style="color: inherit; display: flex; align-items: center; gap: 12px;">
|
||||
@if($video->user)
|
||||
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" style="width: 36px; height: 36px;" alt="{{ $video->user->name }}">
|
||||
@else
|
||||
@@ -69,9 +71,9 @@
|
||||
|
||||
{{-- Share Button --}}
|
||||
@if($video->isShareable())
|
||||
<button class="action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
|
||||
<x-share-button :video="$video" class="action-btn">
|
||||
<i class="bi bi-share"></i> <span>Share</span>
|
||||
</button>
|
||||
</x-share-button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -648,7 +648,10 @@
|
||||
.vdb-desc-text { font-size: 14px; line-height: 1.6; color: var(--text-primary); white-space: pre-wrap; }
|
||||
.vdb-desc-text p { margin-bottom: 8px; }
|
||||
.vdb-desc-text a { color: #3ea6ff; }
|
||||
.vdb-show-more { background: none; border: none; color: var(--text-primary); font-weight: 700; font-size: 13px; cursor: pointer; padding: 6px 0 0; }
|
||||
.vdb-show-more { display: flex; align-items: center; justify-content: center; gap: 6px; margin: 12px auto 0; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); font-weight: 700; font-size: 13px; cursor: pointer; padding: 7px 16px; border-radius: 18px; transition: background .15s ease, border-color .15s ease; }
|
||||
.vdb-show-more:hover { background: var(--bg-hover, rgba(127,127,127,.12)); }
|
||||
.vdb-show-more i { font-size: 14px; transition: transform .2s ease; }
|
||||
.vdb-show-more.expanded i { transform: rotate(180deg); }
|
||||
|
||||
</style>
|
||||
|
||||
@@ -680,7 +683,7 @@
|
||||
<div id="vdbDescShort" class="vdb-desc-text">{{ $needsExpand ? $shortDescription : $fullDescription }}</div>
|
||||
@if($needsExpand)
|
||||
<div id="vdbDescFull" class="vdb-desc-text" style="display:none;">{{ $fullDescription }}</div>
|
||||
<button class="vdb-show-more" onclick="toggleVdbDesc(this)">Show more</button>
|
||||
<button class="vdb-show-more" onclick="toggleVdbDesc(this)"><span>Show more</span> <i class="bi bi-chevron-down"></i></button>
|
||||
@endif
|
||||
@else
|
||||
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No description added.</p>
|
||||
@@ -707,20 +710,24 @@
|
||||
function toggleVdbDesc(btn) {
|
||||
const short = document.getElementById('vdbDescShort');
|
||||
const full = document.getElementById('vdbDescFull');
|
||||
const label = btn.querySelector('span');
|
||||
if (full.style.display === 'none') {
|
||||
short.style.display = 'none';
|
||||
full.style.display = 'block';
|
||||
btn.textContent = 'Show less';
|
||||
label.textContent = 'Show less';
|
||||
btn.classList.add('expanded');
|
||||
} else {
|
||||
short.style.display = 'block';
|
||||
full.style.display = 'none';
|
||||
btn.textContent = 'Show more';
|
||||
label.textContent = 'Show more';
|
||||
btn.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Share + track ──────────────────────────────────────
|
||||
function videoShare(url, title, trackUrl) {
|
||||
openShareModal(url, title);
|
||||
// Pass the full argument set so email + share-to-members are available (see share component rule).
|
||||
openShareModal(url, title, '', '{{ auth()->check() ? route('videos.shareEmail', $video) : '' }}', '{{ auth()->check() ? route('videos.shareMembers', $video) : '' }}');
|
||||
fetch(trackUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '', 'Accept': 'application/json' }
|
||||
@@ -773,7 +780,8 @@
|
||||
@foreach ($playlistVideos as $index => $playlistVideo)
|
||||
@php $isCurrent = $playlistVideo->id === $video->id; @endphp
|
||||
<div class="sidebar-video-card{{ $isCurrent ? ' current-video' : '' }}"
|
||||
{{ $isCurrent ? '' : "onclick=\"window.location.href='".route('videos.show', $playlistVideo)."?playlist={$playlist->share_token}'\"" }}
|
||||
data-pl-id="{{ $playlistVideo->id }}"
|
||||
{{ $isCurrent ? '' : 'onclick="plGoTo(\''.route('videos.show', $playlistVideo).'?playlist='.$playlist->share_token.'\')"' }}
|
||||
style="{{ $isCurrent ? 'cursor:default;' : 'cursor:pointer;' }}">
|
||||
<div class="sidebar-thumb" style="position: relative;">
|
||||
@if ($playlistVideo->thumbnail)
|
||||
@@ -892,10 +900,9 @@
|
||||
@endauth
|
||||
|
||||
@if ($video->isShareable())
|
||||
<button class="yt-action-btn"
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')" style="flex:1;">
|
||||
<x-share-button :video="$video" class="yt-action-btn" style="flex:1;">
|
||||
<i class="bi bi-share"></i><span>Share</span>
|
||||
</button>
|
||||
</x-share-button>
|
||||
@endif
|
||||
|
||||
<button class="yt-action-btn" onclick="openAddToPlaylistModal({{ $video->id }})" style="flex:1;">
|
||||
@@ -921,7 +928,7 @@
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@include('layouts.partials.share-modal')
|
||||
{{-- Share modal is the app-layout singleton (<x-share-modal/>); do not re-render it here. --}}
|
||||
@include('layouts.partials.edit-video-modal')
|
||||
@include('layouts.partials.add-to-playlist-modal')
|
||||
|
||||
@@ -1000,12 +1007,12 @@
|
||||
var pos = order.indexOf(curIdx);
|
||||
var nextPos = (pos + 1) % order.length;
|
||||
if (pos === order.length - 1 && plLoop !== 'all') return; // end of shuffle, no loop
|
||||
window.location.href = PL_VIDEOS[order[nextPos]].url;
|
||||
plGoTo(PL_VIDEOS[order[nextPos]].url);
|
||||
} else {
|
||||
if (PL_NEXT_URL) {
|
||||
window.location.href = PL_NEXT_URL;
|
||||
plGoTo(PL_NEXT_URL);
|
||||
} else if (plLoop === 'all' && PL_FIRST_URL) {
|
||||
window.location.href = PL_FIRST_URL;
|
||||
plGoTo(PL_FIRST_URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1004,7 +1004,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
@include('layouts.partials.share-modal')
|
||||
<x-share-modal />
|
||||
@include('layouts.partials.edit-video-modal')
|
||||
|
||||
@if (Session::has('openEditModal') && Session::get('openEditModal'))
|
||||
|
||||
@@ -3109,7 +3109,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
@include('layouts.partials.share-modal')
|
||||
<x-share-modal />
|
||||
@include('layouts.partials.edit-video-modal')
|
||||
|
||||
<!-- Add Round Modal -->
|
||||
|
||||
@@ -957,7 +957,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
@include('layouts.partials.share-modal')
|
||||
<x-share-modal />
|
||||
@include('layouts.partials.edit-video-modal')
|
||||
|
||||
@if (Session::has('openEditModal') && Session::get('openEditModal'))
|
||||
|
||||
@@ -477,6 +477,11 @@
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<span class="pl-autoplay-label">Autoplay</span>
|
||||
</button>
|
||||
<div class="pl-ctrl-divider"></div>
|
||||
<button class="pl-ctrl-btn" id="plShareBtn" title="Share playlist"
|
||||
onclick="openShareModal('{{ route('playlists.showByToken', $playlist->share_token) }}','{{ addslashes($playlist->name) }}','{{ route('playlists.recordShare', $playlist) }}','','')">
|
||||
<i class="bi bi-share"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -516,16 +521,22 @@
|
||||
async function plTransitionTo(url, pushHist) {
|
||||
if(!url||plTransiting) return;
|
||||
plTransiting=true;
|
||||
if(window._spaScrollToVideo) window._spaScrollToVideo();
|
||||
try {
|
||||
var m=url.match(/\/videos\/([^/?#]+)/);
|
||||
if(!m){ window.location.href=url; return; }
|
||||
var qs=url.indexOf('?')!==-1?url.substring(url.indexOf('?')):'';
|
||||
var resp=await fetch('/videos/'+m[1]+'/player-data'+qs);
|
||||
var resp;
|
||||
try { resp=await fetch('/videos/'+m[1]+'/player-data'+qs); }
|
||||
catch(e){ window.location.href=url; return; }
|
||||
if(!resp.ok){ window.location.href=url; return; }
|
||||
var d=await resp.json();
|
||||
var d;
|
||||
try { d=await resp.json(); }
|
||||
catch(e){ window.location.href=url; return; }
|
||||
|
||||
// reload video source (HLS or MP4)
|
||||
if(window._ytpLoadSource) window._ytpLoadSource(d.hls_url, d.stream_url);
|
||||
try { if(window._ytpLoadSource) window._ytpLoadSource(d.hls_url, d.stream_url); }
|
||||
catch(e){ console.warn('_ytpLoadSource', e); }
|
||||
|
||||
// reset progress bar
|
||||
var pl=document.getElementById('ytpPlayed'),sc=document.getElementById('ytpScrubber');
|
||||
@@ -545,14 +556,12 @@
|
||||
if(vid) vid.loop=(plLoop==='one');
|
||||
|
||||
PL_CURRENT=d.id;
|
||||
plRender();
|
||||
plHighlight(d.id, true);
|
||||
try { plRender(); plHighlight(d.id, true); } catch(e){ console.warn('plRender', e); }
|
||||
if(pushHist!==false) history.pushState({plVideoId:d.id,url:url},'',url);
|
||||
|
||||
plSwapContent(url);
|
||||
} catch(e){
|
||||
console.warn('plTransitionTo',e);
|
||||
window.location.href=url;
|
||||
} finally {
|
||||
plTransiting=false;
|
||||
}
|
||||
@@ -783,6 +792,7 @@
|
||||
async function recTransitionTo(url, pushHist) {
|
||||
if (!url || recTransiting) return;
|
||||
recTransiting = true;
|
||||
if (window._spaScrollToVideo) window._spaScrollToVideo();
|
||||
try {
|
||||
var m = url.match(/\/videos\/([^/?#]+)/);
|
||||
if (!m) { window.location.href = url; return; }
|
||||
|
||||
@@ -2555,6 +2555,11 @@
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<span class="pl-autoplay-label">Autoplay</span>
|
||||
</button>
|
||||
<div class="pl-ctrl-divider"></div>
|
||||
<button class="pl-ctrl-btn" id="plShareBtn" title="Share playlist"
|
||||
onclick="openShareModal('{{ route('playlists.showByToken', $playlist->share_token) }}','{{ addslashes($playlist->name) }}','{{ route('playlists.recordShare', $playlist) }}','','')">
|
||||
<i class="bi bi-share"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -2594,15 +2599,21 @@
|
||||
async function plTransitionTo(url, pushHist) {
|
||||
if(!url||plTransiting) return;
|
||||
plTransiting=true;
|
||||
if(window._spaScrollToVideo) window._spaScrollToVideo();
|
||||
try {
|
||||
var m=url.match(/\/videos\/([^/?#]+)/);
|
||||
if(!m){ window.location.href=url; return; }
|
||||
var qs=url.indexOf('?')!==-1?url.substring(url.indexOf('?')):'';
|
||||
var resp=await fetch('/videos/'+m[1]+'/player-data'+qs);
|
||||
var resp;
|
||||
try { resp=await fetch('/videos/'+m[1]+'/player-data'+qs); }
|
||||
catch(e){ window.location.href=url; return; }
|
||||
if(!resp.ok){ window.location.href=url; return; }
|
||||
var d=await resp.json();
|
||||
var d;
|
||||
try { d=await resp.json(); }
|
||||
catch(e){ window.location.href=url; return; }
|
||||
|
||||
if(window._ytpLoadSource) window._ytpLoadSource(d.hls_url, d.stream_url);
|
||||
try { if(window._ytpLoadSource) window._ytpLoadSource(d.hls_url, d.stream_url); }
|
||||
catch(e){ console.warn('_ytpLoadSource', e); }
|
||||
|
||||
var pl=document.getElementById('ytpPlayed'),sc=document.getElementById('ytpScrubber');
|
||||
var cu=document.getElementById('ytpCurrent'),dr=document.getElementById('ytpDuration');
|
||||
@@ -2617,14 +2628,12 @@
|
||||
if(vid) vid.loop=(plLoop==='one');
|
||||
|
||||
PL_CURRENT=d.id;
|
||||
plRender();
|
||||
plHighlight(d.id, true);
|
||||
try { plRender(); plHighlight(d.id, true); } catch(e){ console.warn('plRender', e); }
|
||||
if(pushHist!==false) history.pushState({plVideoId:d.id,url:url},'',url);
|
||||
|
||||
plSwapContent(url);
|
||||
} catch(e){
|
||||
console.warn('plTransitionTo',e);
|
||||
window.location.href=url;
|
||||
} finally {
|
||||
plTransiting=false;
|
||||
}
|
||||
@@ -2856,6 +2865,7 @@
|
||||
async function recTransitionTo(url, pushHist) {
|
||||
if (!url || recTransiting) return;
|
||||
recTransiting = true;
|
||||
if (window._spaScrollToVideo) window._spaScrollToVideo();
|
||||
try {
|
||||
var m = url.match(/\/videos\/([^/?#]+)/);
|
||||
if (!m) { window.location.href = url; return; }
|
||||
|
||||
@@ -511,6 +511,11 @@
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<span class="pl-autoplay-label">Autoplay</span>
|
||||
</button>
|
||||
<div class="pl-ctrl-divider"></div>
|
||||
<button class="pl-ctrl-btn" id="plShareBtn" title="Share playlist"
|
||||
onclick="openShareModal('{{ route('playlists.showByToken', $playlist->share_token) }}','{{ addslashes($playlist->name) }}','{{ route('playlists.recordShare', $playlist) }}','','')">
|
||||
<i class="bi bi-share"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -554,13 +559,18 @@
|
||||
async function plTransitionTo(url, pushHist) {
|
||||
if (!url || plTransiting) return;
|
||||
plTransiting = true;
|
||||
if (window._spaScrollToVideo) window._spaScrollToVideo();
|
||||
try {
|
||||
var m = url.match(/\/videos\/([^/?#]+)/);
|
||||
if (!m) { window.location.href=url; return; }
|
||||
var qs = url.indexOf('?')!==-1 ? url.substring(url.indexOf('?')) : '';
|
||||
var resp = await fetch('/videos/'+m[1]+'/player-data'+qs);
|
||||
var resp;
|
||||
try { resp = await fetch('/videos/'+m[1]+'/player-data'+qs); }
|
||||
catch(e) { window.location.href=url; return; }
|
||||
if (!resp.ok) { window.location.href=url; return; }
|
||||
var d = await resp.json();
|
||||
var d;
|
||||
try { d = await resp.json(); }
|
||||
catch(e) { window.location.href=url; return; }
|
||||
|
||||
// swap audio src (keeps browser autoplay permission)
|
||||
var audio = document.getElementById('audioEl');
|
||||
@@ -571,13 +581,13 @@
|
||||
}
|
||||
|
||||
// update cover / slideshow / title / progress via shared hook
|
||||
if (window._audioPlayerUpdate) window._audioPlayerUpdate(d);
|
||||
try { if (window._audioPlayerUpdate) window._audioPlayerUpdate(d); }
|
||||
catch(e) { console.warn('_audioPlayerUpdate', e); }
|
||||
|
||||
// update state
|
||||
PL_CURRENT = d.id;
|
||||
if(audio&&plLoop==='one') audio.loop=true; else if(audio) audio.loop=false;
|
||||
plRender();
|
||||
plHighlight(d.id, true);
|
||||
try { plRender(); plHighlight(d.id, true); } catch(e) { console.warn('plRender', e); }
|
||||
|
||||
// history
|
||||
if(pushHist!==false) history.pushState({plVideoId:d.id,url:url},'',url);
|
||||
@@ -589,7 +599,6 @@
|
||||
plSwapContent(url);
|
||||
} catch(e) {
|
||||
console.warn('plTransitionTo',e);
|
||||
window.location.href=url;
|
||||
} finally {
|
||||
plTransiting=false;
|
||||
}
|
||||
@@ -631,8 +640,17 @@
|
||||
c.classList.toggle('current-video', parseInt(c.dataset.plId)===activeId);
|
||||
});
|
||||
if(scroll) {
|
||||
var active=document.querySelector('.sidebar-video-card.current-video');
|
||||
if(active) active.scrollIntoView({behavior:'smooth',block:'nearest'});
|
||||
// Scroll only inside the sidebar list container — never the page —
|
||||
// so focus stays on the player when the track changes.
|
||||
var active = document.querySelector('.sidebar-video-card.current-video');
|
||||
if (!active) return;
|
||||
var list = active.closest('.recommended-videos-list') || active.parentElement;
|
||||
if (list) {
|
||||
var lr = list.getBoundingClientRect();
|
||||
var ar = active.getBoundingClientRect();
|
||||
var delta = (ar.top + ar.height/2) - (lr.top + lr.height/2);
|
||||
list.scrollTo({ top: list.scrollTop + delta, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,6 +669,9 @@
|
||||
if(ab){ ab.classList.toggle('pl-ctrl-active',plAutoplay==='1'); ab.title='Autoplay: '+(plAutoplay==='1'?'On':'Off'); }
|
||||
if(nb) nb.disabled=!adj.next;
|
||||
if(pb) pb.disabled=!adj.prev;
|
||||
// In-player prev/next buttons (audio-player.blade.php)
|
||||
document.querySelectorAll('.ytp-prev-btn').forEach(function(b){ b.disabled=!adj.prev; b.style.opacity=adj.prev?'':'0.4'; });
|
||||
document.querySelectorAll('.ytp-next-btn').forEach(function(b){ b.disabled=!adj.next; b.style.opacity=adj.next?'':'0.4'; });
|
||||
}
|
||||
|
||||
// ── public API ────────────────────────────────────────────
|
||||
@@ -768,6 +789,7 @@
|
||||
async function recTransitionTo(url, pushHist) {
|
||||
if (recTransiting) return;
|
||||
recTransiting = true;
|
||||
if (window._spaScrollToVideo) window._spaScrollToVideo();
|
||||
try {
|
||||
var dataUrl = url.split('?')[0] + '/player-data';
|
||||
var resp = await fetch(dataUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
|
||||
Reference in new issue
Block a user