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
@@ -7,12 +7,8 @@
|
||||
])
|
||||
|
||||
@php
|
||||
$orientationClass = match($video->orientation ?? 'landscape') {
|
||||
'portrait' => 'portrait',
|
||||
'square' => 'square',
|
||||
'ultrawide' => 'ultrawide',
|
||||
default => '',
|
||||
};
|
||||
// Force 16:9 for every video — orientation classes intentionally disabled
|
||||
$orientationClass = '';
|
||||
$hlsUrl = $video->has_hls ? route('videos.hls', ['video' => $video, 'file' => 'master.m3u8']) : null;
|
||||
$mp4Url = route('videos.stream', $video) . '?v=' . $video->updated_at->timestamp;
|
||||
$nextUrl = $nextVideo && $playlist ? route('videos.show', $nextVideo) .'?playlist='.$playlist->share_token : null;
|
||||
@@ -35,7 +31,9 @@
|
||||
{{-- ══════════════════════════════════════════════
|
||||
PLAYER WRAP — theater class toggled by JS
|
||||
══════════════════════════════════════════════ --}}
|
||||
<div class="ytp-wrap {{ $orientationClass }}" id="ytpWrap">
|
||||
<div class="ytp-wrap {{ $orientationClass }}" id="ytpWrap"
|
||||
data-progress-url="{{ route('videos.viewProgress', $video) }}"
|
||||
data-video-id="{{ $video->id }}">
|
||||
<div class="ytp" id="videoContainer" tabindex="0">
|
||||
|
||||
{{-- ── Video element ── --}}
|
||||
@@ -1399,6 +1397,73 @@ function init() {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
// ── View-progress heartbeat ──────────────────────────────
|
||||
(function() {
|
||||
const _csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
let _hbVideoId = null;
|
||||
let _hbUrl = null;
|
||||
let _hbLast = 0;
|
||||
let _hbCompleted = false;
|
||||
|
||||
function _refreshHbTarget() {
|
||||
const wrap = document.getElementById('ytpWrap');
|
||||
if (!wrap) return;
|
||||
const vid = parseInt(wrap.dataset.videoId || '0', 10);
|
||||
if (vid && vid !== _hbVideoId) {
|
||||
_hbVideoId = vid;
|
||||
_hbUrl = wrap.dataset.progressUrl || null;
|
||||
_hbLast = 0;
|
||||
_hbCompleted = false;
|
||||
}
|
||||
}
|
||||
function _sendHb(completed) {
|
||||
if (!_hbUrl) return;
|
||||
const v = document.getElementById('videoPlayer');
|
||||
if (!v) return;
|
||||
const cur = Math.floor(v.currentTime || 0);
|
||||
if (!completed && cur <= _hbLast) return;
|
||||
const body = new URLSearchParams({ watched_seconds: cur, completed: completed ? '1' : '0' });
|
||||
try {
|
||||
if (completed && navigator.sendBeacon) {
|
||||
const blob = new Blob([body.toString() + '&_token=' + _csrf], { type: 'application/x-www-form-urlencoded' });
|
||||
navigator.sendBeacon(_hbUrl, blob);
|
||||
} else {
|
||||
fetch(_hbUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': _csrf, 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },
|
||||
body: body.toString(),
|
||||
keepalive: true,
|
||||
credentials: 'same-origin',
|
||||
}).catch(function() {});
|
||||
}
|
||||
_hbLast = cur;
|
||||
if (completed) _hbCompleted = true;
|
||||
} catch (e) {}
|
||||
}
|
||||
function _hbBind() {
|
||||
const v = document.getElementById('videoPlayer');
|
||||
if (!v || v._hbBound) return;
|
||||
v._hbBound = true;
|
||||
_refreshHbTarget();
|
||||
v.addEventListener('ended', () => _sendHb(true));
|
||||
v.addEventListener('pause', () => _sendHb(false));
|
||||
}
|
||||
setInterval(function() { _refreshHbTarget(); if (!_hbCompleted) _sendHb(false); }, 5000);
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.visibilityState === 'hidden') _sendHb(false);
|
||||
});
|
||||
window.addEventListener('pagehide', function() { _sendHb(false); });
|
||||
document.addEventListener('DOMContentLoaded', _hbBind);
|
||||
// also rebind after SPA source swaps
|
||||
const _origLoad = window._ytpLoadSource;
|
||||
if (typeof _origLoad === 'function') {
|
||||
window._ytpLoadSource = function() {
|
||||
_refreshHbTarget();
|
||||
return _origLoad.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
})();
|
||||
</script>
|
||||
@endonce
|
||||
Reference in new issue
Block a user