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

365 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('admin.layout')
@section('title', 'Videos')
@php
$adminNeedsOtp = auth()->user()->two_factor_enabled &&
(! session('admin_2fa_verified_at') || now()->timestamp - session('admin_2fa_verified_at') >= 1800);
@endphp
@section('content')
{{-- ── Page header ──────────────────────────────────────────────────── --}}
<div class="adm-page-header">
<h1 class="adm-page-title"><i class="bi bi-play-circle"></i> Videos</h1>
</div>
{{-- ── Alerts ───────────────────────────────────────────────────────── --}}
@if(session('success'))
<div class="adm-alert adm-alert-success">
<i class="bi bi-check-circle-fill"></i>
<span>{{ session('success') }}</span>
<button class="adm-alert-close"><i class="bi bi-x"></i></button>
</div>
@endif
@if(session('error'))
<div class="adm-alert adm-alert-error">
<i class="bi bi-exclamation-circle-fill"></i>
<span>{{ session('error') }}</span>
<button class="adm-alert-close"><i class="bi bi-x"></i></button>
</div>
@endif
{{-- ── Filter card ──────────────────────────────────────────────────── --}}
<div class="adm-card">
<div class="adm-card-body" style="padding:16px 20px;">
<form method="GET" action="{{ route('admin.videos') }}" class="adm-filter-form">
<div class="adm-filter-search">
<i class="bi bi-search"></i>
<input type="text" name="search" class="adm-input"
placeholder="Search title or description…"
value="{{ request('search') }}" autocomplete="off">
</div>
<select name="status" class="adm-select">
<option value="">All Status</option>
<option value="ready" {{ request('status') === 'ready' ? 'selected' : '' }}>Ready</option>
<option value="processing" {{ request('status') === 'processing' ? 'selected' : '' }}>Processing</option>
<option value="pending" {{ request('status') === 'pending' ? 'selected' : '' }}>Pending</option>
<option value="failed" {{ request('status') === 'failed' ? 'selected' : '' }}>Failed</option>
</select>
<select name="visibility" class="adm-select">
<option value="">All Visibility</option>
<option value="public" {{ request('visibility') === 'public' ? 'selected' : '' }}>Public</option>
<option value="unlisted" {{ request('visibility') === 'unlisted' ? 'selected' : '' }}>Unlisted</option>
<option value="private" {{ request('visibility') === 'private' ? 'selected' : '' }}>Private</option>
</select>
<select name="type" class="adm-select">
<option value="">All Types</option>
<option value="generic" {{ request('type') === 'generic' ? 'selected' : '' }}>Generic</option>
<option value="music" {{ request('type') === 'music' ? 'selected' : '' }}>Music</option>
<option value="match" {{ request('type') === 'match' ? 'selected' : '' }}>Match</option>
</select>
<select name="sort" class="adm-select">
<option value="latest" {{ request('sort', 'latest') === 'latest' ? 'selected' : '' }}>Newest first</option>
<option value="oldest" {{ request('sort') === 'oldest' ? 'selected' : '' }}>Oldest first</option>
<option value="title_asc" {{ request('sort') === 'title_asc' ? 'selected' : '' }}>Title AZ</option>
<option value="title_desc" {{ request('sort') === 'title_desc' ? 'selected' : '' }}>Title ZA</option>
</select>
<button type="submit" class="adm-btn adm-btn-primary">
<i class="bi bi-funnel"></i> Filter
</button>
@if(request()->hasAny(['search','status','visibility','type','sort']))
<a href="{{ route('admin.videos') }}" class="adm-btn">
<i class="bi bi-x-lg"></i> Clear
</a>
@endif
</form>
</div>
</div>
{{-- ── Videos table ─────────────────────────────────────────────────── --}}
<div class="adm-card">
<div class="adm-card-header">
<div class="adm-card-title">
<i class="bi bi-play-circle"></i>
All Videos
<span class="adm-badge adm-badge-user">{{ $videos->total() ?? $videos->count() }}</span>
</div>
</div>
<div class="adm-table-wrap">
<table class="adm-table">
<thead>
<tr>
<th>Video</th>
<th>Owner</th>
<th>Status</th>
<th>Visibility</th>
<th>Type</th>
<th>Views</th>
<th>Likes</th>
<th>Shares</th>
<th>Uploaded</th>
<th style="width:110px; text-align:right;">Actions</th>
</tr>
</thead>
<tbody>
@forelse($videos as $video)
<tr style="cursor:pointer;" onclick="window.open('{{ route('videos.show', $video) }}', '_blank')">
{{-- Thumbnail + title --}}
<td>
<div style="display:flex; align-items:center; gap:12px;">
@if($video->thumbnail)
<img src="{{ route('media.thumbnail', $video->thumbnail) }}"
alt="" style="width:72px;height:44px;object-fit:cover;border-radius:6px;flex-shrink:0;border:1px solid var(--border);">
@else
<div style="width:72px;height:44px;border-radius:6px;background:var(--bg-card2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-play-circle" style="color:var(--text-3);font-size:18px;"></i>
</div>
@endif
<div style="min-width:0;">
<div style="font-weight:500;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;">
{{ $video->title }}
</div>
@if($video->description)
<div style="font-size:11px;color:var(--text-2);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;">
{{ Str::limit($video->description, 55) }}
</div>
@endif
</div>
</div>
</td>
{{-- Owner --}}
<td onclick="event.stopPropagation()">
<a href="{{ route('channel', $video->user->channel) }}" target="_blank"
style="font-size:13px;color:var(--text);text-decoration:none;display:flex;align-items:center;gap:4px;">
{{ $video->user->name }}
<i class="bi bi-box-arrow-up-right" style="font-size:10px;opacity:.4;"></i>
</a>
</td>
{{-- Status --}}
<td>
@php
$statusMap = [
'ready' => ['adm-badge-ready', 'bi-check-circle-fill', 'Ready'],
'processing' => ['adm-badge-unlisted','bi-arrow-repeat', 'Processing'],
'pending' => ['adm-badge-unverified','bi-clock', 'Pending'],
'failed' => ['adm-badge-superadmin','bi-x-circle-fill', 'Failed'],
];
[$cls, $ico, $lbl] = $statusMap[$video->status] ?? ['adm-badge-user','bi-dash','Unknown'];
@endphp
<span class="adm-badge {{ $cls }}"><i class="bi {{ $ico }}"></i> {{ $lbl }}</span>
</td>
{{-- Visibility --}}
<td>
@php
$visMap = [
'public' => ['adm-badge-ready', 'bi-globe2', 'Public'],
'unlisted' => ['adm-badge-unlisted','bi-link-45deg', 'Unlisted'],
'private' => ['adm-badge-private', 'bi-lock-fill', 'Private'],
];
[$vcls, $vico, $vlbl] = $visMap[$video->visibility] ?? ['adm-badge-user','bi-question','—'];
@endphp
<span class="adm-badge {{ $vcls }}"><i class="bi {{ $vico }}"></i> {{ $vlbl }}</span>
</td>
{{-- Type --}}
<td>
@php
$typeMap = [
'generic' => ['bi-play-circle','#94a3b8'],
'music' => ['bi-music-note-beamed','#a78bfa'],
'match' => ['bi-trophy','#fbbf24'],
];
[$tico, $tcol] = $typeMap[$video->type] ?? ['bi-play-circle','#94a3b8'];
@endphp
<span style="display:inline-flex;align-items:center;gap:5px;font-size:12px;color:{{ $tcol }};">
<i class="bi {{ $tico }}"></i>
{{ ucfirst($video->type) }}
</span>
</td>
{{-- Views --}}
<td style="font-size:13px;color:var(--text-2);">
{{ number_format(\DB::table('video_views')->where('video_id',$video->id)->count()) }}
</td>
{{-- Likes --}}
<td style="font-size:13px;color:var(--text-2);">
{{ number_format(\DB::table('video_likes')->where('video_id',$video->id)->count()) }}
</td>
{{-- Shares --}}
<td style="font-size:13px;color:var(--text-2);">
{{ number_format(\DB::table('video_shares')->where('video_id',$video->id)->count()) }}
</td>
{{-- Date --}}
<td class="text-muted-sm">{{ $video->created_at->format('M d, Y') }}</td>
{{-- Actions --}}
<td onclick="event.stopPropagation()">
<div class="adm-row-actions" style="justify-content:flex-end;">
<a href="{{ route('videos.show', $video) }}" target="_blank"
class="adm-btn adm-btn-sm" title="Watch">
<i class="bi bi-play"></i>
</a>
<a href="{{ route('admin.videos.edit', $video) }}"
class="adm-btn adm-btn-sm" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<button type="button"
class="adm-btn adm-btn-sm adm-btn-danger"
title="Delete"
onclick="openDeleteDialog('{{ $video->getRouteKey() }}', '{{ addslashes($video->title) }}')">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="10">
<div class="empty-state">
<i class="bi bi-play-circle"></i>
<p>No videos found</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Pagination --}}
@if($videos instanceof \Illuminate\Pagination\LengthAwarePaginator && $videos->hasPages())
<div style="padding:16px 20px; border-top:1px solid var(--border);">
{{ $videos->onEachSide(1)->links() }}
</div>
@endif
</div>
{{-- ── Delete confirmation dialog ───────────────────────────────────── --}}
<div class="adm-dialog-overlay" id="deleteDialog">
<div class="adm-dialog">
<div class="adm-dialog-header">
<div class="adm-dialog-title">
<i class="bi bi-exclamation-triangle-fill"></i> Delete Video
</div>
<button type="button" class="adm-btn adm-btn-sm" onclick="closeDeleteDialog()"
style="border:none;background:none;color:var(--text-2);">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="adm-dialog-body">
<p>You are about to permanently delete <strong id="dlgVideoTitle"></strong>.</p>
<div class="adm-dialog-warning">
<i class="bi bi-info-circle-fill" style="flex-shrink:0;margin-top:1px;"></i>
All views, likes, comments, and HLS files for this video will also be deleted.
</div>
@if($adminNeedsOtp)
<div style="margin-top:14px;">
<label style="font-size:13px;color:var(--text-2);display:flex;align-items:center;gap:6px;margin-bottom:6px;">
<i class="bi bi-shield-lock-fill" style="color:#a78bfa;"></i>
Enter your <strong style="color:var(--text);">2FA code</strong> to confirm
</label>
<input type="text" id="dlgVideoOtp" inputmode="numeric" pattern="[0-9]*"
maxlength="6" autocomplete="one-time-code"
placeholder="000000"
style="width:100%;background:var(--bg-card2);border:1px solid var(--border-light);border-radius:8px;padding:10px 14px;color:#fff;font-size:22px;letter-spacing:0.3em;text-align:center;">
</div>
@endif
<div id="dlgVideoError" style="display:none;margin-top:10px;padding:8px 12px;background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.3);border-radius:6px;color:#f87171;font-size:13px;"></div>
</div>
<div class="adm-dialog-footer">
<button type="button" class="adm-btn" onclick="closeDeleteDialog()">Cancel</button>
<button type="button" class="adm-btn adm-btn-danger" id="dlgVideoConfirmBtn" onclick="confirmDeleteVideo()">
<i class="bi bi-trash"></i> Delete Video
</button>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
const _adminNeedsOtp = {{ $adminNeedsOtp ? 'true' : 'false' }};
let _deleteVideoId = null;
function openDeleteDialog(videoId, videoTitle) {
_deleteVideoId = videoId;
document.getElementById('dlgVideoTitle').textContent = videoTitle;
document.getElementById('dlgVideoError').style.display = 'none';
if (_adminNeedsOtp) document.getElementById('dlgVideoOtp').value = '';
document.getElementById('deleteDialog').classList.add('open');
if (_adminNeedsOtp) setTimeout(() => document.getElementById('dlgVideoOtp').focus(), 100);
}
function closeDeleteDialog() {
document.getElementById('deleteDialog').classList.remove('open');
_deleteVideoId = null;
}
function confirmDeleteVideo() {
if (!_deleteVideoId) return;
const btn = document.getElementById('dlgVideoConfirmBtn');
const errEl = document.getElementById('dlgVideoError');
const otpCode = _adminNeedsOtp ? document.getElementById('dlgVideoOtp').value.replace(/\s/g,'') : '';
if (_adminNeedsOtp && otpCode.length !== 6) {
errEl.textContent = 'Please enter your 6-digit 2FA code.';
errEl.style.display = 'block';
document.getElementById('dlgVideoOtp').focus();
return;
}
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting…';
errEl.style.display = 'none';
const body = new URLSearchParams({ '_token': '{{ csrf_token() }}', '_method': 'DELETE' });
if (_adminNeedsOtp) body.append('otp_code', otpCode);
fetch('/admin/videos/' + _deleteVideoId, {
method: 'POST',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
})
.then(r => r.json())
.then(data => {
if (data.success) {
closeDeleteDialog();
window.location.reload();
} else {
errEl.textContent = data.message || 'Delete failed.';
errEl.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash"></i> Delete Video';
if (_adminNeedsOtp) { document.getElementById('dlgVideoOtp').value = ''; document.getElementById('dlgVideoOtp').focus(); }
}
})
.catch(() => {
errEl.textContent = 'An error occurred. Please try again.';
errEl.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash"></i> Delete Video';
});
}
document.getElementById('deleteDialog').addEventListener('click', function(e) {
if (e.target === this) closeDeleteDialog();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeDeleteDialog();
});
</script>
@endsection