WIP: storage-fix-local-nas work before playlist controls feature
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
6b3ab5b65e
commit
c160242dbc
46 files changed
+5365
-815
No files matched your search
@@ -2,6 +2,11 @@
|
||||
|
||||
@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 ──────────────────────────────────────────────────── --}}
|
||||
@@ -100,18 +105,19 @@
|
||||
<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>
|
||||
<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="{{ asset('storage/thumbnails/' . $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;">
|
||||
@@ -132,7 +138,7 @@
|
||||
</td>
|
||||
|
||||
{{-- Owner --}}
|
||||
<td>
|
||||
<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 }}
|
||||
@@ -193,11 +199,16 @@
|
||||
{{ 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>
|
||||
<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">
|
||||
@@ -218,7 +229,7 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<td colspan="10">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<p>No videos found</p>
|
||||
@@ -256,15 +267,25 @@
|
||||
<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>
|
||||
<form id="deleteForm" method="POST">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="adm-btn adm-btn-danger" style="height:36px;">
|
||||
<i class="bi bi-trash"></i> Delete Video
|
||||
</button>
|
||||
</form>
|
||||
<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>
|
||||
@@ -273,13 +294,65 @@
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
const _adminNeedsOtp = {{ $adminNeedsOtp ? 'true' : 'false' }};
|
||||
let _deleteVideoId = null;
|
||||
|
||||
function openDeleteDialog(videoId, videoTitle) {
|
||||
_deleteVideoId = videoId;
|
||||
document.getElementById('dlgVideoTitle').textContent = videoTitle;
|
||||
document.getElementById('deleteForm').action = '/admin/videos/' + videoId;
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user