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

653 lines
32 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('layouts.app')
@section('title', 'Edit ' . $video->title . ' | ' . config('app.name'))
@section('main_class', 'edit-page-only edit-page-responsive')
@section('extra_styles')
<style>
@media (max-width: 991px) {
.edit-page-only .yt-header, .edit-page-only .yt-sidebar { display: block !important; }
.edit-page-only .yt-main { display: block !important; margin-left: 0; padding-bottom: 80px; }
.edit-page-only.edit-page-responsive { display: block; background: transparent; padding: 0; }
.edit-page-only.edit-page-responsive .edit-page-standalone { max-width: 100%; border-radius: 0; box-shadow: none; background: var(--bg-primary); border: none; padding: 16px; }
.edit-page-only.edit-page-responsive .edit-page-body { padding: 0; background: transparent; }
.edit-page-only.edit-page-responsive .form-input,
.edit-page-only.edit-page-responsive .form-textarea { background: var(--bg-secondary); border-color: var(--border-color); color: var(--text-primary); }
.edit-page-only.edit-page-responsive .form-label { color: var(--text-primary); }
#edit-thumbnail-dropzone { padding: 20px 16px; }
.options-grid { flex-direction: column; }
.option-item { min-width: 100%; }
}
@media (min-width: 992px) {
.edit-page-only { min-height: auto; display: block; padding: 20px; background: #121212; }
}
.edit-page-standalone {
background: #1a1a1a;
border: none;
border-radius: 0;
box-shadow: none;
overflow: hidden;
width: 100%;
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.edit-page-body { padding: 0; background: transparent; }
/* Form Groups */
.form-group { margin-bottom: 20px; }
.form-label { display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; color: #e5e5e5; }
.form-input, .form-textarea { width: 100%; background: #121212; border: 1px solid #333; border-radius: 8px; padding: 12px 16px; color: var(--text-primary); font-size: 14px; }
.form-input:focus, .form-textarea:focus { outline: none; border-color: var(--brand-red); }
.form-textarea { resize: vertical; min-height: 100px; }
/* Current thumbnail */
#current-thumb-wrap {
width: 100%; height: 140px; border-radius: 10px; overflow: hidden;
background: #151515; border: 2px dashed #404040; position: relative; margin-bottom: 8px;
}
#current-thumb-wrap img { width: 100%; height: 100%; object-fit: cover; display: none; }
#current-thumb-wrap.has-thumb img { display: block; }
#current-thumb-placeholder {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; color: #666; gap: 8px;
}
#current-thumb-placeholder i { font-size: 32px; }
#current-thumb-placeholder span { font-size: 13px; }
#current-thumb-wrap.has-thumb #current-thumb-placeholder { display: none; }
/* Thumbnail Dropzone */
#edit-thumbnail-dropzone {
border: 2px dashed #404040;
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #151515;
position: relative;
}
#edit-thumbnail-dropzone:hover { border-color: var(--brand-red); }
#edit-thumbnail-dropzone.dragover { border-color: var(--brand-red); background: rgba(230,30,30,.05); }
#edit-thumbnail { display: none; }
#edit-thumbnail-default i { font-size: 28px; color: #666; margin-bottom: 6px; }
#edit-thumbnail-default p { color: #888; font-size: 13px; margin: 0; }
#edit-thumbnail-info { display: none; }
#edit-thumbnail-info.active { display: block; }
#edit-thumbnail-info img { max-width: 100%; max-height: 120px; border-radius: 8px; }
/* Options Grid */
.options-section { margin-bottom: 24px; }
.options-label { font-size: 14px; font-weight: 500; color: #e5e5e5; margin-bottom: 12px; display: block; }
.options-grid { display: flex; gap: 8px; flex-wrap: wrap; }
.option-item { flex: 1; min-width: 100px; cursor: pointer; }
.option-item input { display: none; }
.option-content { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: #1a1a1a; border: 2px solid #333; border-radius: 10px; transition: all 0.2s; }
.option-item:hover .option-content { border-color: #555; background: #1f1f1f; }
.option-item.active .option-content { border-color: var(--brand-red); background: rgba(230,30,30,.1); }
.option-content i { font-size: 18px; color: #666; width: 20px; text-align: center; }
.option-item.active .option-content i { color: var(--brand-red); }
.option-content span { font-size: 13px; color: #aaa; }
.option-item.active .option-content span { color: #fff; }
/* File info row (for thumbnail) */
.file-info-row {
display: flex; align-items: center; gap: 12px; padding: 12px;
background: #1f1f1f; border-radius: 8px; border: 1px solid #333;
}
.file-info-row .thumb-preview { width: 80px; height: 48px; overflow: hidden; border-radius: 6px; flex-shrink: 0; }
.file-info-row .thumb-preview img { width: 100%; height: 100%; object-fit: cover; }
.file-info-row .file-meta { flex: 1; text-align: left; min-width: 0; }
.file-info-row .file-meta .fn { color: #e5e5e5; font-weight: 500; font-size: 13px; margin: 0 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-info-row .file-meta .fs { color: #888; font-size: 12px; margin: 0; }
.btn-remove { width: 32px; height: 32px; border-radius: 50%; background: #333; border: none; color: #888; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.btn-remove:hover { background: var(--brand-red); color: white; }
/* Slides manager */
.ep-slides-strip {
display: flex; flex-wrap: wrap; gap: 8px;
min-height: 72px; padding: 10px;
background: rgba(255,255,255,.03);
border: 2px dashed rgba(255,255,255,.1);
border-radius: 10px; margin-bottom: 8px;
}
.ep-slides-empty {
display: flex; align-items: center; justify-content: center;
width: 100%; color: var(--text-secondary); font-size: 13px;
}
.ep-item {
position: relative; width: 72px; height: 72px;
border-radius: 8px; overflow: hidden;
border: 2px solid rgba(255,255,255,.12);
cursor: grab; flex-shrink: 0;
transition: border-color .2s;
}
.ep-item:hover { border-color: var(--brand-red); }
.ep-item img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.ep-drag-handle {
position: absolute; top: 2px; left: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55); border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; color: #ccc; cursor: grab;
}
.ep-remove {
position: absolute; top: 2px; right: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55); border: none; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 10px; color: #ccc; cursor: pointer; padding: 0;
transition: background .15s, color .15s;
}
.ep-remove:hover { background: var(--brand-red); color: #fff; }
.ep-item.ep-dragging { opacity: .35; outline: 2px dashed var(--brand-red); }
/* Status */
#edit-status { display: none; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
#edit-status.success { display: block; background: rgba(34,197,94,.2); color: #4ade80; border: 1px solid rgba(34,197,94,.3); }
#edit-status.error { display: block; background: rgba(239,68,68,.2); color: #f87171; border: 1px solid rgba(239,68,68,.3); }
/* Submit Button */
.btn-submit { width: 100%; justify-content: center; font-size: 15px; font-weight: 600; }
.btn-submit:disabled { background: #555 !important; border-color: #555 !important; cursor: not-allowed; opacity: 1; }
/* Danger zone */
.danger-zone { margin-top: 32px; padding-top: 20px; border-top: 1px solid #333; }
.danger-zone p { font-size: 13px; color: #888; margin-bottom: 12px; }
.danger-zone .action-btn { width: 100%; justify-content: center; }
@media (max-width: 600px) {
.options-grid { flex-direction: column; }
.option-item { min-width: 100%; }
}
</style>
@endsection
@section('content')
<div class="edit-page-standalone">
<div class="edit-page-body">
<form id="edit-form" enctype="multipart/form-data">
@csrf
@method('PUT')
@if(!$video->isAudioOnly())
{{-- Current Thumbnail (non-audio only) --}}
<div class="form-group">
<label class="form-label"><i class="bi bi-image"></i> Current Thumbnail</label>
<div id="current-thumb-wrap" class="{{ $video->thumbnail ? 'has-thumb' : '' }}">
<img src="{{ $video->thumbnail ? route('media.thumbnail', $video->thumbnail) : '' }}" alt="Thumbnail">
<div id="current-thumb-placeholder">
<i class="bi bi-card-image"></i>
<span>No thumbnail set</span>
</div>
</div>
</div>
@endif
{{-- Title --}}
<div class="form-group">
<label class="form-label">Title *</label>
<input type="text" name="title" id="edit-title" required class="form-input"
value="{{ $video->title }}" placeholder="Enter video title">
</div>
{{-- Description --}}
<div class="form-group">
<label class="form-label">Description</label>
<textarea name="description" id="edit-description" rows="4" class="form-textarea"
placeholder="Tell viewers about your video (Markdown supported)">{{ $video->description }}</textarea>
</div>
@if(!$video->isAudioOnly())
{{-- Thumbnail dropzone (non-audio) --}}
<div class="form-group">
<label class="form-label"><i class="bi bi-camera"></i> Change Thumbnail</label>
<div id="edit-thumbnail-dropzone">
<input type="file" name="thumbnail" id="edit-thumbnail" accept="image/*">
<div id="edit-thumbnail-default">
<i class="bi bi-card-image"></i>
<p>Click to select new thumbnail (optional, max 20MB)</p>
</div>
<div id="edit-thumbnail-info">
<div class="file-info-row">
<div class="thumb-preview"><img id="edit-thumb-preview-img" src="" alt=""></div>
<div class="file-meta">
<p class="fn" id="edit-thumb-filename"></p>
<p class="fs" id="edit-thumb-filesize"></p>
</div>
<button type="button" class="btn-remove" onclick="removeEditThumb(event)"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
</div>
@else
{{-- Slides manager (audio tracks only) --}}
<div class="form-group">
<label class="form-label">
<i class="bi bi-images"></i> Cover Slides
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:6px;">Drag to reorder · click × to remove</span>
</label>
<input type="hidden" name="slides_order" id="ep-slides-order" value="[]">
<input type="file" name="slides_add[]" id="ep-slides-add-input" accept="image/*" multiple style="display:none;">
<div class="ep-slides-strip" id="ep-slides-strip">
<div class="ep-slides-empty" id="ep-slides-empty">No slides yet</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button type="button" class="action-btn" onclick="document.getElementById('ep-slides-add-input').click()">
<i class="bi bi-plus-lg"></i> <span>Add Images</span>
</button>
<span id="ep-slides-hint" style="font-size:12px;color:var(--text-secondary);"></span>
</div>
</div>
@endif
{{-- Video Type --}}
<div class="options-section">
<span class="options-label">Video Type</span>
<div class="options-grid" id="edit-type-options">
@foreach([['generic','bi-film','Generic'],['music','bi-music-note','Music'],['match','bi-trophy','Match']] as [$val,$icon,$label])
<label class="option-item {{ ($video->type ?? 'generic') === $val ? 'active' : '' }}" data-type="{{ $val }}">
<input type="radio" name="type" value="{{ $val }}" {{ ($video->type ?? 'generic') === $val ? 'checked' : '' }}>
<div class="option-content">
<i class="bi {{ $icon }}"></i>
<span>{{ $label }}</span>
</div>
</label>
@endforeach
</div>
</div>
{{-- Privacy --}}
<div class="options-section">
<span class="options-label">Privacy</span>
<div class="options-grid" id="edit-visibility-options">
@foreach([['public','bi-globe','Public'],['unlisted','bi-link-45deg','Unlisted'],['private','bi-lock','Private']] as [$val,$icon,$label])
<label class="option-item {{ ($video->visibility ?? 'public') === $val ? 'active' : '' }}" data-privacy="{{ $val }}">
<input type="radio" name="visibility" value="{{ $val }}" {{ ($video->visibility ?? 'public') === $val ? 'checked' : '' }}>
<div class="option-content">
<i class="bi {{ $icon }}"></i>
<span>{{ $label }}</span>
</div>
</label>
@endforeach
</div>
</div>
{{-- Download Access --}}
<div class="options-section">
<span class="options-label"><i class="bi bi-download" style="color:var(--brand-red);margin-right:6px;"></i>Who can download?</span>
<div class="options-grid" id="edit-download-options">
@foreach([['disabled','bi-slash-circle','Disabled'],['everyone','bi-globe','Everyone'],['registered','bi-person-check','Members'],['subscribers','bi-star','Subscribers']] as [$val,$icon,$label])
<label class="option-item {{ ($video->download_access ?? 'disabled') === $val ? 'active' : '' }}" data-download="{{ $val }}">
<input type="radio" name="download_access" value="{{ $val }}" {{ ($video->download_access ?? 'disabled') === $val ? 'checked' : '' }}>
<div class="option-content">
<i class="bi {{ $icon }}"></i>
<span>{{ $label }}</span>
</div>
</label>
@endforeach
</div>
</div>
{{-- Status --}}
<div id="edit-status"></div>
{{-- Submit --}}
<button type="submit" id="edit-submit-btn" class="action-btn action-btn-primary btn-submit">
<i class="bi bi-check-lg"></i> <span>Save Changes</span>
</button>
</form>
{{-- Replace File --}}
<div class="danger-zone" style="margin-top:20px;">
<p style="color:#f87171;"><i class="bi bi-arrow-repeat" style="margin-right:6px;"></i>Replace Media File</p>
<p style="font-size:12px;color:#888;margin:-8px 0 14px;line-height:1.5;">
Fix a corrupted or missing file without losing any views, likes, or comments.
</p>
<div id="ep-rfl-dropzone" onclick="document.getElementById('ep-rfl-input').click()"
style="border:2px dashed #444;border-radius:10px;padding:18px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;margin-bottom:10px;"
onmouseenter="this.style.borderColor='#ef4444';this.style.background='rgba(239,68,68,.05)'"
onmouseleave="this.style.borderColor='#444';this.style.background='transparent'">
<i class="bi bi-cloud-upload" style="font-size:26px;color:#666;display:block;margin-bottom:6px;"></i>
<span id="ep-rfl-label" style="font-size:13px;color:#888;">Tap to choose replacement file</span>
<input type="file" id="ep-rfl-input" accept="video/*,audio/*,.mp4,.webm,.ogg,.mov,.avi,.wmv,.flv,.mkv,.mp3,.m4a,.aac,.wav,.flac,.opus" style="display:none;" onchange="epRflSelected(this)">
</div>
<div id="ep-rfl-info" style="display:none;background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:10px 14px;align-items:center;gap:10px;margin-bottom:10px;">
<i class="bi bi-file-earmark-play" style="font-size:20px;color:#ef4444;flex-shrink:0;"></i>
<div style="flex:1;min-width:0;">
<div id="ep-rfl-name" style="font-size:13px;font-weight:600;color:#e5e5e5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></div>
<div id="ep-rfl-size" style="font-size:11px;color:#888;margin-top:2px;"></div>
</div>
<button type="button" onclick="epRflClear()" style="background:none;border:none;color:#888;cursor:pointer;font-size:16px;flex-shrink:0;padding:0;"><i class="bi bi-x-lg"></i></button>
</div>
<div id="ep-rfl-status" style="display:none;margin-bottom:10px;font-size:12px;padding:8px 12px;border-radius:6px;"></div>
<button type="button" id="ep-rfl-btn" onclick="epRflSubmit()" disabled
class="action-btn action-btn-danger" style="width:100%;justify-content:center;">
<i class="bi bi-arrow-repeat"></i> <span>Replace File</span>
</button>
</div>
{{-- Danger zone --}}
<div class="danger-zone" style="margin-top:12px;">
<p>Danger Zone</p>
<form action="{{ route('videos.destroy', $video) }}" method="POST" id="delete-edit-form">
@csrf
@method('DELETE')
<button type="button" class="action-btn action-btn-danger"
onclick="showConfirm('Delete this video? This cannot be undone.', function(){ document.getElementById('delete-edit-form').submit(); }, 'Delete')">
<i class="bi bi-trash"></i> <span>Delete Video</span>
</button>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024, sizes = ['Bytes','KB','MB','GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
@if(!$video->isAudioOnly())
// ── Thumbnail dropzone (non-audio) ────────────────────────────
const editThumbInput = document.getElementById('edit-thumbnail');
const editThumbDropzone = document.getElementById('edit-thumbnail-dropzone');
editThumbDropzone.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove')) return;
if (typeof window.openCropperModal_thumb_edit_mobile === 'function') {
window.openCropperModal_thumb_edit_mobile();
} else {
editThumbInput.click();
}
});
editThumbDropzone.addEventListener('dragover', e => { e.preventDefault(); editThumbDropzone.classList.add('dragover'); });
editThumbDropzone.addEventListener('dragleave', () => editThumbDropzone.classList.remove('dragover'));
editThumbDropzone.addEventListener('drop', e => {
e.preventDefault(); editThumbDropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_edit_mobile === 'function') {
window.tcPreload_thumb_edit_mobile(droppedFile);
window.openCropperModal_thumb_edit_mobile();
} else {
editThumbInput.files = e.dataTransfer.files;
handleEditThumbSelect();
}
}
});
editThumbInput.addEventListener('change', handleEditThumbSelect);
function handleEditThumbSelect() {
if (!editThumbInput.files || !editThumbInput.files[0]) return;
const file = editThumbInput.files[0];
document.getElementById('edit-thumb-filename').textContent = file.name;
document.getElementById('edit-thumb-filesize').textContent = formatFileSize(file.size);
const reader = new FileReader();
reader.onload = e => { document.getElementById('edit-thumb-preview-img').src = e.target.result; };
reader.readAsDataURL(file);
document.getElementById('edit-thumbnail-default').style.display = 'none';
document.getElementById('edit-thumbnail-info').classList.add('active');
}
function removeEditThumb(e) {
e.preventDefault(); e.stopPropagation();
editThumbInput.value = '';
document.getElementById('edit-thumbnail-default').style.display = 'block';
document.getElementById('edit-thumbnail-info').classList.remove('active');
}
@else
// ── Slides manager (audio tracks only) ────────────────────────
let epSlidesData = @json($video->slides->map(fn($s) => ['id' => $s->id, 'url' => route('media.thumbnail', $s->filename)])->values());
let epDragSrc = null;
function _epSyncOrder() {
const keptIds = epSlidesData.filter(s => s.id).map(s => s.id);
document.getElementById('ep-slides-order').value = JSON.stringify(keptIds);
const count = epSlidesData.length;
document.getElementById('ep-slides-hint').textContent =
count === 1 ? '1 image — static cover' : count + ' images — will crossfade';
}
function epSlidesRefresh() {
const strip = document.getElementById('ep-slides-strip');
const empty = document.getElementById('ep-slides-empty');
strip.querySelectorAll('.ep-item').forEach(el => el.remove());
empty.style.display = epSlidesData.length === 0 ? 'flex' : 'none';
epSlidesData.forEach((item) => {
const div = document.createElement('div');
div.className = 'ep-item';
div.draggable = true;
div._epData = item; // reference on element — avoids stale-index issues during drag
div.innerHTML = `
<div class="ep-drag-handle" title="Drag to reorder"><i class="bi bi-grip-vertical"></i></div>
<img src="${item.url}" alt="">
<button type="button" class="ep-remove" title="Remove"><i class="bi bi-x-lg"></i></button>
`;
div.querySelector('.ep-remove').addEventListener('click', () => {
epSlidesData.splice(epSlidesData.indexOf(div._epData), 1);
epSlidesRefresh();
});
div.addEventListener('dragstart', e => {
epDragSrc = div;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => div.classList.add('ep-dragging'), 0);
});
// On dragend: sync epSlidesData from current DOM order, then update inputs
div.addEventListener('dragend', () => {
div.classList.remove('ep-dragging');
epDragSrc = null;
const newData = [];
strip.querySelectorAll('.ep-item').forEach(el => newData.push(el._epData));
epSlidesData = newData;
_epSyncOrder();
});
// On dragover: only move element in DOM — never re-render
div.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!epDragSrc || epDragSrc === div) return;
const items = [...strip.querySelectorAll('.ep-item')];
const srcPos = items.indexOf(epDragSrc);
const tgtPos = items.indexOf(div);
if (srcPos < tgtPos) div.after(epDragSrc);
else div.before(epDragSrc);
});
strip.appendChild(div);
});
_epSyncOrder();
}
document.getElementById('ep-slides-add-input').addEventListener('change', function() {
Array.from(this.files).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
epSlidesData.push({ file, url: e.target.result });
epSlidesRefresh();
};
reader.readAsDataURL(file);
});
this.value = '';
});
// Initialise strip with existing slides
epSlidesRefresh();
@endif
// ── Option selectors ──────────────────────────────────────────
document.querySelectorAll('#edit-type-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#edit-type-options .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
document.querySelectorAll('#edit-visibility-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#edit-visibility-options .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
document.querySelectorAll('#edit-download-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#edit-download-options .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// ── Form submission ───────────────────────────────────────────
document.getElementById('edit-form').addEventListener('submit', function(e) {
e.preventDefault();
const title = document.getElementById('edit-title').value.trim();
if (!title) { showEditError('Please enter a title.'); return; }
const btn = document.getElementById('edit-submit-btn');
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Saving...';
document.getElementById('edit-status').className = '';
let formData = new FormData(this);
@if($video->isAudioOnly())
// Append newly added slide files
epSlidesData.filter(s => s.file).forEach(s => formData.append('slides_add[]', s.file, s.file.name));
@endif
fetch('{{ route("videos.update", $video) }}', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
})
.then(r => r.json())
.then(data => {
if (data.success) {
const status = document.getElementById('edit-status');
status.innerHTML = '<i class="bi bi-check-circle-fill"></i> ' + data.message;
status.className = 'success';
setTimeout(() => window.location.href = '{{ route("videos.show", $video) }}', 1000);
} else {
showEditError(data.message || 'Update failed.');
}
})
.catch(() => showEditError('Something went wrong. Please try again.'));
});
function showEditError(message) {
const status = document.getElementById('edit-status');
status.innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + message;
status.className = 'error';
const btn = document.getElementById('edit-submit-btn');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg"></i> <span>Save Changes</span>';
}
// ── Replace File ──────────────────────────────────────────────────────
let _epRflFile = null;
const _epRflFmtSize = b => b >= 1073741824 ? (b/1073741824).toFixed(1)+' GB' : b >= 1048576 ? (b/1048576).toFixed(1)+' MB' : Math.round(b/1024)+' KB';
function epRflSelected(input) {
_epRflFile = input.files[0] || null;
const info = document.getElementById('ep-rfl-info');
if (_epRflFile) {
document.getElementById('ep-rfl-name').textContent = _epRflFile.name;
document.getElementById('ep-rfl-size').textContent = _epRflFmtSize(_epRflFile.size);
document.getElementById('ep-rfl-label').textContent = _epRflFile.name;
info.style.display = 'flex';
} else {
info.style.display = 'none';
document.getElementById('ep-rfl-label').textContent = 'Tap to choose replacement file';
}
document.getElementById('ep-rfl-btn').disabled = !_epRflFile;
document.getElementById('ep-rfl-status').style.display = 'none';
}
function epRflClear() {
_epRflFile = null;
document.getElementById('ep-rfl-input').value = '';
document.getElementById('ep-rfl-info').style.display = 'none';
document.getElementById('ep-rfl-label').textContent = 'Tap to choose replacement file';
document.getElementById('ep-rfl-btn').disabled = true;
document.getElementById('ep-rfl-status').style.display = 'none';
}
function epRflSubmit() {
if (!_epRflFile) return;
const btn = document.getElementById('ep-rfl-btn');
const status = document.getElementById('ep-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', _epRflFile);
fetch('{{ route("videos.replaceFile", $video) }}', {
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,.1);border:1px solid rgba(74,222,128,.25);color:#4ade80;';
status.innerHTML = '<i class="bi bi-check-circle-fill"></i> ' + data.message;
epRflClear();
setTimeout(() => location.reload(), 2000);
} else {
status.style.cssText = 'display:block;background:rgba(239,68,68,.1);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> <span>Replace File</span>';
}
})
.catch(() => {
status.style.cssText = 'display:block;background:rgba(239,68,68,.1);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> <span>Replace File</span>';
});
}
</script>
<x-image-cropper
id="thumb_edit_mobile"
:width="448"
:height="252"
shape="square"
target-input="edit-thumbnail"
preview-img="edit-thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>
@endsection