WIP: storage-fix-local-nas work before playlist controls feature

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
ghassanandClaude Sonnet 4.6 committed 2026-05-16 11:15:20 +03:00
1 parent 6b3ab5b65e
commit c160242dbc
46 files changed
+5365 -815

No files matched your search

+182 -8
View File
@@ -3,6 +3,11 @@
@section('title', 'Edit Video')
@section('page_title', 'Edit Video')
@php
$adminNeedsOtp = auth()->user()->two_factor_enabled &&
(! session('admin_2fa_verified_at') || now()->timestamp - session('admin_2fa_verified_at') >= 1800);
@endphp
@section('extra_styles')
<style>
.ef-grid {
@@ -77,6 +82,9 @@
background: rgba(248,113,113,.05);
}
.ef-danger-title { font-size: 12px; font-weight: 700; color: #f87171; margin-bottom: 10px; text-transform: uppercase; letter-spacing: .05em; }
.adm-btn-warning { color: #fb923c; border-color: rgba(251,146,60,.3); }
.adm-btn-warning:hover { background: rgba(251,146,60,.1); border-color: rgba(251,146,60,.5); color: #fdba74; }
.adm-btn-warning:disabled { opacity:.5; cursor:not-allowed; pointer-events:none; }
</style>
@endsection
@@ -203,7 +211,7 @@
<div class="adm-card">
<div class="adm-card-body">
@if($video->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $video->thumbnail) }}"
<img src="{{ route('media.thumbnail', $video->thumbnail) }}"
alt="{{ $video->title }}" class="ef-thumb">
@else
<div class="ef-thumb-placeholder"><i class="bi bi-play-circle"></i></div>
@@ -259,6 +267,41 @@
</div>
</div>
{{-- Replace File --}}
<div class="ef-danger-zone" style="margin-bottom:12px;">
<div class="ef-danger-title" style="color:#fb923c;border-color:rgba(251,146,60,.2);">
<i class="bi bi-arrow-repeat me-1"></i> Replace Media File
</div>
<p style="font-size:12px;color:var(--text-2);margin:0 0 12px;line-height:1.5;">
Fix a corrupted or missing file. All stats are preserved.
</p>
<div id="adm-rfl-dropzone" onclick="document.getElementById('adm-rfl-input').click()"
style="border:2px dashed var(--border);border-radius:8px;padding:14px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;margin-bottom:8px;"
onmouseenter="this.style.borderColor='#ef4444';this.style.background='rgba(239,68,68,.05)'"
onmouseleave="this.style.borderColor='var(--border)';this.style.background='transparent'">
<i class="bi bi-cloud-upload" style="font-size:22px;color:var(--text-2);display:block;margin-bottom:4px;"></i>
<span id="adm-rfl-label" style="font-size:12px;color:var(--text-2);">Click to choose replacement file</span>
<input type="file" id="adm-rfl-input" accept="video/*,audio/*,.mp4,.webm,.ogg,.mov,.avi,.wmv,.flv,.mkv,.mp3,.m4a,.aac,.wav,.flac,.opus" style="display:none;" onchange="admRflSelected(this)">
</div>
<div id="adm-rfl-info" style="display:none;background:var(--bg-2);border:1px solid var(--border);border-radius:6px;padding:8px 12px;align-items:center;gap:8px;margin-bottom:8px;">
<i class="bi bi-file-earmark-play" style="font-size:18px;color:#ef4444;flex-shrink:0;"></i>
<div style="flex:1;min-width:0;">
<div id="adm-rfl-name" style="font-size:12px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></div>
<div id="adm-rfl-size" style="font-size:11px;color:var(--text-2);margin-top:1px;"></div>
</div>
<button type="button" onclick="admRflClear()" style="background:none;border:none;color:var(--text-2);cursor:pointer;font-size:14px;flex-shrink:0;padding:0;"><i class="bi bi-x-lg"></i></button>
</div>
<div id="adm-rfl-status" style="display:none;margin-bottom:8px;font-size:12px;padding:7px 10px;border-radius:6px;"></div>
<button type="button" id="adm-rfl-btn" onclick="admRflSubmit()" disabled
class="adm-btn adm-btn-warning" style="width:100%;">
<i class="bi bi-arrow-repeat"></i> Replace File
</button>
</div>
{{-- Danger zone --}}
<div class="ef-danger-zone">
<div class="ef-danger-title"><i class="bi bi-exclamation-triangle-fill me-1"></i> Danger Zone</div>
@@ -289,15 +332,25 @@
<i class="bi bi-info-circle-fill" style="flex-shrink:0;margin-top:1px;"></i>
The video file, thumbnail, and all associated data will be removed. This cannot be undone.
</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="delDialogOtp" 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="delDialogError" 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="closeDelDialog()">Cancel</button>
<form method="POST" action="{{ route('admin.videos.delete', $video) }}">
@csrf @method('DELETE')
<button type="submit" class="adm-btn adm-btn-danger" style="height:36px;">
<i class="bi bi-trash3-fill"></i> Delete Permanently
</button>
</form>
<button type="button" class="adm-btn adm-btn-danger" id="delDialogConfirmBtn" onclick="confirmDelVideo()">
<i class="bi bi-trash3-fill"></i> Delete Permanently
</button>
</div>
</div>
</div>
@@ -306,9 +359,130 @@
@section('scripts')
<script>
function openDelDialog() { document.getElementById('delDialog').classList.add('open'); }
const _adminNeedsOtp = {{ $adminNeedsOtp ? 'true' : 'false' }};
const _editVideoId = '{{ $video->getRouteKey() }}';
function openDelDialog() {
document.getElementById('delDialogError').style.display = 'none';
if (_adminNeedsOtp) document.getElementById('delDialogOtp').value = '';
document.getElementById('delDialog').classList.add('open');
if (_adminNeedsOtp) setTimeout(() => document.getElementById('delDialogOtp').focus(), 100);
}
function closeDelDialog() { document.getElementById('delDialog').classList.remove('open'); }
function confirmDelVideo() {
const btn = document.getElementById('delDialogConfirmBtn');
const errEl = document.getElementById('delDialogError');
const otpCode = _adminNeedsOtp ? document.getElementById('delDialogOtp').value.replace(/\s/g,'') : '';
if (_adminNeedsOtp && otpCode.length !== 6) {
errEl.textContent = 'Please enter your 6-digit 2FA code.';
errEl.style.display = 'block';
document.getElementById('delDialogOtp').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/' + _editVideoId, {
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) {
window.location.href = '/admin/videos';
} else {
errEl.textContent = data.message || 'Delete failed.';
errEl.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash3-fill"></i> Delete Permanently';
if (_adminNeedsOtp) { document.getElementById('delDialogOtp').value = ''; document.getElementById('delDialogOtp').focus(); }
}
})
.catch(() => {
errEl.textContent = 'An error occurred. Please try again.';
errEl.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash3-fill"></i> Delete Permanently';
});
}
document.getElementById('delDialog').addEventListener('click', e => { if (e.target === document.getElementById('delDialog')) closeDelDialog(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeDelDialog(); });
// ── Admin Replace File ────────────────────────────────────────────────────
let _admRflFile = null;
const _admRflFmtSize = b => b >= 1073741824 ? (b/1073741824).toFixed(1)+' GB' : b >= 1048576 ? (b/1048576).toFixed(1)+' MB' : Math.round(b/1024)+' KB';
function admRflSelected(input) {
_admRflFile = input.files[0] || null;
const info = document.getElementById('adm-rfl-info');
if (_admRflFile) {
document.getElementById('adm-rfl-name').textContent = _admRflFile.name;
document.getElementById('adm-rfl-size').textContent = _admRflFmtSize(_admRflFile.size);
document.getElementById('adm-rfl-label').textContent = _admRflFile.name;
info.style.display = 'flex';
} else {
info.style.display = 'none';
document.getElementById('adm-rfl-label').textContent = 'Click to choose replacement file';
}
document.getElementById('adm-rfl-btn').disabled = !_admRflFile;
document.getElementById('adm-rfl-status').style.display = 'none';
}
function admRflClear() {
_admRflFile = null;
document.getElementById('adm-rfl-input').value = '';
document.getElementById('adm-rfl-info').style.display = 'none';
document.getElementById('adm-rfl-label').textContent = 'Click to choose replacement file';
document.getElementById('adm-rfl-btn').disabled = true;
document.getElementById('adm-rfl-status').style.display = 'none';
}
function admRflSubmit() {
if (!_admRflFile) return;
const btn = document.getElementById('adm-rfl-btn');
const status = document.getElementById('adm-rfl-status');
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Uploading…';
status.style.display = 'none';
const fd = new FormData();
fd.append('_token', '{{ csrf_token() }}');
fd.append('replacement_file', _admRflFile);
fetch('/videos/' + _editVideoId + '/replace-file', {
method: 'POST',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
body: fd
})
.then(r => r.json())
.then(data => {
if (data.success) {
status.style.cssText = 'display:block;background:rgba(74,222,128,.08);border:1px solid rgba(74,222,128,.25);color:#4ade80;';
status.innerHTML = '<i class="bi bi-check-circle-fill"></i> ' + data.message;
admRflClear();
setTimeout(() => location.reload(), 2200);
} else {
status.style.cssText = 'display:block;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.25);color:#f87171;';
status.textContent = data.message || 'Upload failed. Please try again.';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Replace File';
}
})
.catch(() => {
status.style.cssText = 'display:block;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.25);color:#f87171;';
status.textContent = 'Upload failed. Please try again.';
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Replace File';
});
}
</script>
@endsection