Add multi-language audio tracks and self-hosted flag-icons

Introduce per-video language support and multiple audio tracks
(VideoAudioTrack model + migrations for language, description, title),
a reusable language-select component, and a track-editor form. Bundle
the self-hosted flag-icons v7.2.3 library and a NAS auto-sync command.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-05-22 21:32:52 +03:00
1 parent f8d13457fa
commit 66fd78c10f
314 files changed
+16507 -3172

No files matched your search

+490 -91
View File
@@ -88,6 +88,29 @@
.option-content span { font-size: 13px; color: #aaa; }
.option-item.active .option-content span { color: #fff; }
/* Language track accordion (create page) */
.lta-create { display: flex; flex-direction: column; gap: 6px; margin-bottom: 0; }
.ltac-item { background: #151515; border: 1px solid #333; border-radius: 10px; overflow: hidden; transition: border-color .2s; }
.ltac-item.open { border-color: rgba(230,30,30,.45); }
.ltac-header { display: flex; align-items: center; justify-content: space-between; padding: 11px 14px; cursor: pointer; user-select: none; gap: 8px; }
.ltac-hd-left { display: flex; align-items: center; gap: 9px; flex: 1; min-width: 0; }
.ltac-num { width: 22px; height: 22px; border-radius: 50%; background: rgba(230,30,30,.15); border: 1px solid rgba(230,30,30,.3); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: var(--brand-red); flex-shrink: 0; }
.ltac-flag-cell { flex-shrink: 0; }
.ltac-name { font-size: 13px; color: #888; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ltac-item.open .ltac-name { color: #e5e5e5; }
.ltac-arr { font-size: 12px; color: #555; transition: transform .2s, color .2s; flex-shrink: 0; }
.ltac-body { padding: 12px 14px 14px; border-top: 1px solid #2a2a2a; }
.ltac-item:not(.open) .ltac-body { display: none; }
.ltac-remove { background: none; border: none; color: #555; cursor: pointer; font-size: 13px; padding: 3px 6px; border-radius: 4px; transition: color .15s, background .15s; display: flex; align-items: center; }
.ltac-remove:hover { color: var(--brand-red); background: rgba(230,30,30,.1); }
.ltac-file-label { display: flex; align-items: center; gap: 8px; background: #121212; border: 1px solid #333; border-radius: 8px; padding: 10px 12px; cursor: pointer; transition: border-color .15s; }
.ltac-file-name { font-size: 12px; color: #888; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.ltac-slides-zone { border: 1.5px dashed #333; border-radius: 10px; min-height: 70px; background: #121212; cursor: pointer; transition: border-color .2s; display: flex; align-items: center; justify-content: center; }
.ltac-slides-zone:hover { border-color: var(--brand-red); }
.ltac-slides-zone.dragover { border-color: var(--brand-red); background: rgba(230,30,30,.05); }
.ltac-slides-ph { display: flex; align-items: center; gap: 8px; color: #555; font-size: 13px; padding: 16px; }
.ltac-slides-ph i { color: #444; }
/* Two Column Layout */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@@ -145,7 +168,7 @@
@csrf
<!-- Video File Dropzone -->
<div class="form-group">
<div class="form-group" id="file-dropzone-group">
<label class="form-label"><i class="bi bi-camera-video" id="file-label-icon"></i> <span id="file-label-text">Video or Music File *</span></label>
<div id="dropzone">
<input type="file" name="video" id="video" accept="video/*,audio/mpeg,audio/mp4,audio/aac,audio/flac,audio/wav,.mp3,.m4a,.aac,.flac,.wav" required>
@@ -165,16 +188,16 @@
</div>
</div>
<!-- Title -->
<div class="form-group">
<label class="form-label">Title *</label>
<input type="text" name="title" id="video-title" required class="form-input" placeholder="Enter video title">
</div>
<!-- Description -->
<div class="form-group">
<label class="form-label">Description</label>
<textarea name="description" id="video-description" rows="4" class="form-textarea" placeholder="Tell viewers about your video (Markdown supported)"></textarea>
<!-- Title + Description (video mode) -->
<div id="basic-fields-create">
<div class="form-group">
<label class="form-label">Title *</label>
<input type="text" name="title" id="video-title" class="form-input" placeholder="Enter video title">
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea name="description" id="video-description" rows="4" class="form-textarea" placeholder="Tell viewers about your video (Markdown supported)"></textarea>
</div>
</div>
<!-- Thumbnail (video) -->
@@ -184,7 +207,7 @@
<input type="file" name="thumbnail" id="thumbnail" accept="image/*">
<div id="thumbnail-default">
<i class="bi bi-card-image"></i>
<p id="thumbnail-dropzone-hint">Click to select thumbnail (optional, max 20MB)</p>
<p id="thumbnail-dropzone-hint">Click to select thumbnail (optional)</p>
</div>
<div id="thumbnail-info">
<div style="display:flex;align-items:center;gap:12px;padding:12px;background:#1f1f1f;border-radius:8px;border:1px solid #333;">
@@ -201,28 +224,105 @@
</div>
</div>
<!-- Slides dropzone (audio only) -->
<div class="form-group" id="slides-section" style="display:none;">
<label class="form-label">
<i class="bi bi-images"></i> Slides <span style="color:var(--brand-red);">*</span>
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:6px;">Multiple images will crossfade while playing</span>
</label>
<div id="slides-dropzone" style="border:2px dashed var(--brand-red);border-radius:10px;padding:12px;cursor:pointer;text-align:center;min-height:72px;">
<input type="file" name="slides[]" id="slides-input" accept="image/*" multiple style="display:none;">
<div id="slides-default">
<i class="bi bi-images" style="font-size:28px;color:var(--text-secondary);"></i>
<p style="color:var(--text-secondary);font-size:13px;margin:4px 0 0;">Click or drag to add cover images</p>
</div>
<div id="slides-preview" style="display:none;">
<div id="slides-strip" style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:8px;"></div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:0 4px;">
<span id="slides-count" style="font-size:12px;color:var(--text-secondary);"></span>
<button type="button" onclick="removeSlides(event)" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;"><i class="bi bi-x-lg"></i> Clear</button>
<!-- Language Tracks accordion (audio mode only) -->
<div class="form-group" id="lang-tracks-section-create" style="display:none;">
<label class="form-label"><i class="bi bi-translate"></i> Language Tracks</label>
<div id="lta-create" class="lta-create">
<!-- Track 1 — always present, primary -->
<div class="ltac-item open" id="ltac-t1">
<div class="ltac-header" onclick="ltToggleCreate('t1')">
<div class="ltac-hd-left">
<span class="ltac-num">1</span>
<span class="ltac-flag-cell" id="ltac-t1-flag">
<span class="fi fi-xx lsd-flag" style="width:22px;height:16px;"></span>
</span>
<span class="ltac-name" id="ltac-t1-name">Primary — Select language</span>
</div>
<i class="bi bi-chevron-up ltac-arr" style="color:var(--brand-red);" id="ltac-t1-arr"></i>
</div>
<div class="ltac-body" id="ltac-t1-body">
<!-- Audio File Picker -->
<div style="margin-bottom:12px;" id="ltc-track1-file-wrap">
<label class="form-label" style="font-size:13px;margin-bottom:6px;"><i class="bi bi-music-note-beamed"></i> Audio File *</label>
<label class="ltac-file-label"
onclick="document.getElementById('video').click()"
onmouseenter="this.style.borderColor='var(--brand-red)'"
onmouseleave="this.style.borderColor='#333'">
<i class="bi bi-music-note-beamed" style="color:var(--brand-red);font-size:16px;flex-shrink:0;"></i>
<span class="ltac-file-name" id="ltc-track1-fname">Choose audio file…</span>
</label>
</div>
<!-- Title (audio mode) -->
<div style="margin-bottom:12px;">
<label class="form-label" style="font-size:13px;margin-bottom:6px;">Title *</label>
<input type="text" id="lt-track1-title-create" class="form-input"
style="font-size:13px;" placeholder="Track title…" autocomplete="off">
</div>
<!-- Description (audio mode) -->
<div style="margin-bottom:14px;">
<label class="form-label" style="font-size:13px;margin-bottom:6px;">Description</label>
<textarea id="lt-track1-desc-create" class="form-textarea"
rows="2" style="font-size:13px;min-height:64px;"
placeholder="Tell viewers about this track…"></textarea>
</div>
<!-- Slides -->
<div style="margin-bottom:14px;">
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
<i class="bi bi-images"></i> Cover Slides <span style="color:var(--brand-red);">*</span>
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:4px;">drag to reorder</span>
</label>
<div id="slides-zone-ct1" class="ltac-slides-zone"
onclick="cSlidesZoneClick(event,'ct1')"
ondragover="cSlidesZoneDragover(event,'ct1')"
ondragleave="cSlidesZoneDragleave(event,'ct1')"
ondrop="cSlidesZoneDrop(event,'ct1')">
<input type="file" accept="image/*" multiple style="display:none" id="slides-input-ct1"
onchange="handleCSlidesForTrack('ct1',this.files)">
<div class="ltac-slides-ph" id="slides-ph-ct1">
<i class="bi bi-images" style="font-size:22px;"></i>
<span>Click or drag to add cover images</span>
</div>
<div id="slides-preview-ct1" style="display:none;padding:10px 12px;width:100%;box-sizing:border-box;">
<div id="slides-strip-ct1" class="slides-strip" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;"></div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span id="slides-count-ct1" style="font-size:12px;color:var(--text-secondary);"></span>
<button type="button" onclick="clearCSlidesForTrack(event,'ct1')"
style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:0;display:flex;align-items:center;gap:4px;">
<i class="bi bi-x-lg"></i> Clear
</button>
</div>
</div>
</div>
</div>
<!-- Primary Language -->
<div>
<x-language-select
name="primary_language"
id="primary_language_create"
label="Language"
placeholder="Select language"
/>
</div>
</div>
</div>
<!-- Extra tracks appended here by JS -->
<div id="ltac-extra"></div>
</div>
<button type="button" class="action-btn" onclick="addLangTrackCreate()"
style="font-size:13px;margin-top:8px;">
<i class="bi bi-plus-circle"></i> <span>Add Language Track</span>
</button>
</div>
<!-- Video Type -->
<div class="options-section">
<span class="options-label">Video Type</span>
@@ -345,9 +445,16 @@
const fileInput = document.getElementById('video');
let thumbnailInput = document.getElementById('thumbnail');
const thumbnailDropzone = document.getElementById('thumbnail-dropzone');
const slidesDropzone = document.getElementById('slides-dropzone');
let slidesInput = document.getElementById('slides-input');
let currentFile = null;
let _pageUploadInProgress = false;
// Warn before navigating away when a file is selected or upload is running
window.addEventListener('beforeunload', function(e) {
if (currentFile && !_pageUploadInProgress) {
e.preventDefault();
e.returnValue = '';
}
});
// ── Helpers ──────────────────────────────────────────────
function formatFileSize(bytes) {
@@ -367,10 +474,29 @@
document.getElementById('file-label-icon').className = audio ? 'bi bi-music-note-beamed' : 'bi bi-camera-video';
document.getElementById('file-label-text').textContent = audio ? 'Music File *' : 'Video or Music File *';
// Toggle thumbnail vs slides section
document.getElementById('thumbnail-section').style.display = audio ? 'none' : '';
document.getElementById('slides-section').style.display = audio ? '' : 'none';
slidesDropzone.style.borderColor = audio ? 'var(--brand-red)' : '';
// Show/hide sections
document.getElementById('file-dropzone-group').style.display = audio ? 'none' : '';
document.getElementById('ltc-track1-file-wrap').style.display = audio ? '' : 'none';
document.getElementById('basic-fields-create').style.display = audio ? 'none' : '';
document.getElementById('thumbnail-section').style.display = audio ? 'none' : '';
document.getElementById('lang-tracks-section-create').style.display = audio ? '' : 'none';
if (audio) {
const z = document.getElementById('slides-zone-ct1');
if (z && !(_cSlidesData['ct1'] && _cSlidesData['ct1'].length)) z.style.borderColor = 'var(--brand-red)';
}
// Swap name attributes so only the active set submits
const outerTitle = document.getElementById('video-title');
const outerDesc = document.getElementById('video-description');
const innerTitle = document.getElementById('lt-track1-title-create');
const innerDesc = document.getElementById('lt-track1-desc-create');
if (audio) {
outerTitle.removeAttribute('name'); outerDesc.removeAttribute('name');
innerTitle.setAttribute('name', 'title'); innerDesc.setAttribute('name', 'description');
} else {
outerTitle.setAttribute('name', 'title'); outerDesc.setAttribute('name', 'description');
innerTitle.removeAttribute('name'); innerDesc.removeAttribute('name');
}
if (audio) {
document.querySelectorAll('#type-options .option-item').forEach(o => o.classList.remove('active'));
@@ -399,7 +525,6 @@
function handleFileSelect(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
const maxSize = 512 * 1024 * 1024;
const validVideoTypes = ['video/mp4','video/webm','video/ogg','video/quicktime','video/x-msvideo','video/x-flv','video/x-matroska'];
const validVideoExts = ['.mp4','.webm','.ogg','.mov','.avi','.wmv','.flv','.mkv'];
const ext = '.' + file.name.split('.').pop().toLowerCase();
@@ -410,20 +535,22 @@
showError('Invalid format. Accepted: MP4, MOV, AVI, WebM, MKV, MP3, M4A, AAC, FLAC, WAV.');
input.value = ''; return;
}
if (file.size > maxSize) {
showError('File size exceeds 512MB limit.');
input.value = ''; return;
}
currentFile = file;
setAudioMode(audio);
const titleWithoutExt = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
document.getElementById('video-title').value = titleWithoutExt;
if (audio) {
document.getElementById('lt-track1-title-create').value = titleWithoutExt;
} else {
document.getElementById('video-title').value = titleWithoutExt;
}
document.getElementById('filename').textContent = file.name;
document.getElementById('filesize').textContent = formatFileSize(file.size);
document.getElementById('dropzone-default').style.display = 'none';
document.getElementById('file-info').classList.add('active');
const cFname = document.getElementById('ltc-track1-fname');
if (cFname) cFname.textContent = file.name;
}
function removeVideo(e) {
@@ -431,7 +558,11 @@
fileInput.value = ''; currentFile = null;
document.getElementById('dropzone-default').style.display = 'block';
document.getElementById('file-info').classList.remove('active');
const cFname = document.getElementById('ltc-track1-fname');
if (cFname) cFname.textContent = 'Choose audio file…';
document.getElementById('video-title').value = '';
document.getElementById('lt-track1-title-create').value = '';
document.getElementById('lt-track1-desc-create').value = '';
setAudioMode(false);
}
@@ -445,6 +576,8 @@
_thumbClickTs = now;
if (typeof window.openCropperModal_thumb_create_mobile === 'function') {
window.openCropperModal_thumb_create_mobile();
const internal = document.getElementById('tcInput_thumb_create_mobile');
if (internal) internal.click();
} else {
thumbnailInput.click();
}
@@ -487,54 +620,119 @@
document.getElementById('thumbnail-info').classList.remove('active');
}
// ── Slides dropzone (audio mode) ─────────────────────────
slidesDropzone.addEventListener('click', e => { if (e.target.closest('button')) return; slidesInput.click(); });
slidesInput.addEventListener('change', function() { handleSlidesSelect(this); });
slidesDropzone.addEventListener('dragover', e => { e.preventDefault(); slidesDropzone.classList.add('dragover'); });
slidesDropzone.addEventListener('dragleave', () => slidesDropzone.classList.remove('dragover'));
slidesDropzone.addEventListener('drop', e => {
e.preventDefault(); slidesDropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) { slidesInput.files = e.dataTransfer.files; handleSlidesSelect(slidesInput); }
});
// ── Per-track slides management (create page) ────────────
const _cSlidesData = {}; // trackId → File[]
let _cSlidesDragSrc = null;
function handleSlidesSelect(input) {
if (!input.files || !input.files.length) return;
const files = Array.from(input.files);
const strip = document.getElementById('slides-strip');
strip.innerHTML = '';
files.forEach(f => {
const reader = new FileReader();
reader.onload = e => {
const img = document.createElement('img');
img.src = e.target.result;
img.style.cssText = 'width:56px;height:56px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.15);';
strip.appendChild(img);
};
reader.readAsDataURL(f);
});
document.getElementById('slides-count').textContent =
files.length === 1 ? '1 image — static cover' : files.length + ' images — will crossfade';
document.getElementById('slides-default').style.display = 'none';
document.getElementById('slides-preview').style.display = '';
slidesDropzone.style.borderColor = '#22c55e';
function cSlidesZoneClick(e, trackId) {
if (e.target.closest('button')) return;
document.getElementById('slides-input-' + trackId)?.click();
}
function cSlidesZoneDragover(e, trackId) {
if (_cSlidesDragSrc) { e.preventDefault(); return; }
if (Array.from(e.dataTransfer.types || []).includes('Files')) {
e.preventDefault();
document.getElementById('slides-zone-' + trackId)?.classList.add('dragover');
}
}
function cSlidesZoneDragleave(e, trackId) {
document.getElementById('slides-zone-' + trackId)?.classList.remove('dragover');
}
function cSlidesZoneDrop(e, trackId) {
e.preventDefault();
document.getElementById('slides-zone-' + trackId)?.classList.remove('dragover');
if (_cSlidesDragSrc) return;
if (e.dataTransfer.files.length) handleCSlidesForTrack(trackId, e.dataTransfer.files);
}
function removeSlides(e) {
function handleCSlidesForTrack(trackId, fileList) {
if (!fileList || !fileList.length) return;
if (!_cSlidesData[trackId]) _cSlidesData[trackId] = [];
for (const f of Array.from(fileList)) {
if (_cSlidesData[trackId].length >= 10) break;
_cSlidesData[trackId].push(f);
}
renderCSlidesStrip(trackId);
}
function renderCSlidesStrip(trackId) {
const files = _cSlidesData[trackId] || [];
const strip = document.getElementById('slides-strip-' + trackId);
const ph = document.getElementById('slides-ph-' + trackId);
const prev = document.getElementById('slides-preview-' + trackId);
const cnt = document.getElementById('slides-count-' + trackId);
const zone = document.getElementById('slides-zone-' + trackId);
if (!strip) return;
strip.innerHTML = '';
if (!files.length) {
if (ph) ph.style.display = ''; if (prev) prev.style.display = 'none';
if (zone) zone.style.borderColor = ''; return;
}
files.forEach((f, index) => {
const wrap = document.createElement('div');
wrap.style.cssText = 'position:relative;flex-shrink:0;cursor:grab;';
wrap.draggable = true; wrap.dataset.index = index; wrap.dataset.trackId = trackId;
wrap.addEventListener('dragstart', function(ev) {
_cSlidesDragSrc = { trackId, index: parseInt(this.dataset.index) };
ev.dataTransfer.effectAllowed = 'move';
setTimeout(() => { this.style.opacity = '0.4'; }, 0);
});
wrap.addEventListener('dragend', function() { _cSlidesDragSrc = null; this.style.opacity = ''; });
wrap.addEventListener('dragover', function(ev) {
if (!_cSlidesDragSrc || _cSlidesDragSrc.trackId !== trackId) return;
ev.preventDefault(); this.style.outline = '2px solid var(--brand-red)';
});
wrap.addEventListener('dragleave', function() { this.style.outline = ''; });
wrap.addEventListener('drop', function(ev) {
ev.preventDefault(); ev.stopPropagation(); this.style.outline = '';
if (!_cSlidesDragSrc || _cSlidesDragSrc.trackId !== trackId) return;
const from = _cSlidesDragSrc.index, to = parseInt(this.dataset.index);
if (from === to) return;
const arr = _cSlidesData[trackId];
const [moved] = arr.splice(from, 1); arr.splice(to, 0, moved);
renderCSlidesStrip(trackId);
});
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.style.cssText = 'position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:var(--brand-red);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;';
removeBtn.innerHTML = '<i class="bi bi-x"></i>';
removeBtn.addEventListener('click', function(ev) {
ev.stopPropagation(); _cSlidesData[trackId].splice(index, 1); renderCSlidesStrip(trackId);
});
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 = '';
const reader = new FileReader();
reader.onload = ev2 => { img.src = ev2.target.result; };
reader.readAsDataURL(f);
wrap.appendChild(img); wrap.appendChild(removeBtn); strip.appendChild(wrap);
});
if (cnt) cnt.textContent = files.length === 1 ? '1 image — static cover' : files.length + ' images — will crossfade';
if (ph) ph.style.display = 'none'; if (prev) prev.style.display = '';
if (zone) zone.style.borderColor = '#22c55e';
}
function clearCSlidesForTrack(e, trackId) {
e.preventDefault(); e.stopPropagation();
const fresh = slidesInput.cloneNode(false);
slidesInput.parentNode.replaceChild(fresh, slidesInput);
slidesInput = fresh;
slidesInput.addEventListener('change', function() { handleSlidesSelect(this); });
document.getElementById('slides-default').style.display = 'block';
document.getElementById('slides-preview').style.display = 'none';
slidesDropzone.style.borderColor = 'var(--brand-red)';
_cSlidesData[trackId] = [];
const input = document.getElementById('slides-input-' + trackId);
if (input) {
const fresh = input.cloneNode(false);
fresh.addEventListener('change', function() { handleCSlidesForTrack(trackId, this.files); });
input.parentNode.replaceChild(fresh, input);
}
renderCSlidesStrip(trackId);
}
// ── Option selectors ─────────────────────────────────────
document.querySelectorAll('#type-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#type-options .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
item.addEventListener('click', function() {
document.querySelectorAll('#type-options .option-item').forEach(o => {
o.classList.toggle('active', o.dataset.type === this.dataset.type);
const inp = o.querySelector('input');
if (inp) inp.checked = (o.dataset.type === this.dataset.type);
});
setAudioMode(this.dataset.type === 'music');
});
});
document.querySelectorAll('#visibility-options .option-item').forEach(item => {
@@ -557,21 +755,35 @@
if (!currentFile) { showError('Please select a video or audio file.'); return; }
const title = document.getElementById('video-title').value.trim();
const isAudio = isAudioFile(currentFile);
const titleEl = isAudio ? document.getElementById('lt-track1-title-create') : document.getElementById('video-title');
const title = titleEl.value.trim();
if (!title) { showError('Please enter a title.'); return; }
if (isAudioFile(currentFile)) {
if (!slidesInput.files || !slidesInput.files.length) {
const _isAudioSubmitC = isAudioFile(currentFile);
if (_isAudioSubmitC) {
const t1Slides = _cSlidesData['ct1'] || [];
if (!t1Slides.length) {
showError('At least one cover image is required for audio tracks.');
slidesDropzone.style.borderColor = 'var(--brand-red)';
const z = document.getElementById('slides-zone-ct1');
if (z) z.style.borderColor = 'var(--brand-red)';
return;
}
}
const formData = new FormData(this);
if (_isAudioSubmitC) {
(_cSlidesData['ct1'] || []).forEach(f => formData.append('slides[]', f));
for (const [tid, files] of Object.entries(_cSlidesData)) {
if (tid === 'ct1') continue;
const n = parseInt(tid.replace('ce', ''));
files.forEach(f => formData.append('extra_track_slides_' + n + '[]', f));
}
}
const xhr = new XMLHttpRequest();
xhr.timeout = 1800000; // 30 min
xhr.timeout = 0; // no timeout — unlimited file size
_pageUploadInProgress = true;
document.getElementById('progress-container').classList.add('active');
document.getElementById('submit-btn').disabled = true;
document.getElementById('submit-btn').innerHTML = '<i class="bi bi-arrow-repeat"></i> Uploading...';
@@ -593,8 +805,12 @@
try {
const res = JSON.parse(xhr.responseText);
if (res.success) {
document.getElementById('progress-text').textContent = 'Done! Redirecting...';
window.location.href = res.redirect;
document.getElementById('progress-container').classList.remove('active');
const sm = document.getElementById('status-message');
sm.innerHTML = '<i class="bi bi-check-circle-fill"></i> Upload successful! Taking you to your video…';
sm.className = 'success';
document.getElementById('submit-btn').innerHTML = '<i class="bi bi-check-lg"></i> <span>Uploaded!</span>';
setTimeout(() => { window.location.href = res.redirect; }, 1800);
} else {
showError(res.message || 'Upload failed.');
}
@@ -613,7 +829,7 @@
});
xhr.addEventListener('error', () => showError('Upload failed. Check your connection.'));
xhr.addEventListener('timeout', () => showError('Upload timed out. The file may be too large.'));
xhr.addEventListener('timeout', () => showError('Upload timed out.'));
xhr.open('POST', '{{ route("videos.store") }}');
xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}');
@@ -621,13 +837,196 @@
});
function showError(message) {
_pageUploadInProgress = false;
const status = document.getElementById('status-message');
status.innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + message;
status.className = 'error';
document.getElementById('submit-btn').disabled = false;
document.getElementById('submit-btn').innerHTML = '<i class="bi bi-cloud-arrow-up-fill"></i> <span>Upload Video</span>';
const isAudio = document.querySelector('#type-options .option-item[data-type="music"]')?.classList.contains('active');
document.getElementById('submit-btn').innerHTML =
'<i class="bi bi-cloud-arrow-up-fill"></i> <span>' + (isAudio ? 'Upload Track' : 'Upload Video') + '</span>';
document.getElementById('progress-container').classList.remove('active');
}
// ── Language track accordion (create page) ───────────────
const LANG_OPTIONS_CREATE = @json(\App\Data\Languages::forLanguage());
let _ltCreateExtraCounter = 0;
function _ltcBuildOptsHtml() {
return LANG_OPTIONS_CREATE.map(o =>
`<li class="csd-opt" role="option" tabindex="-1" data-v="${o.value}" data-s="${o.search}" aria-selected="false">
<span class="csd-opt-ico"><span class="fi fi-${o.flag} lsd-flag"></span></span>
<span class="csd-opt-main">${o.label}</span>
<span class="csd-opt-sub">${o.native}</span>
</li>`
).join('');
}
function ltToggleCreate(trackId) {
const item = document.getElementById('ltac-' + trackId);
const arr = document.getElementById('ltac-' + trackId + '-arr');
if (!item) return;
const isOpen = item.classList.contains('open');
if (isOpen) {
item.classList.remove('open');
if (arr) { arr.style.transform = ''; arr.style.color = ''; }
} else {
item.classList.add('open');
if (arr) { arr.style.transform = 'rotate(180deg)'; arr.style.color = 'var(--brand-red)'; }
}
}
function updateLtacHeader(trackId, langCode) {
const lang = LANG_OPTIONS_CREATE.find(o => o.value === langCode);
if (!lang) return;
const flagEl = document.getElementById('ltac-' + trackId + '-flag');
const nameEl = document.getElementById('ltac-' + trackId + '-name');
if (flagEl) flagEl.innerHTML = `<span class="fi fi-${lang.flag} lsd-flag" style="width:22px;height:16px;"></span>`;
if (nameEl) {
const prefix = trackId === 't1' ? 'Primary' : 'Track ' + (parseInt(trackId.replace('e', '')) + 1);
nameEl.textContent = prefix + ' — ' + lang.label;
}
}
function ltRemoveCreate(e, trackId) {
e.preventDefault();
e.stopPropagation();
const item = document.getElementById('ltac-' + trackId);
if (item) item.remove();
delete _cSlidesData['c' + trackId];
}
function addLangTrackCreate() {
const n = ++_ltCreateExtraCounter;
const trackNum = n + 1;
const uid = 'ltce_' + n;
const item = document.createElement('div');
item.className = 'ltac-item open';
item.id = 'ltac-e' + n;
item.innerHTML = `
<div class="ltac-header" onclick="ltToggleCreate('e${n}')">
<div class="ltac-hd-left">
<span class="ltac-num">${trackNum}</span>
<span class="ltac-flag-cell" id="ltac-e${n}-flag">
<span class="fi fi-xx lsd-flag" style="width:22px;height:16px;"></span>
</span>
<span class="ltac-name" id="ltac-e${n}-name">Track ${trackNum} — Select language</span>
</div>
<div style="display:flex;align-items:center;gap:6px;">
<button type="button" class="ltac-remove" onclick="ltRemoveCreate(event,'e${n}')" title="Remove track">
<i class="bi bi-trash"></i>
</button>
<i class="bi bi-chevron-up ltac-arr" style="color:var(--brand-red);" id="ltac-e${n}-arr"></i>
</div>
</div>
<div class="ltac-body" id="ltac-e${n}-body">
<div style="margin-bottom:12px;">
<label class="form-label" style="font-size:13px;margin-bottom:6px;"><i class="bi bi-music-note-beamed"></i> Audio File</label>
<label class="ltac-file-label"
onmouseenter="this.style.borderColor='var(--brand-red)'"
onmouseleave="this.style.borderColor='#333'">
<i class="bi bi-music-note-beamed" style="color:var(--brand-red);font-size:16px;flex-shrink:0;"></i>
<span class="ltac-file-name" id="ltac-e${n}-fname">Choose audio file…</span>
<input type="file" name="extra_track_files[]"
accept="audio/*,.mp3,.m4a,.aac,.flac,.wav" style="display:none;"
onchange="document.getElementById('ltac-e${n}-fname').textContent=this.files[0]?.name||'Choose audio file…'">
</label>
</div>
<div style="margin-bottom:12px;">
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
Localized Title <span style="font-weight:400;font-size:12px;color:var(--text-secondary);">optional</span>
</label>
<input type="text" name="extra_track_titles[]" class="form-input"
style="font-size:13px;" placeholder="Title in this language…">
</div>
<div style="margin-bottom:12px;">
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
Localized Description <span style="font-weight:400;font-size:12px;color:var(--text-secondary);">optional</span>
</label>
<textarea name="extra_track_descriptions[]" class="form-textarea"
rows="2" style="font-size:13px;min-height:64px;"
placeholder="Description in this language…"></textarea>
</div>
<div style="margin-bottom:12px;">
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
Cover Slides <span style="font-weight:400;font-size:12px;color:var(--text-secondary);">optional · drag to reorder</span>
</label>
<div id="slides-zone-ce${n}" class="ltac-slides-zone"
onclick="cSlidesZoneClick(event,'ce${n}')"
ondragover="cSlidesZoneDragover(event,'ce${n}')"
ondragleave="cSlidesZoneDragleave(event,'ce${n}')"
ondrop="cSlidesZoneDrop(event,'ce${n}')">
<input type="file" accept="image/*" multiple style="display:none" id="slides-input-ce${n}"
onchange="handleCSlidesForTrack('ce${n}',this.files)">
<div class="ltac-slides-ph" id="slides-ph-ce${n}">
<i class="bi bi-images" style="font-size:22px;"></i>
<span>Click or drag to add cover images</span>
</div>
<div id="slides-preview-ce${n}" style="display:none;padding:10px 12px;width:100%;box-sizing:border-box;">
<div id="slides-strip-ce${n}" class="slides-strip" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;"></div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span id="slides-count-ce${n}" style="font-size:12px;color:var(--text-secondary);"></span>
<button type="button" onclick="clearCSlidesForTrack(event,'ce${n}')"
style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:0;display:flex;align-items:center;gap:4px;">
<i class="bi bi-x-lg"></i> Clear
</button>
</div>
</div>
</div>
</div>
<div>
<label class="form-label" style="font-size:13px;margin-bottom:6px;">Language</label>
<div class="csd-wrap" id="${uid}">
<button type="button" class="csd-btn" aria-haspopup="listbox" aria-expanded="false" aria-label="Select language">
<span class="csd-ico"><span class="fi fi-xx lsd-flag"></span></span>
<span class="csd-val ph">Select language</span>
<i class="bi bi-chevron-down csd-arr"></i>
</button>
<div class="csd-panel" hidden role="listbox">
<div class="csd-srch"><i class="bi bi-search"></i><input class="csd-sinput" type="text" placeholder="Search language…" autocomplete="off" spellcheck="false"></div>
<ul class="csd-list">${_ltcBuildOptsHtml()}</ul>
<p class="csd-empty" hidden>No results</p>
</div>
<input type="hidden" name="extra_track_languages[]" id="csd_v_${uid}">
</div>
</div>
</div>`;
document.getElementById('ltac-extra').appendChild(item);
_cSlidesData['ce' + n] = [];
if (window.CSD) new CSD(uid);
const hiddenInput = document.getElementById('csd_v_' + uid);
if (hiddenInput) {
hiddenInput.dataset.trackId = 'e' + n;
hiddenInput.addEventListener('change', function() {
updateLtacHeader('e' + n, this.value);
});
}
}
// Wire primary language select → update track 1 header
;(function() {
const plInput = document.getElementById('primary_language_create');
if (plInput) {
plInput.dataset.trackId = 't1';
plInput.addEventListener('change', function() {
updateLtacHeader('t1', this.value);
});
}
// Delegated fallback: catches any language hidden-input change within the tracks section
const section = document.getElementById('lang-tracks-section-create');
if (section) {
section.addEventListener('change', function(e) {
const tid = e.target.dataset && e.target.dataset.trackId;
if (tid && e.target.value) updateLtacHeader(tid, e.target.value);
});
}
}());
</script>
<x-image-cropper
id="thumb_create_mobile"