From f98e5415a3b3a13c7fac5cce97e60f7a1db2a2a7 Mon Sep 17 00:00:00 2001 From: ghassan Date: Sun, 31 May 2026 22:01:47 +0300 Subject: [PATCH] Add lyrics pipeline, playlist views, admin toggles, and player polish Lyrics pipeline (Whisper + Demucs + description alignment): - New GenerateLyricsJob runs WhisperX with VAD filtering and forced word alignment, writes per-track JSON to NAS. - New DecorateLyricsJob calls the active LLM provider to bake one to several emojis into each line (heavy decoration prompt). - LyricsDescriptionParser strips heading content, section markers, and emoji-decoration from a song's description while preserving every language verbatim. - correct_whisper_with_description aligner: strong-match anchors only, vocal-region-aware gap-fill so missing verses land on actual singing. - Owner UI for generate/regenerate/edit/delete in the player gear. Admin pages: - /admin/lyrics toggles for VAD, vocal gap-fill, Demucs, master - /admin/gpu extracted GPU section, encoder picker, FFmpeg path - /admin/backup extracted users-and-settings export/import - /admin/settings now AI/LLM only with provider list and Test button - /admin/nas-storage hosts NAS settings, repair, disable flow, browser - Shared partials/settings-styles for a uniform look across pages. Playlist view tracking: - Migration adds playlists.view_count and playlist_views dedup table. - Playlist::bumpViewIfNew increments per device with a one-hour window. - Tracked from /playlists/{id}, /playlists/share/{token}, /ps/{token}, and /videos/{id}?playlist={token}. Dispatched after-response so it never blocks the page render. - Loading a playlist on the video page now runs one query instead of the four the old getNextVideo/getPreviousVideo path triggered. - View counts shown on every playlist card and the playlist hero. Player polish: - Floating mini-player is draggable, persists its position in localStorage, clamps to viewport on resize. - Mini disabled entirely on mobile (less than 768px). - New gear-menu Mini Player toggle (persists in localStorage) lets the user disable both scroll-activation and SPA-nav-activation. - Close button keeps media playing when used on the player's own page. - SPA navigator now swaps a #page-scripts container so per-page JS (channel tabs, etc.) gets re-executed after content swaps. Storage layout: - Runtime data moved from /storage/* to /data/* and gitignored. - /ml/venv, /ml/cache, /ml/__pycache__ excluded. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 11 +- CLAUDE.md | 188 ++- app/Console/Commands/GenerateLyrics.php | 61 + app/Console/Commands/MigrateStorageLayout.php | 241 ++++ app/Http/Controllers/PlaylistController.php | 32 +- app/Http/Controllers/SuperAdminController.php | 290 +++- app/Http/Controllers/VideoController.php | 437 +++++- app/Jobs/DecorateLyricsJob.php | 139 ++ app/Jobs/GenerateLyricsJob.php | 244 ++++ app/Models/Playlist.php | 79 +- app/Models/Video.php | 34 + app/Models/VideoAudioTrack.php | 6 + app/Models/VideoSlide.php | 7 +- app/Services/LlmLyricsService.php | 266 ++++ app/Services/NasSyncService.php | 316 +++- app/Support/LyricsAss.php | 109 ++ app/Support/LyricsDescriptionParser.php | 144 ++ bootstrap/app.php | 8 + ...001_add_audio_track_id_to_video_slides.php | 31 + ..._31_000002_add_playlist_views_tracking.php | 49 + ml/transcribe.py | 895 ++++++++++++ resources/views/admin/backup.blade.php | 69 + resources/views/admin/gpu.blade.php | 283 ++++ resources/views/admin/layout.blade.php | 22 +- resources/views/admin/lyrics.blade.php | 191 +++ resources/views/admin/nas-storage.blade.php | 345 ++++- .../admin/partials/settings-styles.blade.php | 124 ++ resources/views/admin/settings.blade.php | 1007 ++++--------- .../views/components/image-cropper.blade.php | 109 +- .../views/components/playlist-card.blade.php | 81 ++ .../views/components/video-actions.blade.php | 7 + .../views/components/video-card.blade.php | 18 +- .../views/components/video-player.blade.php | 48 +- resources/views/layouts/app.blade.php | 516 ++++++- .../partials/add-to-playlist-modal.blade.php | 29 +- .../layouts/partials/upload-modal.blade.php | 17 +- resources/views/playlists/index.blade.php | 2 +- resources/views/playlists/show.blade.php | 8 + resources/views/user/channel.blade.php | 1268 ++--------------- .../partials/channel/styles/desktop.blade.php | 1169 +++++++++++++++ .../partials/channel/styles/mobile.blade.php | 150 ++ resources/views/videos/create.blade.php | 17 +- .../videos/partials/audio-player.blade.php | 749 +++++++++- .../views/videos/types/generic.blade.php | 527 ++++--- resources/views/videos/types/match.blade.php | 6 + resources/views/videos/types/music.blade.php | 11 + routes/web.php | 17 + screenshots/Screenshot 2026-05-01 104419.png | Bin 34895 -> 0 bytes ...03-73_6012fa4d4ddec268fc5c7112cbb265e7.jpg | Bin 293864 -> 0 bytes scripts/fix-gpu-host.sh | 59 - scripts/fix-gpu-vm.sh | 132 -- storage/app/.gitignore | 3 - storage/app/public/.gitignore | 2 - storage/framework/.gitignore | 9 - storage/framework/cache/.gitignore | 3 - storage/framework/cache/data/.gitignore | 2 - storage/framework/sessions/.gitignore | 2 - storage/framework/testing/.gitignore | 2 - storage/framework/views/.gitignore | 2 - storage/logs/.gitignore | 2 - 60 files changed, 7920 insertions(+), 2675 deletions(-) create mode 100644 app/Console/Commands/GenerateLyrics.php create mode 100644 app/Console/Commands/MigrateStorageLayout.php create mode 100644 app/Jobs/DecorateLyricsJob.php create mode 100644 app/Jobs/GenerateLyricsJob.php create mode 100644 app/Services/LlmLyricsService.php create mode 100644 app/Support/LyricsAss.php create mode 100644 app/Support/LyricsDescriptionParser.php create mode 100644 database/migrations/2026_05_31_000001_add_audio_track_id_to_video_slides.php create mode 100644 database/migrations/2026_05_31_000002_add_playlist_views_tracking.php create mode 100644 ml/transcribe.py create mode 100644 resources/views/admin/backup.blade.php create mode 100644 resources/views/admin/gpu.blade.php create mode 100644 resources/views/admin/lyrics.blade.php create mode 100644 resources/views/admin/partials/settings-styles.blade.php create mode 100644 resources/views/user/partials/channel/styles/desktop.blade.php create mode 100644 resources/views/user/partials/channel/styles/mobile.blade.php delete mode 100644 screenshots/Screenshot 2026-05-01 104419.png delete mode 100644 screenshots/Screenshot_2026-05-04-00-00-03-73_6012fa4d4ddec268fc5c7112cbb265e7.jpg delete mode 100755 scripts/fix-gpu-host.sh delete mode 100755 scripts/fix-gpu-vm.sh delete mode 100755 storage/app/.gitignore delete mode 100755 storage/app/public/.gitignore delete mode 100755 storage/framework/.gitignore delete mode 100755 storage/framework/cache/.gitignore delete mode 100755 storage/framework/cache/data/.gitignore delete mode 100755 storage/framework/sessions/.gitignore delete mode 100755 storage/framework/testing/.gitignore delete mode 100755 storage/framework/views/.gitignore delete mode 100755 storage/logs/.gitignore diff --git a/.gitignore b/.gitignore index da2fd58..1fc0745 100755 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ /public/build /public/hot /public/storage -/storage/*.key +/data/*.key /vendor .env .env.backup @@ -18,3 +18,12 @@ yarn-error.log /.idea /.vscode /.claude/mcp + +# Lyrics ML stack: keep transcribe.py, ignore the heavy venv + model cache +/ml/venv +/ml/cache +/ml/__pycache__ + +# Runtime storage (moved from /storage to /data) — user uploads, sessions, +# cache, logs, tmp. Never goes into git. +/data diff --git a/CLAUDE.md b/CLAUDE.md index abc4832..0c8002f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,6 +122,49 @@ Structure: ` + +@endif + +@if($errors->any()) +
+ + {{ $errors->first() }} + +
+@endif + +
+
+ + Users & Settings Backup +
+
+ +
+
+ Export users & settings + Downloads a JSON file containing all user accounts and system settings. Does not include media files. +
+ +
+ +
+
+ Restore users & settings + Upload a previously exported backup JSON. Existing users are matched by email and updated; new users are created. Settings are merged. +
+
+
+ @csrf + + +
+
+
+ +
+
+@endsection diff --git a/resources/views/admin/gpu.blade.php b/resources/views/admin/gpu.blade.php new file mode 100644 index 0000000..7bb56eb --- /dev/null +++ b/resources/views/admin/gpu.blade.php @@ -0,0 +1,283 @@ +@extends('admin.layout') + +@section('title', 'GPU Accelerator') +@section('page_title', 'GPU Accelerator') + +@section('extra_styles') +@include('admin.partials.settings-styles') +@endsection + +@section('content') +
+

