68 Commits

Author SHA1 Message Date
ghassan
3e18bd2150 Match videos: composed VS-card OG preview + richer share metadata
Sports match share links now show the full VS composition (fighters,
flags, event, weight class, referee/court/match chips) in WhatsApp /
Telegram / Facebook / Twitter previews instead of the raw video frame.

- Browsershot + puppeteer-bundled Chromium screenshots a dedicated
  1200x675 (true 16:9) render of the vs-mini composition. Chrome runs
  through a small wrapper (bin/browsershot-node.sh) that raises the fd
  limit and reads its full env, since PHP-FPM's stripped env otherwise
  makes Chromium fail to launch.
- Render is JPEG @ q85 (~100 KB) instead of PNG (~700 KB) — WhatsApp
  silently drops OG images above roughly 300 KB, which is why music
  thumbnails always showed and match cards didn't.
- SportsMatchController@store/update renders synchronously so the
  picture is guaranteed to exist before the user copies the share URL;
  edits regenerate and old stamped variants are cleaned. The og:image
  URL carries a ?v={updated_at} stamp so scrapers recache on edit.
- accessShare() (/s/{token}) now detects scraper user agents and serves
  the video page HTML inline instead of a 302 — some crawlers don't
  follow redirects when building link previews.
- og:description / twitter:description built from headerData(): flag
  emoji + fighter names (blue vs red), event, weight, match#, court,
  referee, view count, upload date.
- Fixed 3-dot menu on video cards being hidden behind the next card
  (:has(.dropdown-menu.show) promotes the active card above siblings
  and lifts the overflow:hidden clip on .yt-video-info).
- vs-mini: skip the entrance animation on touch devices and expose a
  .vs-mini-static class so the OG render always captures the final
  frame instead of a random mid-animation moment.

Also adds an admin gallery at /admin/og-previews for spot-checking the
generated preview JPEGs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-11 12:34:40 +03:00
ghassan
665bad76bf Match cards: composed title, header stage from match title, equal-sized grid
- SportsMatchController syncs Video.title with SportsMatch.title on
  store/update so the "Match title" field drives the video's display
  title everywhere.
- headerData(): "championship" now prefers event_name (event title)
  and adds a dedicated "stage" key sourced from SportsMatch.title
  (shown between the yellow lines above the weight class in both the
  in-player VS overlay and the vs-mini gallery card).
- Video cards render match titles with flags + corner colours
  ("FINALS – 🇧🇭 Blue vs 🇧🇭 Red (-61 kg)"), matching the video
  page header, and clip long titles to one line.
- Video gallery grid uses repeat(N, minmax(0, 1fr)) + grid-auto-rows:1fr
  so every card thumbnail renders at exactly the same size; hover-preview
  videos use object-fit:cover to stop portrait sources looking smaller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-11 09:14:25 +03:00
ghassan
04d63cccdb VS intro screen (arena design), card VS mini + live scorebar, full country names, sports-match edit fix
Match player + preview
- Rebuild VS intro as a 1920×1080 scaled canvas (arena design from drafts/):
  angled RED/BLUE panels with clip-paths, slam-in VS with pulse+shine,
  event/stage/weight header, Match/Court/Referee chips, per-fighter
  Record/Rank/Stats chip row. Every placeholder from the artifact is
  always in the DOM; empty ones are hidden by `.vs-nullable:empty`
  and `.vs-nullable-hide` so the layout adapts without losing the
  design's HTML shape. Country codes always render as full country
  names (via new App\Data\Countries::name()).
- Real-time 6s countdown Skip button; video pinned at t=0 while
  the intro runs, released to play at zero or on Skip.
- Standalone preview page at /videos/{video}/vs-preview.

Video cards (home + everywhere)
- Match cards get a `vs-mini` overlay of the same arena design
  (fixed canvas, per-card ResizeObserver, animations replay every
  time the mouse leaves the thumb). On hover the mini explodes
  outward (panels fly out, VS scales+blurs+fades) and the hover
  video plays over the cleared thumb.
- Match cards also render a `scorebar-mini` overlay (verbatim of
  the full-player msb-scale scorebar + msb-feed) that shows
  during hover playback. Live-scoring is wired per-card via a
  data-sbm-state payload of rounds+points; RED/BLUE score, round
  label, per-round clock, and the top-right Live Scoring ticker
  (4 entries, sport-aware Yuko/Waza-ari/Ippon or Punch/Body
  kick/… labels) all update from the mini video's timeupdate.

Bug fixes
- Hover video wasn't playing sound: dropped the `muted` attribute
  I'd added while debugging match previews.
- White flash strobing on the left of match cards: removed the
  `.vs-mini-flash` element (intro-only flourish that fired on
  every mouseleave replay).
- Sports-match modal was showing "Please complete the required
  fields" when saving basic info for match videos that had no
  SportsMatch row yet: the submit gate only checked matchId, not
  the attached videoId; now it correctly skips uploadVideoFirst()
  in both edit and attach-existing-video modes.

