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
@@ -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
|
||||
Reference in new issue
Block a user