GPU Accelerator

+
+ +@if(session('success')) +
+ + {{ session('success') }} + +
+@endif + +@if($errors->any()) +
+ + {{ $errors->first() }} + +
+@endif + +
+@csrf + +
+
+ + GPU Accelerated Processing + + @if(count($gpus)) + {{ count($gpus) }} GPU{{ count($gpus) > 1 ? 's' : '' }} detected + @else + No GPU detected + @endif + + + @if($nvencWorks) + NVENC encoding ✓ + @else + NVENC encoding ✗ + @endif + +
+ + @if(!$nvencWorks && count($gpus)) +
+ ⚠ NVENC is not working with the current FFmpeg binary.
+ The GPU is detected but FFmpeg cannot initialise CUDA on this driver.
+ Fix: Install a newer FFmpeg with CUDA 12+ support (e.g. jellyfin-ffmpeg7), then update the binary path below.
+ Until then, video encoding will automatically fall back to CPU (libx264). +
+ @endif + +
+ +
+
+ Available GPUs + +
+
+ @if(count($gpus)) + @include('admin.partials.gpu-cards', ['gpus' => $gpus, 'selectedDevice' => $settings['gpu_device']]) + @else +
+ +

No NVIDIA GPUs detected. Click "Detect GPUs" to scan.

