From 9e613c05d2ed31270498d65b2d2fe2a4baef2d4f Mon Sep 17 00:00:00 2001 From: ghassan Date: Wed, 21 Oct 2026 10:30:42 +0300 Subject: [PATCH] feat(shorts): /shorts/{id} deep-link + URL sync + persistent mute + wheel nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- app/Http/Controllers/VideoController.php | 22 +++++++++-- resources/views/videos/shorts.blade.php | 47 ++++++++++++++++++++++-- routes/web.php | 4 ++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/VideoController.php b/app/Http/Controllers/VideoController.php index 0b1efd5..18b7545 100644 --- a/app/Http/Controllers/VideoController.php +++ b/app/Http/Controllers/VideoController.php @@ -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) diff --git a/resources/views/videos/shorts.blade.php b/resources/views/videos/shorts.blade.php index 6a26b88..3bcccaf 100644 --- a/resources/views/videos/shorts.blade.php +++ b/resources/views/videos/shorts.blade.php @@ -432,9 +432,22 @@