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 <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.8 committed 2026-07-28 22:12:58 +03:00
1 parent 80948efff7
commit 6b43a7060a
14 files changed
+236 -136

No files matched your search

+33 -21
View File
@@ -257,23 +257,19 @@ class PlaylistController extends Controller
'visibility' => $request->visibility ?? 'private',
];
// Handle thumbnail upload
// Thumbnail: an uploaded file always wins over remove_thumbnail. The
// modal sometimes sends both when a user clears the preview and then
// picks a new file — without this guard the new file would be silently
// discarded.
if ($request->hasFile('thumbnail')) {
// Delete old thumbnail from NAS if exists
if ($playlist->thumbnail) {
self::deletePlaylistThumbnailFromNas($playlist->thumbnail);
}
$file = $request->file('thumbnail');
$updateData['thumbnail'] = self::pushPlaylistThumbnailToNas($file, $playlist);
}
// Handle thumbnail removal
if ($request->input('remove_thumbnail') == '1') {
if ($playlist->thumbnail) {
self::deletePlaylistThumbnailFromNas($playlist->thumbnail);
$updateData['thumbnail'] = null;
}
} elseif ($request->input('remove_thumbnail') == '1' && $playlist->thumbnail) {
self::deletePlaylistThumbnailFromNas($playlist->thumbnail);
$updateData['thumbnail'] = null;
}
$playlist->update($updateData);
@@ -519,16 +515,32 @@ class PlaylistController extends Controller
private static function pushPlaylistThumbnailToNas(\Illuminate\Http\UploadedFile $file, Playlist $playlist): string
{
$nas = app(\App\Services\NasSyncService::class);
$ext = $file->getClientOriginalExtension() ?: 'jpg';
$tmpName = self::generateFilename($ext);
$file->storeAs('public/thumbnails', $tmpName);
$tempAbs = storage_path('app/public/thumbnails/' . $tmpName);
$nasPath = self::nasPlaylistThumbPath($playlist, $ext);
$dir = dirname($nasPath);
$nas->mkdirp($dir);
$nas->putFile($tempAbs, $nasPath);
@unlink($tempAbs);
$nas = app(\App\Services\NasSyncService::class);
$ext = $file->getClientOriginalExtension() ?: 'jpg';
$nasPath = self::nasPlaylistThumbPath($playlist, $ext);
$localAbs = storage_path('app/' . $nasPath);
// When NAS is reachable, push directly and avoid the slow timeout path.
if ($nas->isEnabled()) {
$tmpName = self::generateFilename($ext);
$file->storeAs('public/thumbnails', $tmpName);
$tempAbs = storage_path('app/public/thumbnails/' . $tmpName);
$nas->mkdirp(dirname($nasPath));
if ($nas->putFile($tempAbs, $nasPath)) {
@unlink($tempAbs);
return $nasPath;
}
// NAS push failed mid-flight — fall through to local fallback so the
// file isn't lost. nas:auto-sync will retry when NAS comes back.
@mkdir(dirname($localAbs), 0755, true);
@rename($tempAbs, $localAbs);
return $nasPath;
}
// NAS disabled / unreachable — keep on local disk at the same path
// MediaController expects, so it serves directly without a NAS round-trip.
@mkdir(dirname($localAbs), 0755, true);
$file->move(dirname($localAbs), basename($localAbs));
return $nasPath;
}
+9 -10
View File
@@ -997,12 +997,11 @@ class VideoController extends Controller
$data['thumbnail'] = "{$nasDir}/thumb.{$ext}";
}
} else {
// NAS disabled — keep on local disk
$localDir = $nas->localVideoDir($video);
// NAS disabled — keep on local disk, mirroring the NAS path
$nasDir = $this->nasVideoDir($video, $nas);
$localDir = storage_path('app/' . $nasDir);
@mkdir($localDir, 0755, true);
$request->file('thumbnail')->move($localDir, "thumb.{$ext}");
$userSlug = $nas->userSlug($video->user);
$nasDir = $this->nasVideoDir($video, $nas);
$data['thumbnail'] = "{$nasDir}/thumb.{$ext}";
}
}
@@ -1041,13 +1040,13 @@ class VideoController extends Controller
$nasForSlides = app(\App\Services\NasSyncService::class);
$nasEnabled = $nasForSlides->isEnabled();
// Derive NAS dir without scanning when possible
$nasDir = $nasEnabled ? $this->nasVideoDir($video, $nasForSlides) : null;
// Local dir used as fallback or when NAS is off
$localDir = $nasForSlides->localVideoDir($video);
$userSlug = $nasForSlides->userSlug($video->user);
// Derive NAS dir without scanning — works regardless of NAS state
$relBase = $this->nasVideoDir($video, $nasForSlides);
$nasDir = $nasEnabled ? $relBase : null;
// Local dir mirrors the NAS path so MediaController can find files
// whether NAS is reachable or not.
$localDir = storage_path('app/' . $relBase);
if ($nasEnabled) {
$nasForSlides->mkdirp("{$nasDir}/slides");
+10
View File
@@ -490,6 +490,12 @@ class NasSyncService
{
if (file_exists($localPath)) return true;
// Don't shell out to smbclient when we already know NAS is unreachable
// — smbclient hangs ~30 sec on the SMB timeout, multiplied by every
// thumbnail/avatar on the page. isEnabled() uses a 2-sec TCP probe
// cached for 120 sec, so this fails fast during an outage.
if (! $this->isEnabled()) return false;
$dir = dirname($localPath);
if (! is_dir($dir)) @mkdir($dir, 0755, true);
@@ -1015,6 +1021,7 @@ class NasSyncService
public function mkdirp(string $path): void
{
if (! $this->isEnabled()) return; // fail-fast when NAS unreachable
$cfg = $this->cfg();
$target = escapeshellarg($this->smbTarget($cfg));
$cred = escapeshellarg($this->smbCredential($cfg));
@@ -1033,6 +1040,7 @@ class NasSyncService
public function putFile(string $localAbsPath, string $nasRelPath): bool
{
if (! $this->isEnabled()) return false; // fail-fast when NAS unreachable
$cfg = $this->cfg();
$target = escapeshellarg($this->smbTarget($cfg));
$cred = escapeshellarg($this->smbCredential($cfg));
@@ -1058,6 +1066,7 @@ class NasSyncService
public function getContent(string $nasRelPath): ?string
{
if (! $this->isEnabled()) return null; // fail-fast when NAS unreachable
$cfg = $this->cfg();
$target = escapeshellarg($this->smbTarget($cfg));
$cred = escapeshellarg($this->smbCredential($cfg));
@@ -1202,6 +1211,7 @@ class NasSyncService
public function deleteFile(string $nasRelPath): void
{
if (! $this->isEnabled()) return; // fail-fast when NAS unreachable
$cfg = $this->cfg();
$target = escapeshellarg($this->smbTarget($cfg));
$cred = escapeshellarg($this->smbCredential($cfg));