+
+ @endif +
+
+ +
+
+ Enable GPU acceleration + When enabled, video encoding uses the NVIDIA GPU. When disabled, falls back to CPU (libx264). +
+
+ + +
+
+ + + +
+
+ Video encoder + h264_nvenc is broadly compatible. hevc_nvenc produces smaller files (H.265) but requires compatible players. libx264 forces CPU encoding regardless of the toggle above. +
+
+
+ @foreach([ + ['h264_nvenc', 'H.264 NVENC', 'GPU · max compatibility'], + ['hevc_nvenc', 'H.265 NVENC', 'GPU · smaller files'], + ['libx264', 'libx264', 'CPU · software fallback'], + ] as [$val, $label, $desc]) + + @endforeach +
+ +
+
+ +
+
+ Encoding preset + NVENC presets: p1 (fastest) → p7 (best quality). libx264 presets: fast / medium / slow. Preset only affects speed vs file size; quality is controlled by CQ/CRF. +
+
+ +
+
+ +
+
+ Hardware decode acceleration + Use CUDA to decode the source video on the GPU before re-encoding, speeding up the pipeline. Disable if you see CUDA errors in the logs. +
+
+ +
+
+ +
+
+ FFmpeg binary path + + Absolute path to the ffmpeg executable. + Change this to use a newer build (e.g. /usr/lib/jellyfin-ffmpeg/ffmpeg) + that supports your GPU driver. Current: {{ config('ffmpeg.ffmpeg', '/usr/bin/ffmpeg') }} + +
+
+ +
+
+ +
+
+ +
+ Cancel + +
+ +
+@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/admin/layout.blade.php b/resources/views/admin/layout.blade.php index 589c3f9..befb78b 100644 --- a/resources/views/admin/layout.blade.php +++ b/resources/views/admin/layout.blade.php @@ -597,7 +597,23 @@
System
- Settings + AI / LLM + + + Lyrics Pipeline + + + GPU Accelerator + + + NAS Storage + + + Backup & Restore @@ -607,10 +623,6 @@ class="adm-nav-link {{ request()->routeIs('admin.logs') ? 'active' : '' }}"> Error Logs - - NAS Storage - @endif
diff --git a/resources/views/admin/lyrics.blade.php b/resources/views/admin/lyrics.blade.php new file mode 100644 index 0000000..07316b3 --- /dev/null +++ b/resources/views/admin/lyrics.blade.php @@ -0,0 +1,191 @@ +@extends('admin.layout') + +@section('title', 'Lyrics Pipeline') +@section('page_title', 'Lyrics Pipeline') + +@section('extra_styles') +@include('admin.partials.settings-styles') +@endsection + +@section('content') +
+

