feat(shorts): /shorts/{id} deep-link + URL sync + persistent mute + wheel nav
- New route /shorts/{video} opens the feed scrolled to that short.
Controller aborts 404 when the id doesn't resolve to a short or
isn't viewable by the current viewer, so bad links land on the
branded 404 (added in the next commit) rather than loading an
empty feed.
- The IntersectionObserver that detects the active short now
history.replaceState()s the URL to /shorts/{id} so copy-paste of
the address bar works, without pushing a navigation entry — the
back button still exits the feed in one step.
- Mute state is persisted in localStorage (shortsMuted = '0' | '1')
so unmuting once sticks across sessions. Default remains muted
to satisfy autoplay policies on first load.
- Mouse-wheel navigation moves one short per gesture with a 450 ms
debounce. Trackpad flicks no longer skip five shorts at once.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
646d3e9d2a
commit
9e613c05d2
3 files changed
+67
-6
No files matched your search
@@ -432,9 +432,22 @@
|
||||
<script>
|
||||
(function () {
|
||||
const main = document.getElementById('main');
|
||||
let muted = false;
|
||||
// Persist the mute state across sessions. Default to muted so autoplay
|
||||
// policies don't silently fail on first load; the user can unmute once
|
||||
// and we remember it from then on.
|
||||
let muted = (localStorage.getItem('shortsMuted') ?? '1') === '1';
|
||||
let current = -1;
|
||||
|
||||
// ── Deep-link to /shorts/{id} — scroll the matching item into view
|
||||
// before the observer starts, so autoplay fires on the target short
|
||||
// rather than the first in document order. ────────────────────
|
||||
@if($startId)
|
||||
(function () {
|
||||
const target = document.querySelector('.short-item[data-id="{{ (int) $startId }}"]');
|
||||
if (target) target.scrollIntoView({ behavior: 'instant' in window ? 'instant' : 'auto', block: 'start' });
|
||||
})();
|
||||
@endif
|
||||
|
||||
// ── IntersectionObserver — auto-play visible short ─────────
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
@@ -446,12 +459,23 @@
|
||||
if (entry.isIntersecting && entry.intersectionRatio >= 0.6) {
|
||||
if (current !== -1 && current !== idx) pauseShort(current);
|
||||
current = idx;
|
||||
video.muted = muted;
|
||||
|
||||
// ── Reflect the active short in the URL (no history entry) ──
|
||||
// Users can copy-paste the address bar and get back here; the
|
||||
// back button still exits the feed instead of walking through
|
||||
// every short they viewed.
|
||||
try {
|
||||
const vid = item.dataset.id;
|
||||
if (vid) history.replaceState(null, '', '/shorts/' + vid);
|
||||
} catch (_) {}
|
||||
|
||||
video.muted = muted;
|
||||
video.volume = 1;
|
||||
video.play().catch(() => {
|
||||
// autoplay blocked — start muted then unmute
|
||||
// autoplay blocked — fall back to muted
|
||||
video.muted = true;
|
||||
muted = true;
|
||||
localStorage.setItem('shortsMuted', '1');
|
||||
updateMuteIcon(idx);
|
||||
video.play().catch(() => {});
|
||||
});
|
||||
@@ -500,6 +524,7 @@
|
||||
// ── Mute toggle ────────────────────────────────────────────
|
||||
window.toggleMute = function(idx) {
|
||||
muted = !muted;
|
||||
localStorage.setItem('shortsMuted', muted ? '1' : '0');
|
||||
const video = document.getElementById('sv-' + idx);
|
||||
if (video) video.muted = muted;
|
||||
updateMuteIcon(idx);
|
||||
@@ -528,6 +553,22 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ── Mouse wheel nav (desktop) ──────────────────────────────
|
||||
// One wheel event = one short, debounced so a trackpad flick doesn't
|
||||
// skip five shorts at once. The main scroll container's own snap would
|
||||
// nearly handle this, but a slow scroll leaves the feed mid-snap on
|
||||
// some browsers — this makes the step explicit and symmetrical.
|
||||
let wheelLock = false;
|
||||
main?.addEventListener('wheel', (e) => {
|
||||
if (wheelLock) { e.preventDefault(); return; }
|
||||
if (Math.abs(e.deltaY) < 10) return; // ignore tiny jogs
|
||||
wheelLock = true;
|
||||
setTimeout(() => { wheelLock = false; }, 450);
|
||||
e.preventDefault();
|
||||
const dir = e.deltaY > 0 ? 1 : -1;
|
||||
if (current !== -1) navigateShort(dir, current);
|
||||
}, { passive: false });
|
||||
|
||||
// ── Like ───────────────────────────────────────────────────
|
||||
window.shortLike = function(videoId) {
|
||||
@auth
|
||||
|
||||
Reference in new issue
Block a user