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:
ghassanandClaude Opus 4.7 committed 2026-10-21 10:30:42 +03:00
1 parent 646d3e9d2a
commit 9e613c05d2
3 files changed
+67 -6

No files matched your search

+19 -3
View File
@@ -26,7 +26,7 @@ class VideoController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['index', 'show', 'search', 'stream', 'hls', 'trending', 'shorts', 'download', 'downloadMp3', 'recordShare', 'ogImage', 'vsPreview', 'accessShare', 'showByToken', 'recommendations', 'slideshowProgress', 'playerData', 'streamAudioTrack', 'lyricsProgress', 'translateDescription', 'feedMore']);
$this->middleware('auth')->except(['index', 'show', 'search', 'stream', 'hls', 'trending', 'shorts', 'shortsFocused', 'download', 'downloadMp3', 'recordShare', 'ogImage', 'vsPreview', 'accessShare', 'showByToken', 'recommendations', 'slideshowProgress', 'playerData', 'streamAudioTrack', 'lyricsProgress', 'translateDescription', 'feedMore']);
}
/**
@@ -2190,7 +2190,7 @@ class VideoController extends Controller
]);
}
public function shorts(Request $request)
public function shorts(Request $request, ?int $startId = null)
{
$videos = Video::public()
->where('is_shorts', true)
@@ -2199,7 +2199,23 @@ class VideoController extends Controller
->latest()
->get();
return view('videos.shorts', compact('videos'));
return view('videos.shorts', [
'videos' => $videos,
'startId' => $startId, // the id to auto-scroll to, if any
]);
}
/**
* /shorts/{video}: deep-link to a specific short in the vertical feed.
* The feed itself is the same ordered list; the view uses ?start= / the
* $startId prop to scroll into position on load.
*/
public function shortsFocused(Video $video)
{
if (! $video->is_shorts || ! $video->canView(Auth::user())) {
abort(404);
}
return $this->shorts(request(), $video->id);
}
public function stream(Video $video)