Lyrics Pipeline

+
+ +@if(session('success')) +
+ + {{ session('success') }} + +
+@endif + +
+@csrf + + +{{-- ── Master switch ─────────────────────────────────────────── --}} +
+
+ + Master + @if($settings['lyrics_enabled'] === 'true') + Enabled + @else + Disabled + @endif +
+
+ +
+
+ Lyrics generation + + Master switch for the whole feature. When OFF, the Generate / Regenerate + button in the player gear is hidden and the API endpoint refuses new jobs. + Existing lyrics keep displaying — only NEW generation is blocked. + +
+
+ +
+
+ +
+
+ +{{-- ── Pipeline steps ────────────────────────────────────────── --}} +
+
+ + Pipeline steps +
+
+ +
+
+ Description-based correction + + Use the song description as the source of truth for lyric text. Whisper provides + timing anchors; the description provides the exact words. When OFF, the player + shows Whisper's raw transcription (may contain misheard words and miss verses). + +
+
+ +
+
+ +
+
+ Voice activity filter (VAD) + + Runs Silero VAD before Whisper to skip pure-instrumental sections. Without this, + Whisper invents lyrics over intro / outro / breakdown music. When OFF, the entire + audio is transcribed — useful as a fallback if VAD is dropping legitimate quiet vocals. + +
+
+ +
+
+ +
+
+ Vocal-region gap-filling + + For description lines Whisper missed, snap them to moments where vocals are + active (detected by VAD) instead of distributing evenly across time. Keeps + gap-filled lyrics on actual singing. When OFF, missing lines distribute uniformly + between Whisper anchors (faster but can drift over instrumental beats). + +
+
+ +
+
+ +
+
+ Vocal isolation (Demucs) + + Two-stem source separation (vocals vs. instruments) before Whisper. Improves + transcription on noisy mixes but adds ~30s GPU time and has caused a CUDA-handoff + deadlock that hangs the pipeline at 50%. Currently auto-skipped when a description + is present (timing-only mode). Enable to also use Demucs when no description is given. + +
+
+ +
+
+ +
+
+ LLM emoji decoration + + After lyrics are saved, run the active LLM provider to bake one or more emojis + into each line. Without this, the player still decorates lines using a built-in + keyword-to-emoji map (less varied). Provider is configured under + AI / LLM Settings. + +
+
+ +
+
+ +
+
+ +
+ Cancel + +
+ +
+@endsection diff --git a/resources/views/admin/nas-storage.blade.php b/resources/views/admin/nas-storage.blade.php index 18ee93d..fb9f985 100644 --- a/resources/views/admin/nas-storage.blade.php +++ b/resources/views/admin/nas-storage.blade.php @@ -1,23 +1,356 @@ @extends('admin.layout') @section('title', 'NAS Storage') +@section('page_title', 'NAS Storage') + +@section('extra_styles') +@include('admin.partials.settings-styles') + +@endsection @section('content')

