ghassan a4384113c2 Audio songs: one-folder storage, version-aware download/share, GPU-checked renders
Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
  unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
  subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
  updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).

Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
  is carried through share links and auto-selects audio + title + flag + about +
  OG/meta on open.

GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
  256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
  frequency bars; loop-filter rebuild makes generation ~25x faster.

Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:03:43 +03:00

1070 lines
54 KiB
PHP

@php use App\Data\Languages; @endphp
<div class="modal fade" id="editVideoModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable um-dialog">
<form id="edit-form" enctype="multipart/form-data" class="modal-content um-content">
@csrf
@method('PUT')
{{-- Core hidden inputs --}}
<input type="hidden" name="type" id="edit-type-val" value="generic">
<input type="hidden" name="visibility" id="edit-vis-val" value="public">
<input type="hidden" name="download_access" id="edit-dl-val" value="disabled">
{{-- Optional replacement video/audio file --}}
<input type="file" name="video" id="edit-video-file"
accept="video/*,audio/mpeg,audio/mp4,audio/aac,audio/flac,audio/wav,.mp3,.m4a,.aac,.flac,.wav"
style="display:none;">
{{-- ── Discard confirm overlay ── --}}
<div id="edit-close-confirm" style="display:none;position:absolute;inset:0;z-index:50;background:rgba(0,0,0,.88);border-radius:20px;align-items:center;justify-content:center;flex-direction:column;gap:16px;padding:40px;text-align:center;">
<div style="width:60px;height:60px;background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.3);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:26px;color:#f59e0b;">
<i class="bi bi-exclamation-triangle-fill"></i>
</div>
<div>
<p style="color:#f1f1f1;font-size:17px;font-weight:700;margin:0 0 6px;">Discard changes?</p>
<p style="color:#777;font-size:13px;margin:0;line-height:1.5;">Any unsaved changes will be lost.</p>
</div>
<div style="display:flex;gap:10px;">
<button type="button" class="action-btn" onclick="_editHideConfirm()"><span>Keep editing</span></button>
<button type="button" class="action-btn action-btn-danger" onclick="_editDoClose()"><i class="bi bi-trash"></i><span>Discard</span></button>
</div>
</div>
{{-- ── Header ── --}}
<div class="um-header">
<div class="um-header-left">
<div class="um-header-icon"><i class="bi bi-pencil-fill"></i></div>
<div>
<div class="um-header-title">Edit</div>
<div class="um-header-sub" id="edit-modal-label">Loading…</div>
</div>
</div>
<button type="button" class="btn-close btn-close-white" onclick="closeEditVideoModal()" aria-label="Close"></button>
</div>
{{-- ── Body ── --}}
<div class="um-body">
{{-- ── Global Settings ── --}}
<div class="um-gs-row">
<div class="um-gs-wrap">
<span class="um-gs-lbl">Content Type</span>
<button type="button" class="um-gs-btn" id="edit-gs-type-btn">
<i class="bi bi-film um-gs-ico" id="edit-gs-type-ico"></i>
<span class="um-gs-txt" id="edit-gs-type-txt">Generic</span>
<i class="bi bi-chevron-down um-gs-arr"></i>
</button>
<ul class="um-gs-menu" id="edit-gs-type-menu" hidden>
<li class="um-gs-opt active" data-egs="type" data-value="generic" data-icon="bi-film" data-label="Generic"><i class="bi bi-film"></i><span>Generic</span></li>
<li class="um-gs-opt" data-egs="type" data-value="music" data-icon="bi-music-note-beamed" data-label="Music"> <i class="bi bi-music-note-beamed"></i><span>Music</span></li>
<li class="um-gs-opt" data-egs="type" data-value="match" data-icon="bi-trophy" data-label="Match"> <i class="bi bi-trophy"></i><span>Match</span></li>
</ul>
</div>
<div class="um-gs-wrap">
<span class="um-gs-lbl">Visibility</span>
<button type="button" class="um-gs-btn" id="edit-gs-vis-btn">
<i class="bi bi-globe um-gs-ico" id="edit-gs-vis-ico"></i>
<span class="um-gs-txt" id="edit-gs-vis-txt">Public</span>
<i class="bi bi-chevron-down um-gs-arr"></i>
</button>
<ul class="um-gs-menu" id="edit-gs-vis-menu" hidden>
<li class="um-gs-opt active" data-egs="vis" data-value="public" data-icon="bi-globe" data-label="Public"> <i class="bi bi-globe"></i><span>Public</span></li>
<li class="um-gs-opt" data-egs="vis" data-value="unlisted" data-icon="bi-link-45deg" data-label="Unlisted"> <i class="bi bi-link-45deg"></i><span>Unlisted</span></li>
<li class="um-gs-opt" data-egs="vis" data-value="private" data-icon="bi-lock" data-label="Private"> <i class="bi bi-lock"></i><span>Private</span></li>
</ul>
</div>
<div class="um-gs-wrap">
<span class="um-gs-lbl">Downloads</span>
<button type="button" class="um-gs-btn" id="edit-gs-dl-btn">
<i class="bi bi-slash-circle um-gs-ico" id="edit-gs-dl-ico"></i>
<span class="um-gs-txt" id="edit-gs-dl-txt">Off</span>
<i class="bi bi-chevron-down um-gs-arr"></i>
</button>
<ul class="um-gs-menu" id="edit-gs-dl-menu" hidden>
<li class="um-gs-opt active" data-egs="dl" data-value="disabled" data-icon="bi-slash-circle" data-label="Off"> <i class="bi bi-slash-circle"></i><span>Off</span></li>
<li class="um-gs-opt" data-egs="dl" data-value="everyone" data-icon="bi-globe" data-label="Everyone"> <i class="bi bi-globe"></i><span>Everyone</span></li>
<li class="um-gs-opt" data-egs="dl" data-value="registered" data-icon="bi-person-check" data-label="Members"> <i class="bi bi-person-check"></i><span>Members</span></li>
<li class="um-gs-opt" data-egs="dl" data-value="subscribers" data-icon="bi-star" data-label="Subscribers"> <i class="bi bi-star"></i><span>Subscribers</span></li>
</ul>
</div>
</div>
<div class="um-rule"></div>
{{-- ── Track Cards Section ── --}}
<div class="um-tracks-header">
<div>
<span class="um-tracks-title" id="edit-tracks-section-label">Language Tracks</span>
<span class="um-tracks-sub" id="edit-tracks-section-sub">Add audio tracks in different languages</span>
</div>
<button type="button" class="action-btn" id="edit-add-track-btn" onclick="editAddExtraTrack()" style="display:none;font-size:12px;">
<i class="bi bi-plus-circle"></i> <span>Add Language Track</span>
</button>
</div>
<input type="hidden" name="promote_track_id" id="edit-promote-track-id" value="">
<div id="edit-tc-list">
{{-- Track 1 card always visible in edit mode --}}
<div class="um-track-card" id="edit-tc-t1-card">
<div class="um-tc-body">
<div class="um-tc-left">
<div class="um-tc-num">1</div>
<span class="fi fi-xx um-tc-flag" id="edit-tc-flag-t1"></span>
<div class="um-tc-info">
<span class="um-tc-title" id="edit-tc-title-t1"></span>
<span class="um-tc-primary" id="edit-tc-t1-primary-badge"><i class="bi bi-star-fill"></i> Primary</span>
</div>
</div>
<div class="um-tc-right">
<button type="button" class="action-btn icon-only edit-tc-arrow-up" onclick="editMoveTrack('t1-card','up')" title="Move up" style="display:none;">
<i class="bi bi-arrow-up"></i>
</button>
<button type="button" class="action-btn icon-only edit-tc-arrow-down" onclick="editMoveTrack('t1-card','down')" title="Move down" style="display:none;">
<i class="bi bi-arrow-down"></i>
</button>
<button type="button" class="action-btn" onclick="editOpenTrackPopup('t1')">
<i class="bi bi-pencil"></i> <span>Edit</span>
</button>
</div>
</div>
</div>
{{-- Secondary track cards appended here by JS --}}
</div>
{{-- Status --}}
<div id="edit-status-msg" class="status-message-modal"></div>
{{-- Submit --}}
<button type="submit" id="edit-submit-btn" class="action-btn action-btn-primary um-submit">
<i class="bi bi-check-lg"></i> <span>Save Changes</span>
</button>
</div>{{-- /um-body --}}
{{-- ── Track Editor Mini-Popup ── --}}
<div id="edit-track-popup" class="um-track-popup" style="display:none;">
<div class="um-track-popup-box">
<div class="um-tp-header">
<div class="um-tp-header-left">
<div class="um-tp-header-icon"><i class="bi bi-translate"></i></div>
<div class="um-tp-header-title">Track Editor</div>
</div>
<button type="button" class="btn-close btn-close-white" onclick="editCloseTrackPopup()" aria-label="Close"></button>
</div>
<div class="um-tp-body">
{{-- Track 1 form (primary) --}}
<x-track-editor-form
:is-primary="true"
prefix="t1"
language-name="primary_language"
language-id="edit_primary_language"
title-name="title"
title-id="edit-track1-title"
desc-name="description"
desc-id="edit-track1-desc"
video-file-input-id="edit-video-file"
/>
{{-- Extra track forms appended by JS --}}
<div id="edit-tf-extra"></div>
</div>{{-- /um-tp-body --}}
<div class="um-tp-footer">
<button type="button" class="action-btn action-btn-primary" onclick="editCloseTrackPopup()">
<i class="bi bi-check-lg"></i> <span>Done</span>
</button>
</div>
</div>{{-- /um-track-popup-box --}}
</div>{{-- /edit-track-popup --}}
</form>{{-- /edit-form --}}
</div>
</div>
{{-- ── Secondary track form template (cloned by JS for each extra track) ──── --}}
<div id="edit-extra-track-tpl" style="display:none;" aria-hidden="true">
<x-track-editor-form
prefix="__TPL__"
:is-primary="false"
language-name="__LANGNAME__"
language-id="csd_v___TPL__"
title-name="__TITLENAME__"
title-id="edit-__TPL__-title"
desc-name="__DESCNAME__"
desc-id="edit-__TPL__-desc"
:embed-audio-input="true"
audio-input-name="__AUDIONAME__"
video-file-input-id=""
/>
</div>
<style>
#editVideoModal .modal-dialog { opacity: 0; transition: opacity .25s ease; }
#editVideoModal.show .modal-dialog { opacity: 1; }
</style>
<script>
// ── Init primary track CSD ────────────────────────────────────────────────────
if (window.CSD) new CSD('csd_t1');
// ── State ─────────────────────────────────────────────────────────────────────
let _editSaving = false;
let _editExtraCount = 0;
let _editMode = 'generic';
let _editSlidesData = {}; // keyed by track id, array of {id?, url, file?}
let _editSlidesDragSrc = null;
let _editDeleteTrackIds = [];
window._editCurrentVideoId = null;
// ── Modal open / close ────────────────────────────────────────────────────────
function openEditVideoModal(videoId) {
if (window.innerWidth < 992) {
window.location.href = `/videos/${videoId}/edit`;
return;
}
window._editCurrentVideoId = videoId;
fetch(`/videos/${videoId}/edit`, {
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(r => r.json())
.then(data => {
if (!data.success) { showToast('Failed to load video data', 'error'); return; }
const v = data.video;
document.getElementById('edit-form').action = `/videos/${videoId}`;
// Header
const lbl = document.getElementById('edit-modal-label');
if (lbl) lbl.textContent = v.title || 'Video';
// Reset everything first
_editResetForm();
// Global settings
const typeMap = {generic:'bi-film',music:'bi-music-note-beamed',match:'bi-trophy'};
const typeLabels = {generic:'Generic',music:'Music',match:'Match'};
const t = v.type || 'generic';
_editGsSet('type', t, typeMap[t]||'bi-film', typeLabels[t]||'Generic');
const visMap = {public:'bi-globe',unlisted:'bi-link-45deg',private:'bi-lock'};
const visLabel = {public:'Public',unlisted:'Unlisted',private:'Private'};
const vis = v.visibility || 'public';
_editGsSet('vis', vis, visMap[vis]||'bi-globe', visLabel[vis]||'Public');
const dlMap = {disabled:'bi-slash-circle',everyone:'bi-globe',registered:'bi-person-check',subscribers:'bi-star'};
const dlLabel = {disabled:'Off',everyone:'Everyone',registered:'Members',subscribers:'Subscribers'};
const dl = v.download_access || 'disabled';
_editGsSet('dl', dl, dlMap[dl]||'bi-slash-circle', dlLabel[dl]||'Off');
_editApplyMode(t);
// Track 1 popup fields
const titleEl = document.getElementById('edit-track1-title');
const descEl = document.getElementById('edit-track1-desc');
if (titleEl) titleEl.value = v.title || '';
if (descEl) { descEl.value = v.description || ''; descEl.dispatchEvent(new Event('rte:refresh')); }
_editSetLangSelect(v.language || '');
// Thumbnail
if (v.thumbnail && v.thumbnail_url) {
const prev = document.getElementById('edit-t1-thumbnail-preview');
const ph = document.getElementById('edit-t1-thumbnail-ph');
const info = document.getElementById('edit-t1-thumbnail-info');
const fn = document.getElementById('edit-t1-thumbnail-fname');
if (prev) prev.src = v.thumbnail_url;
if (fn) fn.textContent = 'Current thumbnail';
if (ph) ph.style.display = 'none';
if (info) info.style.display = 'flex';
}
// Slides
_editSlidesData['t1'] = (v.slides || []).map(s => ({ id: s.id, url: s.url }));
_editRenderSlides('t1');
// Extra tracks
if (v.is_audio && v.audio_tracks) {
v.audio_tracks.forEach(track => _editAddExistingTrack(track));
}
// Update track 1 card
editUpdateTrackCard('t1');
// Open modal
const modal = new bootstrap.Modal(document.getElementById('editVideoModal'));
modal.show();
setTimeout(() => document.getElementById('editVideoModal').classList.add('show'), 10);
})
.catch(() => showToast('Failed to load video data', 'error'));
}
function closeEditVideoModal() {
const el = document.getElementById('editVideoModal');
const m = bootstrap.Modal.getInstance(el);
if (m) m.hide();
el.addEventListener('hidden.bs.modal', () => {
el.classList.remove('show');
window._editCurrentVideoId = null;
}, { once: true });
}
function _editHideConfirm() { document.getElementById('edit-close-confirm').style.display = 'none'; }
function _editDoClose() { _editHideConfirm(); closeEditVideoModal(); }
// ── Form reset ────────────────────────────────────────────────────────────────
function _editResetForm() {
_editSaving = false;
_editExtraCount = 0;
_editDeleteTrackIds = [];
document.getElementById('edit-form').reset();
document.getElementById('edit-status-msg').className = 'status-message-modal';
const btn = document.getElementById('edit-submit-btn');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg"></i> <span>Save Changes</span>';
document.getElementById('edit-track-popup').style.display = 'none';
document.getElementById('edit-tf-t1').style.display = 'none';
// Clear extra track cards (leave only the primary card in #edit-tc-list)
const _tcList = document.getElementById('edit-tc-list');
const _primaryCard = document.getElementById('edit-tc-t1-card');
Array.from(_tcList.children).forEach(c => { if (c !== _primaryCard) c.remove(); });
document.getElementById('edit-promote-track-id').value = '';
document.getElementById('edit-tf-extra').innerHTML = '';
_editUpdateTrackPositions();
// Clear slides data
_editSlidesData = {};
_editRenderSlides('t1');
// Reset video dropzone
document.getElementById('edit-t1-dz-idle').style.display = '';
document.getElementById('edit-t1-file-info').style.display = 'none';
// Reset thumbnail
document.getElementById('edit-t1-thumbnail-ph').style.display = '';
document.getElementById('edit-t1-thumbnail-info').style.display = 'none';
const prev = document.getElementById('edit-t1-thumbnail-preview');
if (prev) prev.src = '';
// Reset audio box
const fn = document.getElementById('edit-t1-fname');
if (fn) fn.textContent = 'Keep existing / choose new…';
const audioBox = document.getElementById('edit-tf-t1-audio-box');
if (audioBox) audioBox.style.borderColor = '';
// Reset language CSD
_editSetLangSelect('');
}
function _editGsSet(name, value, icon, label) {
const ico = document.getElementById('edit-gs-' + name + '-ico');
const txt = document.getElementById('edit-gs-' + name + '-txt');
const menu = document.getElementById('edit-gs-' + name + '-menu');
const btn = document.getElementById('edit-gs-' + name + '-btn');
if (ico) ico.className = 'bi ' + icon + ' um-gs-ico';
if (txt) txt.textContent = label;
if (menu) menu.querySelectorAll('[data-egs]').forEach(li => li.classList.toggle('active', li.dataset.value === value));
if (btn) { btn.classList.remove('open'); if (menu) menu.hidden = true; }
const inputId = {type:'edit-type-val', vis:'edit-vis-val', dl:'edit-dl-val'}[name];
if (inputId) document.getElementById(inputId).value = value;
}
// ── Apply mode ────────────────────────────────────────────────────────────────
function _editApplyMode(type) {
_editMode = type;
const isMusic = type === 'music';
document.getElementById('edit-add-track-btn').style.display = isMusic ? '' : 'none';
const lbl = document.getElementById('edit-tracks-section-label');
const sub = document.getElementById('edit-tracks-section-sub');
if (isMusic) {
if (lbl) lbl.textContent = 'Language Tracks';
if (sub) sub.textContent = 'Add audio tracks in different languages';
} else {
if (lbl) lbl.textContent = type === 'match' ? 'Match Video' : 'Video Details';
if (sub) sub.textContent = 'Click Edit to update title, description and media';
}
const videoZone = document.getElementById('edit-tf-t1-video-zone');
const thumbWrap = document.getElementById('edit-tf-t1-thumb-wrap');
const musicPair = document.getElementById('edit-tf-t1-music-pair');
if (isMusic) {
if (videoZone) videoZone.style.display = 'none';
if (thumbWrap) thumbWrap.style.display = 'none';
if (musicPair) musicPair.style.display = 'flex';
} else {
if (videoZone) videoZone.style.display = 'flex';
if (thumbWrap) thumbWrap.style.display = 'flex';
if (musicPair) musicPair.style.display = 'none';
}
document.getElementById('edit-type-val').value = type;
}
// ── Global Settings dropdowns ─────────────────────────────────────────────────
document.querySelectorAll('#editVideoModal .um-gs-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const name = this.id.replace('edit-gs-', '').replace('-btn', '');
const menu = document.getElementById('edit-gs-' + name + '-menu');
const wasOpen = !menu.hidden;
document.querySelectorAll('#editVideoModal .um-gs-menu').forEach(m => m.hidden = true);
document.querySelectorAll('#editVideoModal .um-gs-btn').forEach(b => b.classList.remove('open'));
if (!wasOpen) { menu.hidden = false; this.classList.add('open'); }
});
});
document.addEventListener('click', function(e) {
if (!e.target.closest('#editVideoModal .um-gs-wrap')) {
document.querySelectorAll('#editVideoModal .um-gs-menu').forEach(m => m.hidden = true);
document.querySelectorAll('#editVideoModal .um-gs-btn').forEach(b => b.classList.remove('open'));
}
});
document.querySelectorAll('[data-egs]').forEach(opt => {
opt.addEventListener('click', function() {
const name = this.dataset.egs;
const value = this.dataset.value;
const icon = this.dataset.icon;
const label = this.dataset.label;
_editGsSet(name, value, icon, label);
if (name === 'type') _editApplyMode(value);
});
});
// ── Helpers ───────────────────────────────────────────────────────────────────
function _editFmtSize(bytes) {
if (!bytes) return '0 B';
const k = 1024, sizes = ['B','KB','MB','GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k,i)).toFixed(2)) + ' ' + sizes[i];
}
// ── Track ordering ────────────────────────────────────────────────────────────
function editMoveTrack(cardSuffix, dir) {
const card = document.getElementById('edit-tc-' + cardSuffix);
if (!card) return;
const list = document.getElementById('edit-tc-list');
const cards = Array.from(list.querySelectorAll(':scope > .um-track-card'));
const idx = cards.indexOf(card);
console.log('%c[EditTrack] Reorder:', 'color:#3b82f6', { card: card.id, trackId: card.dataset.trackId || 'primary', direction: dir, fromPos: idx + 1 });
if (dir === 'up' && idx > 0) list.insertBefore(card, cards[idx - 1]);
if (dir === 'down' && idx < cards.length - 1) list.insertBefore(cards[idx + 1], card);
_editUpdateTrackPositions();
}
function _editUpdateTrackPositions() {
const list = document.getElementById('edit-tc-list');
if (!list) return;
const cards = Array.from(list.querySelectorAll(':scope > .um-track-card'));
const total = cards.length;
cards.forEach((card, i) => {
const numEl = card.querySelector('.um-tc-num');
if (numEl) numEl.textContent = i + 1;
const badge = card.querySelector('.um-tc-primary');
if (badge) badge.style.display = i === 0 ? '' : 'none';
const up = card.querySelector('.edit-tc-arrow-up');
const down = card.querySelector('.edit-tc-arrow-down');
if (up) up.style.display = (i === 0) ? 'none' : '';
if (down) down.style.display = (i === total - 1) ? 'none' : '';
});
// Set promote_track_id: non-empty only when a secondary is at position 1
const first = cards[0];
const promoteEl = document.getElementById('edit-promote-track-id');
if (promoteEl) {
const isSecondary = first && first.id !== 'edit-tc-t1-card';
promoteEl.value = isSecondary ? (first.dataset.trackId || '') : '';
console.log('%c[EditTrack] Position 1 is now:', 'color:#3b82f6',
isSecondary ? ('secondary track ' + promoteEl.value + ' (will be promoted to primary)') : 'original primary (no promotion)');
}
}
function _editSetLangCsd(wrap, code) {
if (!wrap || !code) return;
const opt = wrap.querySelector(`.csd-opt[data-v="${code}"]`);
const ico = wrap.querySelector('.csd-ico');
const valEl = wrap.querySelector('.csd-val');
if (!opt) return;
if (ico) ico.innerHTML = opt.querySelector('.csd-opt-ico').innerHTML;
if (valEl) { valEl.textContent = opt.querySelector('.csd-opt-main').textContent; valEl.classList.remove('ph'); }
wrap.querySelectorAll('.csd-opt').forEach(o => o.setAttribute('aria-selected', o === opt ? 'true' : 'false'));
}
function _editSetLangSelect(code) {
const inp = document.getElementById('edit_primary_language');
if (!inp) return;
inp.value = code;
const wrap = inp.closest('.csd-wrap');
if (!wrap) return;
const ico = wrap.querySelector('.csd-ico');
const valEl = wrap.querySelector('.csd-val');
if (!code) {
if (ico) ico.innerHTML = '<span class="fi fi-xx lsd-flag"></span>';
if (valEl) { valEl.textContent = 'Select language'; valEl.classList.add('ph'); }
wrap.querySelectorAll('.csd-opt').forEach(o => o.setAttribute('aria-selected','false'));
} else {
const opt = wrap.querySelector(`.csd-opt[data-v="${code}"]`);
if (opt) {
if (ico) ico.innerHTML = opt.querySelector('.csd-opt-ico').innerHTML;
if (valEl) { valEl.textContent = opt.querySelector('.csd-opt-main').textContent; valEl.classList.remove('ph'); }
wrap.querySelectorAll('.csd-opt').forEach(o => o.setAttribute('aria-selected', o===opt ? 'true':'false'));
}
}
}
// ── Video file handling ───────────────────────────────────────────────────────
const editVideoFile = document.getElementById('edit-video-file');
editVideoFile.addEventListener('change', function() { editHandleVideoSelect(this); });
const EDIT_AUDIO_EXTS = ['mp3','m4a','aac','flac','wav'];
function _editIsAudio(file) {
const ext = file.name.split('.').pop().toLowerCase();
return EDIT_AUDIO_EXTS.includes(ext) || file.type.startsWith('audio/');
}
function editHandleVideoSelect(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
const audio = _editIsAudio(file);
if (audio && _editMode !== 'music') {
_editApplyMode('music');
_editGsSet('type','music','bi-music-note-beamed','Music');
} else if (!audio && _editMode === 'music') {
_editApplyMode('generic');
_editGsSet('type','generic','bi-film','Generic');
}
if (audio) {
const fn = document.getElementById('edit-t1-fname');
if (fn) fn.textContent = file.name;
const audioBox = document.getElementById('edit-tf-t1-audio-box');
if (audioBox) audioBox.style.borderColor = '#22c55e';
} else {
document.getElementById('edit-t1-filename').textContent = file.name;
document.getElementById('edit-t1-filesize').textContent = _editFmtSize(file.size);
document.getElementById('edit-t1-dz-idle').style.display = 'none';
document.getElementById('edit-t1-file-info').style.display = 'flex';
}
editUpdateTrackCard('t1');
}
function editRemoveVideo(e) {
e.preventDefault(); e.stopPropagation();
editVideoFile.value = '';
document.getElementById('edit-t1-dz-idle').style.display = '';
document.getElementById('edit-t1-file-info').style.display = 'none';
}
// ── Thumbnail ─────────────────────────────────────────────────────────────────
const editThumbDz = document.getElementById('edit-t1-thumbnail-dropzone');
let editThumbInp = document.getElementById('edit-t1-thumbnail-input');
editThumbDz.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
if (typeof window.openCropperModal_thumb_edit === 'function') {
window.openCropperModal_thumb_edit();
const internal = document.getElementById('tcInput_thumb_edit');
if (internal) internal.click();
} else { editThumbInp.click(); }
});
editThumbDz.addEventListener('dragover', e => { e.preventDefault(); editThumbDz.style.borderColor='#e61e1e'; });
editThumbDz.addEventListener('dragleave', () => { editThumbDz.style.borderColor=''; });
editThumbDz.addEventListener('drop', e => {
e.preventDefault(); editThumbDz.style.borderColor='';
if (e.dataTransfer.files.length) {
if (typeof window.tcPreload_thumb_edit === 'function') {
window.tcPreload_thumb_edit(e.dataTransfer.files[0]);
window.openCropperModal_thumb_edit();
} else { editThumbInp.files = e.dataTransfer.files; editHandleThumbnail(editThumbInp, 't1'); }
}
});
function editHandleThumbnail(input, prefix) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
document.getElementById('edit-' + prefix + '-thumbnail-fname').textContent = file.name;
document.getElementById('edit-' + prefix + '-thumbnail-fsize').textContent = _editFmtSize(file.size);
const reader = new FileReader();
reader.onload = e => { document.getElementById('edit-' + prefix + '-thumbnail-preview').src = e.target.result; };
reader.readAsDataURL(file);
document.getElementById('edit-' + prefix + '-thumbnail-ph').style.display = 'none';
document.getElementById('edit-' + prefix + '-thumbnail-info').style.display = 'flex';
}
function editRemoveThumbnail(e, prefix) {
prefix = prefix || 't1';
if (e) { e.preventDefault(); e.stopPropagation(); }
if (prefix === 't1') {
const fresh = editThumbInp.cloneNode(false);
editThumbInp.parentNode.replaceChild(fresh, editThumbInp);
editThumbInp = fresh;
fresh.setAttribute('onchange', "editHandleThumbnail(this,'t1')");
}
document.getElementById('edit-' + prefix + '-thumbnail-ph').style.display = '';
document.getElementById('edit-' + prefix + '-thumbnail-info').style.display = 'none';
document.getElementById('edit-' + prefix + '-thumbnail-preview').src = '';
}
// ── Slides ────────────────────────────────────────────────────────────────────
function editSlidesZoneClick(e, tid) { tid=tid||'t1'; if (e.target.closest('button')) return; document.getElementById('edit-'+tid+'-slides-input').click(); }
function editSlidesZoneDragover(e, tid) { tid=tid||'t1'; if (Array.from(e.dataTransfer.types||[]).includes('Files')) { e.preventDefault(); document.getElementById('edit-'+tid+'-slides-zone').classList.add('dragover'); } }
function editSlidesZoneDragleave(tid) { tid=tid||'t1'; document.getElementById('edit-'+tid+'-slides-zone').classList.remove('dragover'); }
function editSlidesZoneDrop(e, tid) { tid=tid||'t1'; e.preventDefault(); document.getElementById('edit-'+tid+'-slides-zone').classList.remove('dragover'); if (e.dataTransfer.files.length) editHandleSlides(e.dataTransfer.files, tid); }
function editHandleSlides(fileList, tid) {
tid = tid || 't1';
_editSlidesCropStart(fileList, tid, _editRenderSlides);
}
// ── Slides crop queue: every added image is cropped before it enters the strip ──
let _editSlidesCropQueue = [];
let _editSlidesCropTid = null;
let _editSlidesCropRender = null;
function _editSlidesCropStart(fileList, tid, renderFn) {
const imgs = Array.from(fileList || []).filter(f => f.type && f.type.startsWith('image/'));
if (!imgs.length) return;
_editSlidesCropTid = tid;
_editSlidesCropRender = renderFn;
_editSlidesCropQueue = imgs;
_editSlidesCropLoadNext();
}
function _editSlidesCropLoadNext() {
if (!_editSlidesCropQueue.length) { window.closeCropperModal('slides_edit'); return; }
const f = _editSlidesCropQueue.shift();
if (typeof window.tcPreload_slides_edit === 'function') {
window.tcPreload_slides_edit(f);
window.openCropperModal_slides_edit();
}
}
function editSlidesCropDone(file) {
const tid = _editSlidesCropTid;
if (tid && file) {
if (!_editSlidesData[tid]) _editSlidesData[tid] = [];
if (_editSlidesData[tid].length < 10) {
const reader = new FileReader();
reader.onload = ev => {
_editSlidesData[tid].push({ file, url: ev.target.result });
if (_editSlidesCropRender) _editSlidesCropRender(tid);
_editSlidesCropLoadNext();
};
reader.readAsDataURL(file);
return;
}
}
_editSlidesCropLoadNext();
}
function editClearSlides(e, tid) {
tid = tid || 't1';
if (e) { e.preventDefault(); e.stopPropagation(); }
_editSlidesData[tid] = [];
const inp = document.getElementById('edit-' + tid + '-slides-input');
if (inp) { const fresh = inp.cloneNode(false); fresh.setAttribute('onchange',"editHandleSlides(this.files,'"+tid+"')"); inp.parentNode.replaceChild(fresh, inp); }
_editRenderSlides(tid);
}
let _editSlidesDrag = null;
function _editRenderSlides(tid) {
const files = _editSlidesData[tid] || [];
const strip = document.getElementById('edit-' + tid + '-slides-strip');
const ph = document.getElementById('edit-' + tid + '-slides-ph');
const prev = document.getElementById('edit-' + tid + '-slides-preview');
const cnt = document.getElementById('edit-' + tid + '-slides-count');
if (!strip) return;
strip.innerHTML = '';
if (!files.length) {
if (ph) ph.style.display = '';
if (prev) prev.style.display = 'none';
document.getElementById('edit-' + tid + '-slides-order').value = '[]';
return;
}
files.forEach((item, index) => {
const wrap = document.createElement('div');
wrap.style.cssText = 'position:relative;flex-shrink:0;cursor:grab;';
wrap.draggable = true; wrap.dataset.index = index;
wrap.addEventListener('dragstart', ev => { _editSlidesDrag = index; ev.dataTransfer.effectAllowed='move'; setTimeout(()=>{wrap.style.opacity='.4';},0); });
wrap.addEventListener('dragend', () => { _editSlidesDrag = null; wrap.style.opacity=''; });
wrap.addEventListener('dragover', ev => { if (_editSlidesDrag==null) return; ev.preventDefault(); wrap.style.outline='2px solid #e61e1e'; });
wrap.addEventListener('dragleave', () => { wrap.style.outline=''; });
wrap.addEventListener('drop', ev => {
ev.preventDefault(); ev.stopPropagation(); wrap.style.outline='';
if (_editSlidesDrag==null || _editSlidesDrag===index) return;
const arr = _editSlidesData[tid];
const [moved] = arr.splice(_editSlidesDrag, 1);
arr.splice(index, 0, moved);
_editRenderSlides(tid);
});
const rb = document.createElement('button');
rb.type='button'; rb.style.cssText='position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:#e61e1e;border:none;border-radius:50%;color:#fff;font-size:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;z-index:1;';
rb.innerHTML='<i class="bi bi-x"></i>';
rb.addEventListener('click', ev => { ev.stopPropagation(); _editSlidesData[tid].splice(index,1); _editRenderSlides(tid); });
const img = document.createElement('img');
img.style.cssText='width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.12);display:block;'; img.alt=''; img.src=item.url;
wrap.appendChild(img); wrap.appendChild(rb); strip.appendChild(wrap);
});
const ids = files.filter(s => s.id).map(s => s.id);
document.getElementById('edit-' + tid + '-slides-order').value = JSON.stringify(ids);
if (cnt) cnt.textContent = files.length===1 ? '1 image — static cover' : files.length+' images — will crossfade during playback';
if (ph) ph.style.display = 'none';
if (prev) prev.style.display = '';
document.getElementById('edit-' + tid + '-slides-zone').style.borderColor = '#22c55e';
}
// ── Slides helpers for extra tracks ──────────────────────────────────────────
function editSlidesZoneClickE(e, tid) { if (e.target.closest('button')) return; document.getElementById('edit-slides-input-'+tid).click(); }
function editSlidesZoneDragoverE(e, tid) { if (Array.from(e.dataTransfer.types||[]).includes('Files')) { e.preventDefault(); document.getElementById('edit-slides-zone-'+tid).classList.add('dragover'); } }
function editSlidesZoneDragleaveE(tid) { document.getElementById('edit-slides-zone-'+tid).classList.remove('dragover'); }
function editSlidesZoneDropE(e, tid) { e.preventDefault(); document.getElementById('edit-slides-zone-'+tid).classList.remove('dragover'); if (e.dataTransfer.files.length) editHandleSlidesForTrack(tid, e.dataTransfer.files); }
function editHandleSlidesForTrack(tid, fileList) {
_editSlidesCropStart(fileList, tid, _editRenderSlidesForTrack);
}
function editClearSlidesForTrack(e, tid) {
if (e) { e.preventDefault(); e.stopPropagation(); }
_editSlidesData[tid] = [];
const inp = document.getElementById('edit-slides-input-'+tid);
if (inp) { const fresh = inp.cloneNode(false); fresh.setAttribute('onchange', "editHandleSlidesForTrack('"+tid+"',this.files)"); inp.parentNode.replaceChild(fresh, inp); }
_editRenderSlidesForTrack(tid);
}
let _editExtraSlidesDrag = null;
function _editRenderSlidesForTrack(tid) {
const files = _editSlidesData[tid] || [];
const strip = document.getElementById('edit-slides-strip-'+tid);
const ph = document.getElementById('edit-slides-ph-'+tid);
const prev = document.getElementById('edit-slides-preview-'+tid);
const cnt = document.getElementById('edit-slides-count-'+tid);
const zone = document.getElementById('edit-slides-zone-'+tid);
if (!strip) return;
strip.innerHTML = '';
if (!files.length) {
if (ph) ph.style.display = '';
if (prev) prev.style.display = 'none';
return;
}
files.forEach((item, index) => {
const wrap = document.createElement('div');
wrap.style.cssText = 'position:relative;flex-shrink:0;cursor:grab;';
wrap.draggable = true; wrap.dataset.index = index;
wrap.addEventListener('dragstart', ev => { _editExtraSlidesDrag = {tid, index}; ev.dataTransfer.effectAllowed='move'; setTimeout(()=>{wrap.style.opacity='.4';},0); });
wrap.addEventListener('dragend', () => { _editExtraSlidesDrag = null; wrap.style.opacity=''; });
wrap.addEventListener('dragover', ev => { if (!_editExtraSlidesDrag || _editExtraSlidesDrag.tid !== tid) return; ev.preventDefault(); wrap.style.outline='2px solid #e61e1e'; });
wrap.addEventListener('dragleave', () => { wrap.style.outline=''; });
wrap.addEventListener('drop', ev => {
ev.preventDefault(); ev.stopPropagation(); wrap.style.outline='';
if (!_editExtraSlidesDrag || _editExtraSlidesDrag.tid !== tid || _editExtraSlidesDrag.index === index) return;
const arr = _editSlidesData[tid];
const [moved] = arr.splice(_editExtraSlidesDrag.index, 1);
arr.splice(index, 0, moved);
_editRenderSlidesForTrack(tid);
});
const rb = document.createElement('button');
rb.type='button'; rb.style.cssText='position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:#e61e1e;border:none;border-radius:50%;color:#fff;font-size:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;z-index:1;';
rb.innerHTML='<i class="bi bi-x"></i>';
rb.addEventListener('click', ev => { ev.stopPropagation(); _editSlidesData[tid].splice(index,1); _editRenderSlidesForTrack(tid); });
const img = document.createElement('img');
img.style.cssText='width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.12);display:block;'; img.alt=''; img.src=item.url;
wrap.appendChild(img); wrap.appendChild(rb); strip.appendChild(wrap);
});
if (cnt) cnt.textContent = files.length===1 ? '1 image — static cover' : files.length+' images — will crossfade during playback';
if (ph) ph.style.display = 'none';
if (prev) prev.style.display = '';
if (zone) zone.style.borderColor = '#22c55e';
}
// ── Track popup ───────────────────────────────────────────────────────────────
function editOpenTrackPopup(trackId) {
document.querySelectorAll('#edit-track-popup .um-track-form').forEach(f => f.style.display = 'none');
const form = document.getElementById('edit-tf-' + trackId);
if (form) form.style.display = 'block';
const popup = document.getElementById('edit-track-popup');
popup.style.opacity = '0';
popup.style.display = 'flex';
requestAnimationFrame(() => { popup.style.opacity = '1'; });
}
function editCloseTrackPopup() {
document.querySelectorAll('#edit-track-popup .um-track-form').forEach(f => f.style.display = 'none');
document.getElementById('edit-track-popup').style.display = 'none';
editUpdateTrackCard('t1');
for (let i = 1; i <= _editExtraCount; i++) {
if (document.getElementById('edit-tc-e'+i)) editUpdateTrackCard('e'+i);
}
}
function editUpdateTrackCard(trackId) {
let langCode = '', title = '';
if (trackId === 't1') {
const li = document.getElementById('edit_primary_language');
const ti = document.getElementById('edit-track1-title');
langCode = li ? li.value : '';
title = ti ? ti.value.trim() : '';
} else {
const n = trackId.replace('e','');
const li = document.getElementById('csd_v_e' + n);
const ti = document.getElementById('edit-e' + n + '-title');
langCode = li ? li.value : '';
title = ti ? ti.value.trim() : '';
}
const OPTS = LANG_OPTIONS_EDIT_MODAL;
const lang = OPTS.find(o => o.value === langCode);
const flagEl = document.getElementById('edit-tc-flag-' + trackId);
const titleEl = document.getElementById('edit-tc-title-' + trackId);
if (flagEl) flagEl.className = 'fi fi-' + (lang ? lang.flag : 'xx') + ' um-tc-flag';
if (titleEl) {
if (lang && title) titleEl.textContent = title + ' — ' + lang.label;
else if (lang) titleEl.textContent = lang.label;
else if (title) titleEl.textContent = title;
else titleEl.textContent = '';
}
}
// ── Extra track management ────────────────────────────────────────────────────
const LANG_OPTIONS_EDIT_MODAL = @json(\App\Data\Languages::forLanguage());
function _editCloneTrackForm(prefix, langName, titleName, descName, audioName) {
const tpl = document.getElementById('edit-extra-track-tpl');
if (!tpl) return null;
const html = tpl.innerHTML
.replaceAll('__TPL__', prefix)
.replaceAll('__LANGNAME__', langName)
.replaceAll('__TITLENAME__', titleName)
.replaceAll('__DESCNAME__', descName)
.replaceAll('__AUDIONAME__', audioName);
const tmp = document.createElement('div');
tmp.innerHTML = html;
const formEl = tmp.querySelector('.um-track-form');
if (!formEl) return null;
formEl.style.display = 'none';
document.getElementById('edit-tf-extra').appendChild(formEl);
_editSlidesData[prefix] = [];
if (window.CSD) new CSD('csd_' + prefix);
return formEl;
}
function _editTrackCard(n, trackId, isExisting) {
const card = document.createElement('div');
card.className = 'um-track-card'; card.id = 'edit-tc-e' + n;
card.dataset.trackId = trackId || '';
const deleteBtn = isExisting
? `<button type="button" class="action-btn action-btn-danger icon-only" onclick="editDeleteTrack(${trackId},${n})" title="Delete"><i class="bi bi-trash"></i></button>`
: `<button type="button" class="action-btn action-btn-danger icon-only" onclick="editRemoveNewTrack(${n})" title="Remove"><i class="bi bi-trash"></i></button>`;
card.innerHTML = `
<div class="um-tc-body">
<div class="um-tc-left">
<div class="um-tc-num">${n + 1}</div>
<span class="fi fi-xx um-tc-flag" id="edit-tc-flag-e${n}"></span>
<div class="um-tc-info">
<span class="um-tc-title" id="edit-tc-title-e${n}"></span>
<span class="um-tc-primary" style="display:none;"><i class="bi bi-star-fill"></i> Primary</span>
</div>
</div>
<div class="um-tc-right">
<button type="button" class="action-btn icon-only edit-tc-arrow-up" onclick="editMoveTrack('e${n}','up')" title="Move up" style="display:none;"><i class="bi bi-arrow-up"></i></button>
<button type="button" class="action-btn icon-only edit-tc-arrow-down" onclick="editMoveTrack('e${n}','down')" title="Move down" style="display:none;"><i class="bi bi-arrow-down"></i></button>
<button type="button" class="action-btn" onclick="editOpenTrackPopup('e${n}')"><i class="bi bi-pencil"></i> <span>Edit</span></button>
${deleteBtn}
</div>
</div>`;
document.getElementById('edit-tc-list').appendChild(card);
return card;
}
function _editAddExistingTrack(track) {
const n = ++_editExtraCount;
const pfx = 'e' + n;
_editTrackCard(n, track.id, true);
_editCloneTrackForm(
pfx,
'track_language_updates[' + track.id + ']',
'track_title_updates[' + track.id + ']',
'track_description_updates[' + track.id + ']',
'track_file_updates[' + track.id + ']'
);
const titleEl = document.getElementById('edit-' + pfx + '-title');
if (titleEl) titleEl.value = track.title || '';
const descEl = document.getElementById('edit-' + pfx + '-desc');
if (descEl) { descEl.value = track.description || ''; descEl.dispatchEvent(new Event('rte:refresh')); }
if (track.language && window.CSD) {
const hi = document.getElementById('csd_v_' + pfx);
const wrap = document.getElementById('csd_' + pfx);
if (hi) hi.value = track.language;
if (wrap) _editSetLangCsd(wrap, track.language);
}
const hi = document.getElementById('csd_v_' + pfx);
if (hi) hi.addEventListener('change', () => editUpdateTrackCard(pfx));
editUpdateTrackCard(pfx);
_editUpdateTrackPositions();
}
function editDeleteTrack(trackId, n) {
_editDeleteTrackIds.push(trackId);
document.getElementById('edit-tc-e'+n)?.remove();
document.getElementById('edit-tf-e'+n)?.remove();
const popup = document.getElementById('edit-track-popup');
if (popup.style.display !== 'none') {
const vis = popup.querySelector('.um-track-form:not([style*="none"])');
if (!vis || vis.id === 'edit-tf-e'+n) popup.style.display = 'none';
}
_editUpdateTrackPositions();
}
function editAddExtraTrack() {
const n = ++_editExtraCount;
const pfx = 'e' + n;
_editTrackCard(n, null, false);
_editCloneTrackForm(
pfx,
'extra_track_languages[]',
'extra_track_titles[]',
'extra_track_descriptions[]',
'extra_track_files[]'
);
const hi = document.getElementById('csd_v_' + pfx);
if (hi) hi.addEventListener('change', () => editUpdateTrackCard(pfx));
_editUpdateTrackPositions();
editOpenTrackPopup(pfx);
}
function editRemoveNewTrack(n) {
document.getElementById('edit-tc-e'+n)?.remove();
document.getElementById('edit-tf-e'+n)?.remove();
delete _editSlidesData['e'+n];
const popup = document.getElementById('edit-track-popup');
if (popup.style.display !== 'none') {
const vis = popup.querySelector('.um-track-form:not([style*="none"])');
if (!vis || vis.id === 'edit-tf-e'+n) popup.style.display = 'none';
}
}
// Wire primary language → update card
;(function() {
const li = document.getElementById('edit_primary_language');
if (li) li.addEventListener('change', () => editUpdateTrackCard('t1'));
}());
// ── Form submission ───────────────────────────────────────────────────────────
document.getElementById('edit-form').addEventListener('submit', function(e) {
e.preventDefault();
const titleVal = (document.getElementById('edit-track1-title')?.value || '').trim();
if (!titleVal) {
document.getElementById('edit-status-msg').innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> Please enter a title';
document.getElementById('edit-status-msg').className = 'status-message-modal error';
editOpenTrackPopup('t1');
return;
}
const formData = new FormData(this);
// Append new slide files (track 1)
(_editSlidesData['t1'] || []).filter(s => s.file).forEach(s => formData.append('slides_add[]', s.file, s.file.name));
// Append new slide files for extra tracks
for (let i = 1; i <= _editExtraCount; i++) {
const tid = 'e'+i;
(_editSlidesData[tid] || []).filter(s => s.file).forEach(s => formData.append('extra_track_slides_'+tid+'[]', s.file, s.file.name));
}
// Append delete IDs
_editDeleteTrackIds.forEach(id => formData.append('delete_track_ids[]', id));
// ── Log tracker: dump the exact track-related payload being submitted ──────
(function () {
const promoteId = document.getElementById('edit-promote-track-id')?.value || '(none)';
const order = Array.from(document.querySelectorAll('#edit-tc-list > .um-track-card')).map((c, i) => ({
pos: i + 1,
card: c.id,
trackId: c.dataset.trackId || 'primary',
title: c.querySelector('.um-tc-title')?.textContent?.trim() || ''
}));
const trackFields = {};
for (const [k, v] of formData.entries()) {
if (/^(promote_track_id|primary_language|title|description|track_|extra_track_|delete_track_ids)/.test(k)) {
trackFields[k] = (v instanceof File) ? `[File: ${v.name}]` : v;
}
}
console.groupCollapsed('%c[EditTrack] Submitting edit for video ' + window._editCurrentVideoId, 'color:#e61e1e;font-weight:700');
console.log('Primary language:', document.getElementById('edit_primary_language')?.value || '(none)');
console.log('Promote track id (secondary→primary):', promoteId);
console.table(order);
console.log('Delete track ids:', _editDeleteTrackIds);
console.log('Track form fields:', trackFields);
console.groupEnd();
})();
const btn = document.getElementById('edit-submit-btn');
const st = document.getElementById('edit-status-msg');
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Saving…';
if (st) st.className = 'status-message-modal';
fetch(`/videos/${window._editCurrentVideoId}`, {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(r => { if (!r.ok) throw new Error('Server error'); return r.json(); })
.then(data => {
console.log('%c[EditTrack] Server response:', 'color:#22c55e;font-weight:700', data);
if (data.success) {
if (st) {
st.innerHTML = '<i class="bi bi-check-circle-fill"></i> ' + (data.message || 'Saved!');
st.className = 'status-message-modal success';
}
setTimeout(() => { closeEditVideoModal(); window.location.reload(); }, 1200);
} else {
throw new Error(data.message || 'Update failed');
}
})
.catch(err => {
console.error('%c[EditTrack] Save failed:', 'color:#e61e1e;font-weight:700', err);
if (st) {
st.innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + (err.message || 'Save failed');
st.className = 'status-message-modal error';
}
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg"></i> <span>Save Changes</span>';
});
});
// Initialise
_editApplyMode('generic');
</script>
<x-image-cropper
id="thumb_edit"
:width="448"
:height="252"
shape="square"
target-input="edit-t1-thumbnail-input"
preview-img="edit-t1-thumbnail-preview"
output-width="1280"
title="Crop Thumbnail"
/>
<x-image-cropper
id="slides_edit"
:width="448"
:height="252"
shape="square"
output-width="1280"
title="Crop Cover Slide"
result-callback="editSlidesCropDone"
/>