Country names project-wide
- New helper App\Data\Countries::name($iso2) → full country name.
- Swapped `?? country` fallbacks to `?? Countries::name(country)`
  in admin dashboard, video-analytics, video-insights payloads
  (VideoController + SuperAdminController), and the VS partials.
  Now user-visible country labels always show "Bahrain" instead
  of "BH", including chart tooltips and modal titles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-10 18:34:31 +03:00
ghassan
b05ff14b3b Fullscreen highlights drawer: keep scoreboard visible, don't hide feed; scrollable gear menu; remove slowmo picker + scrim tint
- Scoreboard scale observes #videoContainer (the .ytp inner box) instead of
  #ytpWrap so the overlay tracks the squeezed container when the fullscreen
  highlights drawer opens. Score bar + live-scoring feed shift left with the
  video and stay fully visible next to the drawer.

- msb-small (auto-hide identity + feed under 700 px) now skipped while
  document.fullscreenElement is set — the drawer squeeze on smaller viewports
  was dropping the container under 700 and hiding the live feed.

- Player settings panel (#ytpSettingsPanel) is now vertical-scroll capable:
  max-height: calc(100vh - 120px) + overflow-y: auto + slim on-brand
  scrollbar + overscroll-behavior: contain. Top items (Mini player, Playback
  speed) stay reachable when the injected Scoreboard toggles push the
  list past the visible area.

- Removed the slow-mo (.slowmo-picker) 1/4x/1/2x/3/4x selector from the
  match highlights tab bar.

- Disabled the scoreboard scrim (.msb-scrim { display: none }). The two
  linear-gradients were tinting the top and bottom of the video for text
  legibility, but every scoreboard text element already carries its own
  text-shadow — the tint was showing as permanent shadows over portrait
  and landscape videos alike.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-10 00:19:18 +03:00
ghassan
2b5e480c9a Remove VS screen; fix cropper + sports edit save + headshot/logo mapping
- Remove VS intro from player and thumbnail VS card from match video cards.
  All references (`msb-vs*`, `initVsIntro`, `setVsImg`, `HAS_VS`, `$vs`,
  `$msbThumbVs`, `$hasMatchMeta`) are stripped from scoreboard partials,
  video-card component, and scoreboard script. Related CSS keyframes
  (`msbFadeIn/Out`, `msbDriftA/B`, `msbSweep`) removed.

- Image cropper (resources/views/components/image-cropper.blade.php):
  replace unreliable third-party Cropme integration with a DIY cropper.
  Pan/zoom/rotate wired directly with pointer + wheel events; on save,
  the visible viewport is rendered to an offscreen canvas at the configured
  output-width (with circular clip for `shape="circle"`). Also reparent the
  overlay to <body> on open to escape any transformed Bootstrap-modal
  ancestor's stacking context that clipped it.

- Sports match modal (resources/views/layouts/partials/sports-match-modal.blade.php):
  set fighter photo croppers to 3:4 portrait (285x380) to match the VS
  card's 228x304 frame; club logos + referee stay 1:1 square.

- SportsMatchController::fillFromRequest (line 174): coerce
  `$this->clean($request->input('media', []))` to `[]` when it returns null
  so array_merge doesn't crash under PHP 8's stricter types.

- SportsMatch::headerData(): read fighter headshots + club logos from the
  canonical media.* keys (participant1_photo / participant2_photo /
  club1_logo / club2_logo) that the uploader modal actually saves, with the
  older p1_* / p2_* names kept as a secondary fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-09 23:47:59 +03:00
ghassan
19741f489b Match scoreboard overlay: identity/ticker/scorebar/timeline + VS intro + gear-menu toggles
- Scoreboard overlay driven by video.currentTime (scores/ticker/timeline as pure
  functions of playback time; scrub-back removes points; slow-mo doesn't affect).
- Score bar + timeline + VS intro rendered verbatim from the design handoff
  (drafts/score-bar.html, drafts/vs-screen.html) — only {{ }} data holes filled.
- Fixed 1150 px design canvas scaled to any player size via ResizeObserver so
  the design's pixel-perfect measurements survive at every player width.
- Gear-menu injection adds a "Scoreboard" section with 7 toggle rows (Score bar,
  Live scoring feed, Match info, Point timeline, Club logos, Country flags,
  Penalties) using a custom 26x14 track/knob toggle matching the design.
  Persists per user in localStorage['msb_prefs']; penalties defaults off.
- VS card fades in on first play of a session (skippable by click/tap/key,
  2 s hold then 0.5 s fade); skipped entirely when no athlete metadata.
- SportsMatch::headerData() extended to expose sport, headshots, club logos,
  division, format, match_date and per-video scoreboard defaults from existing
  JSON columns (no schema migration).
- Every missing field hides its element (no "NOT SET" placeholders); athlete
  name falls back to RED/BLUE per brief.
- Pushed .replay-badge down from top:2.5cqi to top:8cqi so it clears the
  new KARATE KUMITE identity block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-09 22:09:04 +03:00
ghassan
1c4e4986b5 Frame-accurate scrubbing + mobile highlights UX + collapsed action menu
Frame-accurate timestamps (end-to-end)
- Migration 2026_08_08_000002 promotes match_rounds.start_time_seconds,
  match_points.timestamp_seconds, coach_reviews.start_time_seconds and
  coach_reviews.end_time_seconds from INTEGER to decimal(10,3).
- Model $casts updated (float). MatchEventController validators relaxed
  from `integer` to `numeric|min:0` on all four fields.
- Blade SSR data-time-{start,end} attributes cast to (float) so the
  first paint carries frame precision.
- Point + review capture strips: slider step = 1/fps, snapToFrame()
  applied to every seek/nudge/input commit. Clock now shows SMPTE
  MM:SS.FF; parsePcbTimeInput accepts MM:SS.FF, MM:SS, MM.SS or seconds.
- Default fps is 30; override via window.matchFPS.
- confirmPointCapture no longer Math.round()s currentTime — sends the
  exact frame boundary.

Point + review UX
- Video pauses aggressively on scrubber grab (mousedown/pointerdown/
  touchstart) AND on every input, with a 30ms follow-up to beat HLS.js
  race conditions.
- Zoom buttons now scope by data-pcs-zoom / data-rcb-zoom so clicks on
  the review-strip zoom don't clobber the point-strip zoom (root cause
  of the "resets while button still on 20x" bug).
- Point cards now play a preroll/postroll clip at 1× then replay at
  the chosen slow-mo rate, then resume normal playback.
- REPLAY ×N badge shown in the top-left of the video during replays,
  swapping palette when slow-mo kicks in.
- Slow-mo picker (¼× ½× ¾×) in the tab-header — window.slowmoRate is
  the source of truth for both point and review replays.
- Clicking a card hides the player chrome (controls-hidden re-applied
  across 0/10/60/200/500ms to beat pause/seek/play showControls races);
  mousemove over the video brings them back instantly.
- Review-tools slow-mo button removed — the card click IS the trigger.

Coach review specific
- Dual-thumb scrubber on a shared track with a red fill between the
  thumbs (visualises the note's range). Start clamps to ≤ end and
  vice-versa; nudges act on the last-touched thumb.
- Note overlay live-previews as user types and is drag-to-position;
  overlay position persists (position_x/position_y decimal(6,4)).
- Overlay uses container-query units (cqi) so text scales with the
  player width and position stays proportional on any resize.
- Timeline card design (chapter-style time label + subtle card with
  emoji, note, coach name, tools) — nothing else selected.

Sidebar
- "Rounds & points" / "Private notes" section labels removed; only
  the owner-only + Add Round / + Add Note button remains.
- Points that share a timestamp within a round collapse into ONE card
  with a chip per side (equal-width Blue/Red pills), one edit/delete
  pair, and a coloured score line (yellow ROUND N, blue/red score
  badges — background pills with white numerals).
- On mobile the highlights sheet only closes via its own toggle now.
  Backdrop is pointer-events: none so taps pass through to the video.

Mobile capture strip
- Renders below the video on mobile portrait so the paused frame is
  visible while typing. Wider inner padding (10px 20px 14px) so
  content doesn't sit on the edges.
- .pcs-entry uses flex-wrap: nowrap and Action input shrinks to
  fit — the row stays on one line at any width.

Collapsed action menu (video-actions component)
- Every viewport now shows the single  Action dropdown; individual
  desktop-action buttons are hidden by CSS across the board.

SPA navigation fixes
- window.videoId + isOwner now live on window so navigation to a new
  match video via Up Next can update them via reloadMatchVideoState.
- Tabs, event-item clicks, highlights toggle and sidebar-height sync
  re-hydrate on each SPA swap.
- Blade point-time regression: "{{ '@' . $fmtTime(...) }}" instead of
  the misused "@{{ ... }}" escape directive.

Backend
- Point-score recompute (recomputeRoundScores) already sums by
  (timestamp, id) order — works fine with the new decimal columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-09 02:26:19 +03:00
ghassan
cba8e01d56 Match videos: full re-hydration on Up Next SPA navigation
Fixes a raft of things breaking when jumping between match videos via the
Up Next / playlist sidebar without a page reload:

- Highlights toggle stopped working (button held a click handler pointing at
  the OLD, detached .events-sidebar node).
- Points / Coach review tabs went unclickable (new .tab-button elements had
  no bound handlers).
- Event-item clicks (jump-to-timestamp on point / review cards) stopped
  seeking / playing.
- window.matchRounds / window.matchReviews and const videoId were frozen at
  first page load, so edit/delete/save handlers routed to the wrong video.
- Blade point card leaked "{{ $fmtTime(...) }}" as literal text because of
  a misused @{{ }} escape directive.

Changes
- videoId + isOwner now live on window so they can be updated at runtime;
  `var videoId` at module scope keeps existing handlers working.
- Extracted tab-button + attachEventListeners wiring into
  window.initMatchTabsAndEvents (idempotent — _tabBound flag).
- Added window.reloadMatchVideoState(url): parses the new slug from the URL,
  updates window.videoId, calls loadMatchData (refreshes cache + re-renders
  sidebar), then re-runs the tabs/events/toggle/sidebar-height inits.
- recSwapContent and plSwapContent call reloadMatchVideoState whenever the
  target page has an .events-sidebar (falls back to just the toggle re-init
  for non-match targets).
- Fixed the Blade point-card time: {{ '@' . $fmtTime(...) }} instead of
  @{{ ... }} so the timestamp actually renders instead of showing braces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-08 07:20:50 +03:00
ghassan
716693a74f Coach reviews: in-player capture strip, draggable overlay, timeline UI
Capture strip
- Twin of the point-capture strip, embedded inside #ytpWrap. Two time slots
  (Start + optional End) with the scrubber driving whichever is "active".
  Editable mm:ss inputs, zoom (1x/5x/20x), inline note + coach name + emoji
  chip picker. Cancel / Save on the right; Delete in edit mode.
- ▶ preview button plays Start → End and auto-pauses at End; click again to
  stop early. Playback uses the live overlay so previewing shows what the
  viewer will see.
- Removed the #reviewModal popup; openAddReviewModal(), setReviewEmoji(),
  saveReview() and the reviewForm submit binding are gone.

Live-preview overlay + drag-to-position
- As the user types, the coach-note overlay updates on the video in real
  time. In capture mode the overlay is grab-able and draggable to any spot
  over the video (mouse + touch), with a red dashed outline + "↕ drag me"
  hint.
- Overlay position is stored as normalized 0..1 CENTER coords of the video
  wrap and applied via `left: X%; top: Y%; transform: translate(-50%,-50%)`
  so the overlay stays at the same relative spot on any resize / fullscreen.
- Font-size, padding and border-radius scale with the player width via CSS
  container queries (`container-type: inline-size` on #ytpWrap + `clamp(...cqi)`),
  so the note grows/shrinks proportionally.
- Draggable overlay lives above the strip (z-index 70 vs 60) so it can be
  dropped anywhere including over the strip.

Persistence (schema change)
- Migration 2026_08_08_000001_add_position_to_coach_reviews: adds nullable
  decimal(6,4) position_x / position_y to coach_reviews.
- CoachReview fillable + casts updated. store/updateReview validate
  position_x / position_y as numeric between 0 and 1 and persist them.
- showCoachNoteOverlay(text, pos) applies the saved position on playback.

Cards: timeline design (Option B from the mockup)
- Vertical spine with red dots on the left; each review is a chapter-style
  time label above a subtle card. Card body: emoji as the leading indicator,
  note text, then coach name + tools (▶ / ✏️ / 🗑).
- ▶ = playReviewSlowmo(id): plays Start → End once at 1×, then loops back
  and plays again at 0.5×, then pauses and hides the overlay. Only rendered
  when the review has an end_time > start_time.
- Card body still clickable for the classic "jump-and-play-once" behaviour.
- Fixes a latent bug where the primary Save button stayed disabled after a
  successful save, silently swallowing the next click. Both the review and
  point capture strips now re-enable the button in their cancel handlers.

Also
- Design mockup file at public/coach-review-mockups.html shows the four
  candidate layouts side by side (kept for future design iterations).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-08 07:07:03 +03:00
ghassan
402c6dc89f Point capture in the player: inline strip, grouped entries, correct scores
Replaces the point Add/Edit popup with an in-player capture strip and fixes
the underlying running-score bug.

Backend (MatchEventController)
- storePoint used pluck('points','competitor'), which collapses same-side
  points into a single row (last value wins) and excluded same-timestamp
  points (< instead of <=). Same-moment Blue+Red produced 1-0 / 0-1 instead
  of 1-1. Replaced with recomputeRoundScores($roundId), a single pass over
  the round ordered by (timestamp_seconds, id) that walks a running total
  and updates each row. Called after create/update/delete so edits ripple.

Player-embedded capture strip (match.blade.php)
- Adding / editing points no longer opens a modal; the video's chrome
  bottom-bar is swapped for a strip inside #ytpWrap that survives fullscreen.
- Editable mm:ss field + draggable red marker + −1s/+1s nudges. Zoom control
  (1x / 5x / 20x) narrows the slider window around the current time for
  frame-level positioning; nudges scale with zoom. Auto-recentre on edge.
- Inline form: Blue / Red / Both toggle (equal-width segments), action text,
  points number. Both mode splits into two per-side rows. Values carry over
  when switching modes so nothing is retyped.
- Enter saves, Esc cancels. Save button auto-disables during in-flight
  requests. On mobile the buttons collapse to icon-only (× / ✓).

Grouped highlights entry
- Points sharing a timestamp within a round collapse into ONE card with a
  chip per side (equal-width Blue/Red pills), a single ✏️/🗑 pair, and a
  meta line coloured by role: ROUND N in amber, blue score in blue, red
  score in red.
- ✏️ on a grouped entry opens the strip in Both mode with both sides
  pre-filled. Saving PUTs both rows; switching to a single competitor
  keeps that side and deletes the other.
- 🗑 on a grouped entry deletes both rows via the shared custom-confirm.
- loadMatchData now writes back to window.matchRounds so subsequent
  edit/lookup handlers find newly-added points without a page reload.

Removed
- #pointModal HTML block and the openAddPointModal / savePoint /
  confirmDeletePoint helpers (superseded by the capture strip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-08 05:31:26 +03:00
ghassan
ad6cbaf742 Match highlights: DB-backed rounds/points, draggable point capture, sports sheet Edit
Match highlights sidebar (videos/types/match.blade.php)
- Replaced hardcoded 3-round/5-point/3-review mock with server-rendered loops
  over $video->matchRounds (with points) and $video->coachReviews. Owner-only
  buttons carry real DB ids to the existing JS handlers.
- Fixed savePoint create branch: was doing parseInt(rawMmSs) which truncated
  "1.15" -> 1 second. Now sends the parsed value that edit already used.
- Added a point-capture bar: clicking + on a round pauses the video and shows a
  bottom bar with a big draggable red marker + nudge buttons. Dragging seeks
  the video live; Confirm reads video.currentTime and opens the Add Point modal
  with mm.ss pre-filled and Action focused. Fixed both server- and JS-rendered
  round buttons so dynamic reloads keep the new flow.

Edit → sports sheet routing
- Added sports_match_id to /videos/{video}/edit JSON.
- openEditVideoModal delegates type=match videos to openSportsMatchModal —
  edit mode when a SportsMatch is linked, create-with-attached-video mode
  when the video is match-typed but has no SportsMatch row yet.
- Sports sheet gained attachExistingVideo() so create-mode can be seeded with
  a video that's already in the library (skips the upload dropzone).

Upload chooser mobile bottom-nav
- Bottom nav "Upload" now opens the type chooser (bottom sheet on mobile,
  centred modal on desktop) instead of hard-navigating to /videos/create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-08 04:01:06 +03:00
ghassan
76f5ef0c84 Fix match-video playback: HEVC → H.264 transcode + correct HLS URL
- CompressVideoJob: rewrite to exec ffmpeg directly. php-ffmpeg's X264
  format class validates the codec name and only accepts libx264, so
  every job dispatched with h264_nvenc had been throwing within ~1s and
  leaving the source untouched. The job now honours the GPU encoder
  settings, and always replaces the source when the codec is HEVC/H.265
  so downloads and the MP4 fallback stay playable.
- VideoController + video-player component: point HLS URLs at
  playlist.m3u8 (the actual master file GenerateHlsJob writes) instead
  of the non-existent master.m3u8, which was 404-ing and forcing the
  player back to the raw MP4.
- VideoController@store: on the NAS-success branch, flip status to
  'ready' and populate duration/is_shorts before dispatching
  GenerateHlsJob, so the job no longer silently no-ops.
- Upload chooser: render as a bottom sheet on mobile, and forward the
  picked type to /videos/create?type= so the create page pre-selects it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-08 02:08:25 +03:00
ghassan
da714958c6 Scrollable, SPA-navigated filter bar with all languages
Removes the 12-chip cap on the language filter so every language that
appears in public content is listed (video 175 alone contributes 12).

The bar wrapper is now sticky and horizontally scrollable with chevron
buttons that appear only when the bar overflows in that direction, a
gradient fade behind each button, vertical-wheel → horizontal scroll
that hands off to the page at the edges, and sessionStorage-persisted
scroll position so navigation doesn't snap it back to the start.

Chip clicks are intercepted (only for /videos URLs; Trending/Shorts
still render different templates via a normal navigation) and swap
only the #filter-content region via fetch + DOMParser, updating
history.pushState, document.title, and active-chip state without a
full page reload. Modifier-clicks and middle-clicks fall through
to native "open in new tab" behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-31 03:51:45 +03:00
ghassan
fcb752dd76 Language filter chips on the videos feed
Adds one chip per language currently present in public content —
counting both primary language and any secondary audio track, sorted
by video count, capped at 12. Clicking a chip lists every video whose
primary is that language or that has a matching secondary track; cards
surfaced only via a secondary track link straight to /videos/{key}?track=<id>
so the pinned playback wiring loads the right audio, get a small flag
badge on the thumbnail, and their hover-preview streams the track file
instead of the primary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-31 03:27:42 +03:00
ghassan
8d27520bdb Video merge feature, default avatar, video-redirects table, misc UI tweaks
Bundled pre-existing changes on the branch:
- Video merge: controller, service, admin merge modal, and a repair
  console command; new video_redirects table (with source_folder)
  so merged/moved videos keep resolving under their old keys.
- Default avatar SVG for users without a profile picture.
- Assorted view tweaks across profile, video actions/comments/insights,
  admin layout, video-details, and the app layout.
- Small touch-ups in SuperAdminController, User model, NasSyncService.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-31 02:42:43 +03:00
ghassan
ba8ab11ce4 Chunked video upload, persistent track order, per-track playlist entries
Chunked uploads: bypass Cloudflare's 100 MB request cap by slicing the
video/audio into 8 MB chunks and reassembling server-side before the
normal store flow runs.

Track reorder: audio tracks gain a `position` column so a user's edit-
modal ordering survives reload. `Video::audioTracks()` now orders by
position, `promote_track_id` remembers the swapped row, and freshly
uploaded tracks append to the end.

Playlist per-track pinning: `playlist_videos.audio_track_id` remembers
which language the user pinned when adding a song. Same song with a
different track becomes a separate row (Spotify-style duplicates).
Player-data exposes `active_stream_url` + `active_track_id`, sidebar
cards render `?track=` from the pivot, and both `plTransitionTo` and
initial-load code activate the corresponding language option.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-31 02:42:25 +03:00
ghassan
af276f2cd1 Wire match header to real SportsMatch data; add Match#/Court + country flags
The match page header rendered hard-coded demo content (Sami Al Manea vs Hassan
Al Shallan, Match# 103, referee Ghassan Yusuf, external takeone-dev links) on
EVERY match video, ignoring the SportsMatch record the create form saves.

- Video::sportsMatch() hasOne relationship.
- SportsMatch::headerData() maps the record into the header's shape (fighters +
  country flags, championship, weight class, referee, venue, match#/court),
  null-safe, with a country-name/ISO2 -> flag-icons resolver.
- match.blade header now renders from headerData(): real names/flags, no
  external links, and degrades gracefully to just title/views/date when a video
  has no match data (instead of the fake card).
- sports-match-modal: add Match # and Court fields (stored in the existing
  competition JSON, no migration) and convert fighter/referee country inputs to
  the mandated <x-country-select> so header flags resolve from ISO2.
- Tracker updated for the new country-select usages.

Verified: empty state (no SportsMatch) shows only title/views/date; a populated
record renders fighters, flags (bh/us), championship, Match# 42, Court 3, U21,
referee; modal compiles with the new fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 01:57:07 +03:00
ghassan
0f3110f59d Fullscreen drawer: squeeze the video instead of overlaying it
Better layout: when the drawer opens in fullscreen, shrink the player's inner
.ytp container to calc(100% - drawer width) so the video sits in the remaining
left area and the drawer owns the right — no overlap, both fully visible side
by side. Because controls, the coach caption and the Highlights button all live
inside .ytp, they move with the video automatically, so the previous overlay
compensations (control/gradient shift, caption re-anchor, button reposition)
are dropped. Drawer is now a clean solid panel (var(--bg-secondary)) since the
video is no longer behind it; blurred-glass background + @supports fallback
removed. Animated via a width transition on .ytp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 23:24:36 +03:00
ghassan
4cd1d195a7 Fix fullscreen drawer stretching full-width over the video
On a <=991px landscape phone the mobile sheet rule (width:100% !important) beat
the fullscreen drawer's non-important width, so the drawer covered the whole
video instead of just the right side. Make the drawer width !important so it
keeps its narrow clamp(230px, 30vw, 360px).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 23:18:50 +03:00
ghassan
f8cefc1bf9 Match highlights: fullscreen landscape drawer (narrow + clear glass)
Native fullscreen paints only #ytpWrap and its descendants, so the Highlights
pane was invisible in fullscreen. On fullscreenchange the pane is now relocated
INTO the player (and moved back on exit), and CSS reshapes it into a narrow,
transparent right-side drawer:

- Blurred-glass background (Clear) + text shadows so it barely covers the match
  while staying readable; @supports fallback firms up the fill where
  backdrop-filter is unsupported.
- Width clamp(230px, 30vw, 360px) — the Narrow choice, usable on any screen.
- While open: player controls + gradient pull in to the video area (scrubber
  never hidden), coach caption stays bottom-left, and the Highlights button
  slides to the drawer's left edge so it stays tappable to close.
- Same pane/data/tabs/CRUD; windowed bottom-sheet behavior unchanged.

Note: iPhone Safari forces native <video> fullscreen (no custom UI), so the
drawer applies where element fullscreen is supported (Android, desktop, iPad).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 23:16:50 +03:00
ghassan
cffd8491d0 Match highlights sheet: flush to player bottom + non-blocking coach caption
- Sheet top now pins to the player's exact bottom edge (JS measures #ytpWrap
  height into --hl-sheet-top; 56.25vw fallback), so no title/actions row shows
  between the video and the sheet. Recomputes on open and on resize/rotate.
- Coach-note overlay on mobile is now a slim 2-line caption pinned to the very
  bottom of the player instead of a 16px block mid-frame, so it no longer
  covers the video. Full note stays readable in the sheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 22:56:33 +03:00
ghassan
ea95a60d79 Match highlights sheet: hide bottom nav on open too
While the sheet is open the fixed bottom nav now slides down out of view,
matching the top bar so the sheet owns the whole lower area; it slides back
the moment the sheet closes. Same body.hl-sheet-open trigger, reduced-motion
safe. The !important is needed to beat the base rule that pins the nav's
transform to none on mobile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 22:45:55 +03:00
ghassan
ecb89f9514 Match highlights sheet: full-brightness video + hide top bar on open
Two mobile refinements to the bottom sheet:
- Drop the dark scrim (was 32% black over the whole screen). The backdrop is
  now a transparent tap-to-dismiss catcher, so the match video keeps full
  brightness and detail while the sheet is open.
- While the sheet is open, slide the fixed top bar up and float the scroll
  area to top:0 so the player gets the reclaimed 56px. Both revert the instant
  the sheet closes. Driven by a body.hl-sheet-open class; reduced-motion safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 22:42:05 +03:00
ghassan
18fb4fbd63 Match highlights: mobile bottom sheet (Option A)
On mobile (<=991px) the Points/Coach-review pane no longer stacks below the
comments. Tapping "Highlights" now slides it up as a bottom sheet over the
lower ~62vh, and scrolls the player back to the top so the user can watch and
read at once. Includes a drag handle, a light tap-to-dismiss scrim, and
internal scrolling. Desktop keeps its side pane exactly as before — all new
CSS is inside the max-width:991px media query.

- Sheet only restores its open state on desktop; on mobile it stays closed on
  load so it never pops open unexpectedly.
- Scrim sits above the fixed bottom nav (z-index 1400 > 1000).
- Honors prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 22:35:04 +03:00
ghassan
6b43a7060a Fix player UI: sports Highlights pane, scrubber knob, volume knob, description + save-to-playlist
Player/UI fixes:
- Sports (match) Highlights pane: move the toggle click-handler script out of
  the always-false @if(false ...) legacy block so the "Highlights" button opens,
  and emit videoId/isOwner with @json instead of {{ json_encode() }} (escaped
  quotes crashed the data/tab/CRUD block once route keys became hashid strings).
- Progress scrubber knob (video + audio players): center on the track line with
  translate(-50%, -50%); drop the margin-top hack that floated it above the line.
- Music player volume: give the slider wrap height so overflow:hidden no longer
  crops the knob; add a subtle thumb shadow for visibility.
- Music language menu: cap height to available space above the header, sticky
  header, internal scroll; keep controls from auto-hiding while the menu is open.
- Description box: re-check "Show more" overflow at every settle point (fonts,
  images, rAF, tab reopen) so SPA autonext keeps the expand button.

Also bundles pending work: save-to-playlist button component, playlist/channel
views, PlaylistController + NasSyncService, and component-usage tracker updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 22:12:58 +03:00
ghassan
f98e5415a3 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 <noreply@anthropic.com>
2026-05-31 22:01:47 +03:00
ghassan
73527f3781 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
2026-05-29 01:50:28 +03:00
ghassan
6aae6f86b6 Add upload type chooser and redesign upload modal
Clicking Create now opens a card-based chooser (Generic / Music / Sports)
before the upload modal; the chosen type is applied and its Content Type
dropdown is hidden as redundant.

Per type:
- Generic/Match show their fields inline in the modal (no card/popup);
  Music keeps the track-card + Track Editor popup for multi-language tracks.
- "Language Track" wording stays music-only; a single Language field is now
  available for generic/match too (mirrored on the mobile create page with
  name-swapping so only the active picker submits).

Also unifies all modal controls (dropdowns, selects, inputs) to one larger,
red-accented dark style scoped to #uploadModal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 14:12:08 +03:00
ghassan
a4384113c2 Audio songs: one-folder storage, version-aware download/share, GPU-checked renders
Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
  unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
  subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
  updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).

Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
  is carried through share links and auto-selects audio + title + flag + about +
  OG/meta on open.

GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
  256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
  frequency bars; loop-filter rebuild makes generation ~25x faster.

Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:03:43 +03:00
ghassan
66fd78c10f Add multi-language audio tracks and self-hosted flag-icons
Introduce per-video language support and multiple audio tracks
(VideoAudioTrack model + migrations for language, description, title),
a reusable language-select component, and a track-editor form. Bundle
the self-hosted flag-icons v7.2.3 library and a NAS auto-sync command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:32:52 +03:00
ghassan
4887d0c517 Lock screen to landscape on fullscreen, unlock on exit
On mobile, entering fullscreen now also locks the screen orientation to
landscape via the Screen Orientation API. Exiting fullscreen unlocks it,
allowing the device to return to portrait. Applied to both the video
player and audio player. Gracefully ignored on browsers that don't
support screen.orientation.lock (e.g. iOS Safari).

Also includes the playlist auto-scroll fix (committed separately).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:14:28 +03:00
ghassan
6e7d5d178a Fix SPA transitions: title, cover image, and slideshow not updating
Three bugs in the Up Next / playlist SPA transition for music-type videos:
1. Title selector was '.audio-title' (doesn't exist) instead of '.video-title span'
2. Cover image only updated #audioCoverImg — missed when video has a slideshow
3. Slideshow SLIDE_URLS lived in a closed IIFE and couldn't be updated cross-video

Fix: always render both #audioCoverImg and #slideshowWrap in the DOM (toggle
display via inline style), hoist slideshow state variables outside the if-block,
and expose window._audioPlayerUpdate(d) that both recTransitionTo and
plTransitionTo call. The hook handles all cases: cover-to-cover, cover-to-slideshow,
slideshow-to-cover, slideshow-to-slideshow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:02:21 +03:00
ghassan
4f275de15f SPA transitions + autoplay for match type; add no-refresh rule to CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:45:16 +03:00
ghassan
5960c6e7b1 Add Up Next autoplay controls + SPA transitions to music type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:35:41 +03:00
ghassan
d73f877d18 Add SPA autoplay + no-refresh clicks to Up Next recommendations
- Autoplay toggle button in the Up Next header (defaults Off, persisted
  in localStorage as ytpAutoplay_solo)
- When autoplay is On and video ends (_plOnVideoEnd hook), automatically
  loads the first recommended video via SPA — no page refresh
- Clicking any recommended video uses recGoTo() → recTransitionTo()
  instead of window.location.href: swaps video source, resets progress
  bar, updates title, then background-swaps description, channel row,
  comments, and the entire sidebar with fresh recommendations from the
  next page
- First card gets a red ▶ indicator while autoplay is On so the user
  can see what will play next
- Browser back/forward work via popstate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:29:01 +03:00
ghassan
77e7b950be SPA playlist transitions for generic (video) type
- Add _ytpLoadSource(hlsUrl, mp4Url) to video-player component:
  destroys old HLS instance, creates a new one with the new source,
  then plays — browser retains autoplay permission since the <video>
  element never leaves the page
- Add _ytpNavOverride hook: playlist overlay can replace navigateNext/
  navigatePrev without modifying the component internals
- Add _plOnVideoEnd hook to 'ended' handler so the playlist overlay
  can control autoplay/loop behavior independently
- Expose window._ytpHls for HLS instance lifecycle management
- Add hls_url + has_hls to /videos/{video}/player-data JSON endpoint
- Replace generic.blade.php playlist controls with full SPA system
  identical in structure to music type: plTransitionTo, plSwapContent,
  plAdj, plRender, plHighlight — no page refresh on track change
- Sidebar shows all playlist tracks; current track highlighted in red

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:16:25 +03:00
ghassan
da02425aeb SPA playlist transitions — no page refresh on track change
- Add GET /videos/{video}/player-data JSON endpoint returning stream URL,
  cover, slides, title, duration (used by client-side SPA transitions)
- Replace music playlist JS with full SPA system: plTransitionTo() swaps
  audio.src in-place (preserving browser autoplay permission), updates
  cover art, resets progress bar, then background-fetches the new page
  to swap #vdbWrap (description) and #ytcSection (comments) via DOMParser
- plSwapContent() re-runs the YTC comments IIFE after swapping innerHTML
  so comments load correctly for the new video
- Prev/next/shuffle/loop/autoplay controls now computed dynamically from
  PL_VIDEOS array — buttons stay correct after each SPA transition
- Sidebar shows ALL playlist tracks (removed @if filter); current track
  highlighted in red; clicking any card triggers SPA transition
- Browser back/forward handled via popstate + history.pushState

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 12:12:22 +03:00
ghassan
99f71c54e5 Fix playlist controls: add to type-specific views (music, generic, match)
Controls were only added to show.blade.php, but music/generic/match videos
render their own complete layouts with their own sidebars. Added the
pl-controls-bar to all three type views and the global CSS to app.blade.php.

- music: full standalone JS with shuffle/loop/autoplay + _plOnTrackEnd hook
- generic/match: syncs with video-player's existing ytpShuffleRow/ytpAutoplayRow toggles
- audio-player: ended handler now calls window._plOnTrackEnd if defined
- video-player: exposes window._ytpNav.next/prev for sidebar prev/next buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:45:33 +03:00
ghassan
05db0e128a Add playlist controls: prev/next, shuffle, loop, autoplay toggle
Controls bar added to playlist sidebar header with:
- Prev/Next skip buttons (disabled when at bounds)
- Shuffle toggle (Fisher-Yates order stored in localStorage)
- Loop 3-state: off → loop all → loop one
- Autoplay toggle (default on, persists per playlist in localStorage)
All state is instant — no page reload on toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:16:42 +03:00
ghassan
c160242dbc WIP: storage-fix-local-nas work before playlist controls feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:15:20 +03:00
ghassan
d1441b213a Fix progress bar seek pausing playback + add persistent mini-player
- video-player: add userSeeking flag so mid-seek pause events are
  suppressed; force-resume on 'seeked' if video was playing before seek;
  guard player click handler against progCont clicks; e.preventDefault()
  on touchend to stop synthetic click toggling play
- audio-player: apply identical seek fixes (same four changes)
- app layout: add floating mini-player that saves video state to
  sessionStorage when bottom nav is tapped while a video is playing,
  then restores playback on the next page via a floating overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 01:45:20 +03:00
ghassan
0b2e95ea65 Add NAS file manager integration and all pending platform changes
- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
  settings panel, country/phone/timezone components, posts, slideshow,
  playlist shares, video downloads/shares, comment likes, notifications,
  social links, and more

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:24:32 +03:00
ghassan
2a562b99f1 latest update 2026-04-05 03:30:22 +03:00
ghassan
64eadfaf56 converted comment section and the cannel info and the action buttons to components 2026-03-21 03:22:30 +03:00
ghassan
84fcbd84dc latest update 2026-03-21 02:24:27 +03:00
ghassan
3b09f4baed all is working great 2026-03-15 04:55:18 +03:00
ghassan
f850f40f78 all is working great 2026-03-15 04:06:35 +03:00
ghassan
69f5df163a update the match view 2026-03-12 03:59:52 +03:00
ghassan
062c0e896f latest update 2026-03-11 11:21:33 +03:00
ghassan
9ad842dcd5 Add trending videos page with YouTube-style algorithm
- Trending algorithm based on:
  - Recent views (last 48h): 70% weight
  - View velocity (views/hour): 15% weight
  - Recency bonus: 10% weight
  - Like count: 5% weight
- Excludes videos older than 10 days
- Filter options: Today/This Week/This Month
- Added route, controller method, and view
- Updated nav to link to trending page
2026-03-03 21:30:44 +03:00