NAS Storage + @if($settings['nas_sync_enabled'] === 'true') + Enabled + @else + Disabled + @endif

-
- @include('nas-file-manager::file-manager', [ - 'nodes' => $nodes, - 'canEdit' => true, - 'title' => 'NAS Storage Browser', - ]) +@if(session('success')) +
+ + {{ session('success') }} + +
+@endif + +{{-- ── NAS Settings ─────────────────────────────────────────── --}} +
+
+ + NAS Settings +
+
+ +
+
+ Use NAS as primary storage + + When enabled, uploads go directly to the NAS — no permanent local copy is kept. + Files are stored at users/{username}/videos/{title-slug}/ on the NAS share. + When disabled, all files are served from local disk using the same directory schema. + Disabling NAS will prompt you to migrate files or start fresh. + +
+
+ @if($settings['nas_sync_enabled'] === 'true') + + @else + NAS is disabled. Re-enabling is handled by the system once a NAS endpoint is reachable. + @endif +
+
+ + @if($settings['nas_sync_enabled'] === 'true') +
+
+ Repair stuck files + + Scans for files that were saved locally but never reached the NAS (e.g. due to a + connection error during upload or edit). Uploads them to the NAS, then removes the + local copies. Safe to run at any time — nothing is deleted until the NAS confirms receipt. + +
+
+
+ + +
+ +
+
+ @endif + +
+
+ +{{-- ── NAS File Browser ──────────────────────────────────────── --}} +
+
+ + NAS File Browser +
+
+ @include('nas-file-manager::file-manager', [ + 'nodes' => $nodes, + 'canEdit' => true, + 'title' => 'NAS Storage Browser', + ]) +
+
+ +{{-- ── NAS Disable Modal ─────────────────────────────────────── --}} + @endsection @section('scripts') + @endsection diff --git a/resources/views/admin/partials/settings-styles.blade.php b/resources/views/admin/partials/settings-styles.blade.php new file mode 100644 index 0000000..6b2f6a8 --- /dev/null +++ b/resources/views/admin/partials/settings-styles.blade.php @@ -0,0 +1,124 @@ +{{-- Shared styles for all admin settings-style pages: GPU, NAS, Backup, Settings. --}} + diff --git a/resources/views/admin/settings.blade.php b/resources/views/admin/settings.blade.php index 37febe6..1345fc5 100644 --- a/resources/views/admin/settings.blade.php +++ b/resources/views/admin/settings.blade.php @@ -1,136 +1,83 @@ @extends('admin.layout') -@section('title', 'System Settings') -@section('page_title', 'Settings') +@section('title', 'AI / LLM Settings') +@section('page_title', 'AI / LLM Settings') @section('extra_styles') +@include('admin.partials.settings-styles') @endsection @section('content')
-

System Settings

+

AI / LLM Settings

@if(session('success')) @@ -149,279 +96,177 @@
@endif -
+ @csrf + -{{-- ── GPU Processing ───────────────────────────────────────── --}} +{{-- ── AI / LLM toggles ──────────────────────────────────────── --}}
- - GPU Accelerated Processing - - @if(count($gpus)) - {{ count($gpus) }} GPU{{ count($gpus) > 1 ? 's' : '' }} detected - @else - No GPU detected - @endif - - {{-- NVENC encoding health --}} - - @if($nvencWorks) - NVENC encoding ✓ - @else - NVENC encoding ✗ - @endif - + + Lyrics LLM Pipeline + @if($settings['llm_enabled'] === 'true') + Enabled + @else + Disabled + @endif
- - @if(!$nvencWorks && count($gpus)) -
- ⚠ NVENC is not working with the current FFmpeg binary.
- The GPU is detected but FFmpeg {{ shell_exec('/usr/bin/ffmpeg -version 2>/dev/null | head -1') ?? '' }} cannot initialise CUDA on this driver.
- Fix: Install a newer FFmpeg with CUDA 12+ support (e.g. jellyfin-ffmpeg7), then update the binary path below.
- Until then, video encoding will automatically fall back to CPU (libx264). -
- @endif -
- {{-- Detect button + GPU cards --}} -
-
- Available GPUs - -
-
- @if(count($gpus)) - @include('admin.partials.gpu-cards', ['gpus' => $gpus, 'selectedDevice' => $settings['gpu_device']]) - @else -
- -

No NVIDIA GPUs detected. Click "Detect GPUs" to scan.

-
- @endif -
-
+

+ Configure one or more LLM providers (local Ollama or hosted APIs). Pick one + as Active — that's the provider the lyrics pipeline uses + to clean descriptions and (optionally) pick per-line emojis. With LLM off, + the pipeline uses the built-in regex parser and keyword emoji map. +

- {{-- Enable GPU --}}
- Enable GPU acceleration - When enabled, video encoding uses the NVIDIA GPU. When disabled, falls back to CPU (libx264). + Enable LLM + Master switch. When off, the regex/keyword pipeline runs instead.
-
- {{-- GPU device (hidden input, controlled by card click) --}} - - - {{-- GPU Encoder --}} -
+
- Video encoder - h264_nvenc is broadly compatible. hevc_nvenc produces smaller files (H.265) but requires compatible players. libx264 forces CPU encoding regardless of the toggle above. + Clean description lyrics + Use the active LLM to drop titles, section headers, instrument tags, etc. from a song's description before aligning to audio.
-
- @foreach([ - ['h264_nvenc', 'H.264 NVENC', 'GPU · max compatibility'], - ['hevc_nvenc', 'H.265 NVENC', 'GPU · smaller files'], - ['libx264', 'libx264', 'CPU · software fallback'], - ] as [$val, $label, $desc]) -
+
+ +
+
+ Decorate lines with emojis + Replaces the built-in keyword emoji with a per-line contextual emoji chosen by the LLM. Results are cached. +
+
+ +
+
+ +
+
+ +{{-- ── Providers ─────────────────────────────────────────────── --}} +
+
+ + Providers +
+
+ +

+ Add as many as you like. Ollama runs locally with no API key + (default http://localhost:11434). Anthropic / + OpenAI need a key. Pick the radio next to the provider you + want active. +

+ +
+ @foreach($settings['llm_providers'] as $i => $p) + @php + $pid = $p['id'] ?? \Illuminate\Support\Str::uuid()->toString(); + $kind = $p['kind'] ?? 'ollama'; + $hasKey = isset($p['api_key']) && $p['api_key'] !== ''; + @endphp +
+
+ + + + + + + + + + - @endforeach
- +
+
+ @endforeach
- {{-- Encoding preset --}} -
-
- Encoding preset - NVENC presets: p1 (fastest) → p7 (best quality). libx264 presets: fast / medium / slow. Preset only affects speed vs file size; quality is controlled by CQ/CRF. -
-
- -
-
+ - {{-- HW Accel --}} -
-
- Hardware decode acceleration - Use CUDA to decode the source video on the GPU before re-encoding, speeding up the pipeline. Disable if you see CUDA errors in the logs. -
-
- -
-
- - {{-- FFmpeg binary path --}} -
-
- FFmpeg binary path - - Absolute path to the ffmpeg executable. - Change this to use a newer build (e.g. /usr/lib/jellyfin-ffmpeg/ffmpeg) - that supports your GPU driver. Current: {{ config('ffmpeg.ffmpeg', '/usr/bin/ffmpeg') }} - -
-
- -
-
- -
-
- -{{-- ── NAS Storage ───────────────────────────────────────────── --}} -
-
- - NAS Storage - @if($settings['nas_sync_enabled'] === 'true') - Enabled - @else - Disabled - @endif -
-
- -
-
- Use NAS as primary storage - - When enabled, uploads go directly to the NAS — no permanent local copy is kept. - Files are stored at users/{username}/videos/{title-slug}/ on the NAS share. - When disabled, all files are served from local disk using the same directory schema. - Disabling NAS will prompt you to migrate files or start fresh. - -
-
- @if($settings['nas_sync_enabled'] === 'true') -
-{{-- ── Save ─────────────────────────────────────────────────── --}}
Cancel
@@ -430,363 +275,139 @@ @section('scripts') - - -{{-- ── NAS Disable Modal ─────────────────────────────────────── --}} - @endsection diff --git a/resources/views/components/image-cropper.blade.php b/resources/views/components/image-cropper.blade.php index 5a7672b..399b62b 100644 --- a/resources/views/components/image-cropper.blade.php +++ b/resources/views/components/image-cropper.blade.php @@ -14,113 +14,8 @@ $resultCallback = $attributes->get('result-callback', ''); // callback mode: JS fn called with the cropped File @endphp -@once - - - -@endonce +{{-- Cropme assets + .tc-* styles now live in layouts/app.blade.php + so they survive SPA-nav innerHTML swaps on #main. --}} {{-- Modal --}}
+
+ + +
+ +@once + +@endonce diff --git a/resources/views/components/video-actions.blade.php b/resources/views/components/video-actions.blade.php index e966c6f..17752aa 100644 --- a/resources/views/components/video-actions.blade.php +++ b/resources/views/components/video-actions.blade.php @@ -128,6 +128,8 @@ Edit + {{-- Lyrics generate/regenerate + edit now live inside the player's gear menu + so they're always reachable on both mobile and desktop. --}}
+@once +@endonce @once @endonce diff --git a/resources/views/components/video-player.blade.php b/resources/views/components/video-player.blade.php index a4337a8..132b535 100644 --- a/resources/views/components/video-player.blade.php +++ b/resources/views/components/video-player.blade.php @@ -134,6 +134,12 @@
+ {{-- Mini player toggle — desktop-only, persisted in localStorage --}} +
+ + Mini player + On +
Playback speed @@ -818,6 +824,19 @@ video.addEventListener('pause', function () { window._ytpWasPlaying = false; }); function initSource() { video.muted = true; video.autoplay = true; + /* Resume handoff from the mini player: ?t= seeks the video to that + position once metadata is ready. One-shot — only the initial load. */ + try { + var _qs = new URLSearchParams(location.search); + var _t = parseInt(_qs.get('t') || '0', 10); + if (_t > 0) { + video.addEventListener('loadedmetadata', function () { + if (_t < (video.duration || Infinity)) { + try { video.currentTime = _t; } catch (e) {} + } + }, { once: true }); + } + } catch (e) {} if (HLS_URL && window.Hls && Hls.isSupported()) { window._ytpHls = new Hls({ startLevel: -1 }); // Register MANIFEST_PARSED before loadSource to avoid cache race condition @@ -1077,6 +1096,17 @@ settingsBtn.addEventListener('click', e => { e.stopPropagation(); const open = settingsPanel.classList.toggle('open'); if (!open) { speedPanel.classList.remove('open'); speedRow.style.display = ''; } + /* Sync the mini-player toggle row's label to the current preference each + time the gear opens, so reloading the page or toggling from the music + player keeps the indicator honest. */ + if (open) { + const miniRow = document.getElementById('ytpMiniToggleRow'); + if (miniRow) { + const v = miniRow.querySelector('.ytp-settings-val'); + const on = !window._ytpMiniEnabled || window._ytpMiniEnabled(); + if (v) v.textContent = on ? 'On' : 'Off'; + } + } showControls(); clearTimeout(hideTimer); // keep controls visible while settings open }); @@ -1370,10 +1400,11 @@ function init() { largePlay.classList.add('visible'); showControls(); - // Scroll-based mini player: watch when #ytpWrap leaves the viewport - if (window.IntersectionObserver && window._miniPlayer) { - /* On mobile, #main is the scroll container; on desktop the window scrolls */ - var _scrollRoot = window.innerWidth <= 768 ? document.getElementById('main') : null; + // Scroll-based mini player: watch when #ytpWrap leaves the viewport. + // Desktop-only — on mobile the fixed bottom-nav + locked scroll model + // make a floating overlay disruptive. + if (window.IntersectionObserver && window._miniPlayer && window.innerWidth > 768) { + var _scrollRoot = null; /* desktop: window scrolls */ var _scrollMiniOn = false; /* guard against rapid toggling / initial fire */ new IntersectionObserver(function (entries) { var e0 = entries[0]; @@ -1381,7 +1412,8 @@ function init() { Using !video.paused was unreliable: autoplay fires asynchronously and the initial IntersectionObserver callback could run before HLS.js even attaches, teleporting the element before it ever played in the main player. */ - if (!e0.isIntersecting && !_scrollMiniOn && window._ytpWasPlaying && !window._miniPlayer.isNavMode()) { + var miniAllowed = !window._ytpMiniEnabled || window._ytpMiniEnabled(); + if (!e0.isIntersecting && !_scrollMiniOn && window._ytpWasPlaying && miniAllowed && !window._miniPlayer.isNavMode()) { _scrollMiniOn = true; window._miniPlayer.activateScroll( document.title.replace(/\s*\|.*$/, '').trim(), @@ -1392,6 +1424,12 @@ function init() { window._miniPlayer.deactivateScroll(); } }, { root: _scrollRoot, threshold: 0.15 }).observe(wrap); + + /* User clicked the X on the mini while still on the video page — + reset the flag so a subsequent scroll-away re-activates it. */ + window.addEventListener('miniplayer:scroll-closed', function () { + _scrollMiniOn = false; + }); } } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index f5c2aba..d874b82 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -16,6 +16,118 @@ + {{-- Image cropper assets — must be in the layout head (not inside the + x-image-cropper component) because page-level uses of the cropper + render those styles inside #main, which the SPA navigation later + wipes via innerHTML swap. The layout-level modals (upload, + sports-match) would then render their cropper overlays unstyled. --}} + + + +@include('user.partials.channel.styles.desktop') +@include('user.partials.channel.styles.mobile') @endsection @section('content') @@ -1359,6 +213,71 @@ $headerSocialMap = [ @endif
@endif + + @if($isOwner && !$preview) + {{-- Owner manage dropdown: consolidates Edit channel / 2FA / Log Out All + so it can sit inline with the horoscope strip instead of taking up + a whole row beneath it. --}} +
+ +
+ + + +
+
+ + + {{-- Primary CTA + visitor-preview icon, inline alongside Manage. --}} + + + + + @endif @if($socialLinks->isNotEmpty())
@foreach($socialLinks as $slink) @@ -1379,34 +298,10 @@ $headerSocialMap = [
@endif - {{-- Action buttons (owner only) --}} - @if($isOwner && !$preview) -
- - - - - - - -
+ {{-- Owner action row moved up into the horoscope strip — Edit/2FA/Logout + collapsed into Manage; Upload + Preview sit inline next to it. --}} + @if($isOwner && !$preview) {{-- Logout All Devices Modal — appended to body on open to escape stacking context --}}