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

1464 lines
77 KiB
PHP
Raw 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.

@php use App\Data\Languages; @endphp
<div class="modal fade" id="uploadModal" 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="upload-form-modal" enctype="multipart/form-data" class="modal-content um-content">
@csrf
{{-- Core hidden inputs --}}
<input type="hidden" name="is_shorts" id="is_shorts_modal" value="0">
<input type="hidden" name="type" id="type_modal" value="generic">
<input type="hidden" name="visibility" id="gs-vis-val" value="public">
<input type="hidden" name="download_access" id="gs-dl-val" value="disabled">
{{-- Primary video/audio file (shared by all modes) --}}
<input type="file" name="video" id="video-modal"
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="upload-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 this upload?</p>
<p style="color:#777;font-size:13px;margin:0;line-height:1.5;">Your selected file and details will be lost.</p>
</div>
<div style="display:flex;gap:10px;">
<button type="button" class="action-btn" onclick="_hideCloseConfirm()"><span>Keep editing</span></button>
<button type="button" class="action-btn action-btn-danger" onclick="_doCloseModal()"><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-cloud-arrow-up-fill"></i></div>
<div>
<div class="um-header-title">Upload</div>
<div class="um-header-sub" id="uploadModalLabel">Share your content with the world</div>
</div>
</div>
<button type="button" class="btn-close btn-close-white" onclick="closeUploadModal()" 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="gs-type-btn">
<i class="bi bi-film um-gs-ico" id="gs-type-ico"></i>
<span class="um-gs-txt" id="gs-type-txt">Generic</span>
<i class="bi bi-chevron-down um-gs-arr"></i>
</button>
<ul class="um-gs-menu" id="gs-type-menu" hidden>
<li class="um-gs-opt active" data-gs="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-gs="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-gs="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="gs-vis-btn">
<i class="bi bi-globe um-gs-ico" id="gs-vis-ico"></i>
<span class="um-gs-txt" id="gs-vis-txt">Public</span>
<i class="bi bi-chevron-down um-gs-arr"></i>
</button>
<ul class="um-gs-menu" id="gs-vis-menu" hidden>
<li class="um-gs-opt active" data-gs="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-gs="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-gs="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="gs-dl-btn">
<i class="bi bi-slash-circle um-gs-ico" id="gs-dl-ico"></i>
<span class="um-gs-txt" id="gs-dl-txt">Off</span>
<i class="bi bi-chevron-down um-gs-arr"></i>
</button>
<ul class="um-gs-menu" id="gs-dl-menu" hidden>
<li class="um-gs-opt active" data-gs="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-gs="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-gs="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-gs="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="um-tracks-section-label">Language Tracks</span>
<span class="um-tracks-sub" id="um-tracks-section-sub">Add audio tracks in different languages</span>
</div>
<button type="button" class="action-btn" id="um-add-track-btn" onclick="addExtraTrackModal()" style="display:none;font-size:12px;">
<i class="bi bi-plus-circle"></i> <span>Add Language Track</span>
</button>
</div>
{{-- Track 1 (always present) --}}
<div id="um-tc-list">
<div id="um-tc-t1">
{{-- Empty state: just the Add button --}}
<button type="button" class="action-btn action-btn-primary" id="um-tc-t1-add-btn" onclick="openTrackPopup('t1')" style="width:100%;justify-content:center;padding:16px;font-size:14px;border-radius:12px;">
<i class="bi bi-plus-circle"></i> <span>Add Language Track</span>
</button>
{{-- Filled state: track card (hidden until lang or title is set) --}}
<div class="um-track-card" id="um-tc-t1-card" style="display:none;">
<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="um-tc-flag-t1"></span>
<div class="um-tc-info">
<span class="um-tc-title" id="um-tc-title-t1"></span>
<span class="um-tc-primary"><i class="bi bi-star-fill"></i> Primary</span>
</div>
</div>
<div class="um-tc-right">
<button type="button" class="action-btn" onclick="openTrackPopup('t1')">
<i class="bi bi-pencil"></i> <span>Edit</span>
</button>
</div>
</div>
</div>
</div>
<div id="um-tc-extra"></div>
</div>
{{-- Progress --}}
<div id="progress-container-modal" class="um-prog">
<div class="um-prog-track">
<div id="progress-bar-modal" class="um-prog-fill"></div>
</div>
<span id="progress-text-modal" class="um-prog-text">Uploading… 0%</span>
</div>
{{-- Status --}}
<div id="status-message-modal" class="status-message-modal"></div>
{{-- Submit --}}
<button type="submit" id="submit-btn-modal" class="action-btn action-btn-primary um-submit">
<i class="bi bi-cloud-arrow-up-fill"></i> <span>Upload Video</span>
</button>
</div>{{-- /um-body --}}
{{-- ── Track Editor Mini-Popup (position:absolute, overlays entire modal) ── --}}
<div id="um-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="closeTrackPopup()" aria-label="Close"></button>
</div>
<div class="um-tp-body">
{{-- Track 1 form (Blade-rendered) --}}
<div class="um-track-form" id="um-tf-t1" style="display:none;">
<div class="um-tf-primary-note">
<i class="bi bi-star-fill"></i>
<span>Primary Track this language and title are the default display name</span>
</div>
<div class="um-tf-row2">
{{-- Language (always shown; required for music, optional for video) --}}
<div id="um-tf-t1-lang-wrap">
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">Language</label>
<x-language-select
name="primary_language"
id="primary_language_modal"
placeholder="Select language"
/>
</div>
<div>
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">Title <span class="um-req">*</span></label>
<input type="text" id="lt-track1-title-modal" class="um-input"
style="font-size:13px;padding:9px 12px;"
placeholder="Track title…" autocomplete="off">
</div>
</div>
<div class="um-field">
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">Description</label>
<x-rich-text-editor name="" id="lt-track1-desc-modal" placeholder="Tell viewers about this content…" />
</div>
{{-- Video file + Thumbnail side by side (video/match mode) --}}
<div style="display:flex;gap:12px;align-items:stretch;">
<div class="um-field um-field-col" id="um-tf-t1-video-zone" style="display:none;flex:1;min-width:0;margin-bottom:0;">
<label class="um-lbl"><i class="bi bi-film"></i> Video File <span class="um-req">*</span></label>
<div id="um-t1-dropzone" class="um-dropzone um-zone-fill"
onclick="if(!event.target.closest('.um-x-btn')) videoInputModal.click()"
ondragover="event.preventDefault();this.classList.add('dragover')"
ondragleave="this.classList.remove('dragover')"
ondrop="event.preventDefault();this.classList.remove('dragover');if(event.dataTransfer.files.length){videoInputModal.files=event.dataTransfer.files;handleVideoSelectModal(videoInputModal);}">
<div id="um-t1-dz-idle" class="um-dz-idle">
<div class="um-dz-ring"><i class="bi bi-cloud-arrow-up"></i></div>
<p class="um-dz-title">Drop your file here</p>
<p class="um-dz-sub">or click to browse</p>
<div class="um-dz-formats">
<span>MP4</span><span>MOV</span><span>MKV</span><span>AVI</span><span>WebM</span>
</div>
</div>
<div id="um-t1-file-info" class="um-file-card" style="display:none;">
<div class="um-file-icon-wrap"><i class="bi bi-film"></i></div>
<div class="um-file-meta">
<span class="um-file-name" id="filename-modal"></span>
<span class="um-file-size" id="filesize-modal"></span>
</div>
<button type="button" class="um-x-btn" onclick="removeVideoModal(event)" title="Remove">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
<div class="um-field um-field-col" id="um-tf-t1-thumb-wrap" style="display:none;flex:1;min-width:0;margin-bottom:0;">
<label class="um-lbl">
<i class="bi bi-card-image"></i>
<span id="thumbnail-label-text">Thumbnail</span>
<span class="um-lbl-hint">16:9</span>
</label>
<div id="thumbnail-dropzone-modal" class="um-thumb-zone um-zone-fill">
<input type="file" name="thumbnail" id="thumbnail-modal" accept="image/*" style="display:none;">
<div id="thumbnail-default-modal" class="um-thumb-ph">
<div class="um-dz-ring"><i class="bi bi-card-image"></i></div>
<p class="um-thumb-ph-title">Click to upload</p>
<p class="um-thumb-ph-sub">16:9 recommended</p>
</div>
<div id="thumbnail-info-modal" class="um-thumb-info" style="display:none;">
<div class="um-thumb-prev">
<img id="thumbnail-preview-img" src="" alt="">
</div>
<div class="um-thumb-meta">
<span class="um-thumb-name" id="thumbnail-filename-modal"></span>
<span class="um-thumb-size" id="thumbnail-filesize-modal"></span>
</div>
<button type="button" class="btn-remove-file um-x-btn" onclick="removeThumbnailModal(event)">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
</div>
{{-- Audio file + Cover Slides side by side (music mode) --}}
<div id="um-tf-t1-music-pair" style="display:none;gap:12px;align-items:stretch;margin-bottom:14px;">
<div class="um-field-col" id="um-tf-t1-audio-zone" style="flex:1;min-width:0;margin-bottom:0;">
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;"><i class="bi bi-music-note-beamed"></i> Audio File <span class="um-req">*</span></label>
<div id="um-tf-t1-audio-box" class="um-slides-zone"
onclick="videoInputModal.click()"
style="cursor:pointer;flex:1;">
<div class="um-slides-ph" id="lt-track1-audio-ph">
<i class="bi bi-music-note-beamed"></i>
<span id="lt-track1-fname">Click to choose file…</span>
</div>
</div>
</div>
<div class="um-field-col" id="um-tf-t1-slides-wrap" style="flex:1;min-width:0;margin-bottom:0;">
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
Cover Slides <span class="um-req">*</span>
<span class="um-lbl-hint">110 · drag to reorder</span>
</label>
<div id="slides-zone-t1" class="um-slides-zone"
onclick="slidesZoneClick(event,'t1')"
ondragover="slidesZoneDragover(event,'t1')"
ondragleave="slidesZoneDragleave(event,'t1')"
ondrop="slidesZoneDrop(event,'t1')">
<input type="file" accept="image/*" multiple style="display:none" id="slides-input-t1"
onchange="handleSlidesForTrack('t1',this.files)">
<div class="um-slides-ph" id="slides-ph-t1">
<i class="bi bi-images"></i>
<span>Click or drag to add images</span>
</div>
<div id="slides-preview-t1" style="display:none;padding:10px 12px;width:100%;box-sizing:border-box;">
<div id="slides-strip-t1" 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-t1" style="font-size:12px;color:var(--text-secondary);"></span>
<button type="button" onclick="clearSlidesForTrack(event,'t1')"
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>
</div>{{-- /um-tf-t1 --}}
{{-- Extra track forms appended by JS --}}
<div id="um-tf-extra"></div>
</div>{{-- /um-tp-body --}}
<div class="um-tp-footer">
<button type="button" class="action-btn action-btn-primary" onclick="closeTrackPopup()">
<i class="bi bi-check-lg"></i> <span>Done</span>
</button>
</div>
</div>{{-- /um-track-popup-box --}}
</div>{{-- /um-track-popup --}}
</form>{{-- /upload-form-modal --}}
</div>
</div>
<style>
/* ── Dialog shell ─────────────────────────────────────────── */
.um-dialog { max-width: 680px; }
.um-content {
background: #181818;
border: 1px solid #262626;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 32px 80px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.04);
display: flex;
flex-direction: column;
max-height: 90vh;
position: relative;
}
#uploadModal .modal-dialog {
opacity: 0;
transition: opacity .25s ease;
}
#uploadModal.show .modal-dialog { opacity: 1; }
/* ── Header ──────────────────────────────────────────────── */
.um-header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 24px 18px 20px;
background: #111;
border-bottom: 1px solid #222;
flex-shrink: 0;
position: relative;
}
.um-header::before {
content: '';
position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
background: linear-gradient(180deg, #c01010 0%, #e61e1e 50%, #ff4757 100%);
}
.um-header-left { display: flex; align-items: center; gap: 14px; padding-left: 14px; }
.um-header-icon {
width: 44px; height: 44px; flex-shrink: 0;
background: rgba(230,30,30,.13); border: 1px solid rgba(230,30,30,.28);
border-radius: 12px; display: flex; align-items: center; justify-content: center;
font-size: 22px; color: #e61e1e;
}
.um-header-title { font-size: 18px; font-weight: 700; color: #f1f1f1; line-height: 1.2; }
.um-header-sub { font-size: 12px; color: #555; margin-top: 2px; }
/* ── Body ────────────────────────────────────────────────── */
.um-body {
flex: 1; overflow-y: auto; padding: 24px 26px;
background: #181818;
scrollbar-width: thin; scrollbar-color: #2a2a2a transparent;
display: flex; flex-direction: column; gap: 0;
}
.um-body::-webkit-scrollbar { width: 4px; }
.um-body::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 2px; }
/* ── Global Settings ─────────────────────────────────────── */
.um-gs-row { display: flex; gap: 10px; margin-bottom: 0; }
.um-gs-wrap { flex: 1; position: relative; }
.um-gs-lbl {
display: block; font-size: 9px; font-weight: 700;
text-transform: uppercase; letter-spacing: .08em;
color: #444; margin-bottom: 5px;
}
.um-gs-btn {
display: flex; align-items: center; gap: 7px; width: 100%;
background: #111; border: 1px solid #252525; border-radius: 9px;
padding: 9px 12px; color: #e1e1e1; font-size: 12px;
font-family: inherit; cursor: pointer; text-align: left;
transition: border-color .15s;
}
.um-gs-btn:hover, .um-gs-btn.open { border-color: #e61e1e; }
.um-gs-ico { font-size: 13px; color: #aaa; flex-shrink: 0; }
.um-gs-txt { flex: 1; }
.um-gs-arr { font-size: 9px; color: #555; margin-left: auto; transition: transform .2s; flex-shrink: 0; }
.um-gs-btn.open .um-gs-arr { transform: rotate(180deg); }
.um-gs-menu {
position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 50;
background: #1a1a1a; border: 1px solid #303030; border-radius: 9px;
padding: 4px 0; list-style: none; margin: 0;
box-shadow: 0 8px 24px rgba(0,0,0,.6);
}
.um-gs-opt {
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
cursor: pointer; font-size: 12px; color: #aaa; transition: background .1s, color .1s;
}
.um-gs-opt:hover { background: rgba(255,255,255,.05); color: #f1f1f1; }
.um-gs-opt.active { color: #e61e1e; }
.um-gs-opt i { font-size: 13px; width: 14px; flex-shrink: 0; }
/* ── Separator ───────────────────────────────────────────── */
.um-rule { border: none; border-top: 1px solid #1e1e1e; margin: 20px 0; }
/* ── Tracks header ───────────────────────────────────────── */
.um-tracks-header {
display: flex; align-items: flex-start; justify-content: space-between;
margin-bottom: 12px; gap: 12px;
}
.um-tracks-title { display: block; font-size: 11px; font-weight: 700; color: #666; text-transform: uppercase; letter-spacing: .07em; }
.um-tracks-sub { display: block; font-size: 11px; color: #444; margin-top: 3px; }
/* ── Track cards ─────────────────────────────────────────── */
#um-tc-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; }
.um-track-card {
background: #111; border: 1px solid #252525; border-radius: 12px;
transition: border-color .15s; overflow: hidden;
}
.um-track-card:hover { border-color: #333; }
.um-tc-body {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 12px 14px;
}
.um-tc-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.um-tc-num {
width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0;
background: rgba(230,30,30,.12); border: 1px solid rgba(230,30,30,.25);
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; color: #e61e1e;
}
.um-tc-flag { width: 22px; height: 16px; border-radius: 2px; flex-shrink: 0; }
.um-tc-info { flex: 1; min-width: 0; }
.um-tc-title { display: block; font-size: 13px; font-weight: 600; color: #e5e5e5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.um-tc-primary { display: inline-flex; align-items: center; gap: 3px; font-size: 10px; color: #e61e1e; margin-top: 2px; }
.um-tc-primary i { font-size: 9px; }
.um-tc-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* ── Form elements ───────────────────────────────────────── */
.um-field { margin-bottom: 14px; }
.um-field-col { display: flex; flex-direction: column; }
.um-zone-fill { flex: 1; min-height: 0; }
.um-lbl {
display: flex; align-items: center; gap: 5px;
font-size: 11px; font-weight: 700; color: #666;
text-transform: uppercase; letter-spacing: .07em;
margin-bottom: 8px;
}
.um-lbl i { color: #e61e1e; font-size: 12px; }
.um-req { color: #e61e1e; font-size: 13px; }
.um-lbl-hint { font-weight: 400; font-size: 11px; color: #444; text-transform: none; letter-spacing: 0; margin-left: 4px; }
.um-input {
width: 100%; background: #111; border: 1px solid #252525;
border-radius: 10px; padding: 11px 14px;
color: #f1f1f1; font-size: 14px; font-family: inherit;
transition: border-color .2s, box-shadow .2s; box-sizing: border-box; display: block;
}
.um-input:focus { outline: none; border-color: #e61e1e; box-shadow: 0 0 0 3px rgba(230,30,30,.1); }
.um-input::placeholder { color: #3a3a3a; }
.um-textarea { resize: none; line-height: 1.6; }
.um-tf-row2 {
display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px;
}
@media (max-width: 540px) { .um-tf-row2 { grid-template-columns: 1fr; } }
/* ── Video drop zone ─────────────────────────────────────── */
.um-dropzone {
border: 2px dashed #282828; border-radius: 14px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
cursor: pointer; transition: border-color .22s, background .22s;
position: relative; overflow: hidden; min-height: 120px;
background: #0d0d0d;
}
.um-dropzone:hover { border-color: rgba(230,30,30,.7); background: rgba(230,30,30,.04); }
.um-dropzone.dragover { border-color: #e61e1e; background: rgba(230,30,30,.08); }
.um-dz-idle { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 20px 16px; width: 100%; }
.um-dz-ring {
width: 52px; height: 52px; border: 2px dashed #2e2e2e; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 22px; color: #3a3a3a; margin-bottom: 10px;
transition: border-color .22s, color .22s, transform .22s;
}
.um-dropzone:hover .um-dz-ring { border-color: rgba(230,30,30,.5); color: #e61e1e; transform: translateY(-3px); }
.um-dz-title { font-size: 13px; font-weight: 600; color: #777; margin: 0 0 3px; }
.um-dz-sub { font-size: 12px; color: #444; margin: 0 0 10px; }
.um-dz-formats { display: flex; gap: 4px; flex-wrap: wrap; justify-content: center; }
.um-dz-formats span { font-size: 10px; font-weight: 600; color: #444; background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 4px; padding: 2px 6px; }
.um-file-card { display: flex; align-items: center; gap: 10px; padding: 14px 16px; width: 100%; }
.um-file-icon-wrap { width: 40px; height: 40px; flex-shrink: 0; background: rgba(230,30,30,.12); border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #e61e1e; }
.um-file-meta { flex: 1; min-width: 0; }
.um-file-name { display: block; font-size: 12px; font-weight: 600; color: #e5e5e5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.um-file-size { display: block; font-size: 11px; color: #555; margin-top: 3px; }
.um-x-btn {
width: 26px; height: 26px; flex-shrink: 0;
background: rgba(255,255,255,.06); border: none; border-radius: 50%;
color: #666; cursor: pointer; display: flex; align-items: center; justify-content: center;
font-size: 10px; transition: background .15s, color .15s;
}
.um-x-btn:hover { background: #e61e1e; color: #fff; }
/* ── Thumbnail zone — mirrors .um-dropzone exactly ───────── */
.um-thumb-zone {
border: 2px dashed #282828; border-radius: 14px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
cursor: pointer; transition: border-color .22s, background .22s;
position: relative; overflow: hidden; min-height: 120px;
background: #0d0d0d;
}
.um-thumb-zone:hover { border-color: rgba(230,30,30,.7); background: rgba(230,30,30,.04); }
.um-thumb-ph { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 20px 16px; width: 100%; }
.um-thumb-ph .um-dz-ring { margin-bottom: 10px; }
.um-thumb-zone:hover .um-thumb-ph .um-dz-ring { border-color: rgba(230,30,30,.5); color: #e61e1e; transform: translateY(-3px); }
.um-thumb-ph-title { font-size: 13px; font-weight: 600; color: #777; margin: 0 0 3px; }
.um-thumb-ph-sub { font-size: 12px; color: #444; margin: 0; }
.um-thumb-info { display: flex; align-items: center; gap: 10px; padding: 14px 16px; width: 100%; box-sizing: border-box; }
.um-thumb-prev { width: 72px; height: 40px; flex-shrink: 0; border-radius: 6px; overflow: hidden; background: #1a1a1a; }
.um-thumb-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
.um-thumb-meta { flex: 1; min-width: 0; }
.um-thumb-name { display: block; font-size: 12px; font-weight: 600; color: #e5e5e5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.um-thumb-size { display: block; font-size: 11px; color: #555; margin-top: 2px; }
/* ── Audio file label ────────────────────────────────────── */
.lta-file-label { display: flex; align-items: center; gap: 8px; background: #0d0d0d; border: 1px solid #222; border-radius: 8px; padding: 9px 11px; cursor: pointer; transition: border-color .15s; }
.lta-file-name { font-size: 12px; color: #555; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
/* ── Slides zone ─────────────────────────────────────────── */
.um-slides-zone {
border: 1.5px dashed #252525; border-radius: 10px; min-height: 70px;
background: #111; cursor: pointer; transition: border-color .2s;
display: flex; align-items: center; justify-content: center;
}
.um-field-col .um-slides-zone { flex: 1; }
.um-slides-zone:hover { border-color: rgba(230,30,30,.5); }
.um-slides-zone.dragover { border-color: #e61e1e; background: rgba(230,30,30,.05); }
.um-slides-ph { display: flex; align-items: center; gap: 8px; color: #444; font-size: 13px; padding: 16px; }
.um-slides-ph i { font-size: 20px; color: #333; }
/* ── Track editor mini-popup ─────────────────────────────── */
.um-track-popup {
position: fixed; inset: 0; z-index: 1070;
background: rgba(10,10,10,.82); backdrop-filter: blur(6px);
display: flex; align-items: center; justify-content: center;
padding: 16px;
transition: opacity .18s;
}
.um-track-popup-box {
background: #1c1c1c; border: 1px solid #2a2a2a; border-radius: 14px;
width: 100%; max-width: 620px; max-height: 90vh;
display: flex; flex-direction: column; overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,.75);
}
.um-tp-header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 24px 18px 20px; border-bottom: 1px solid #222;
background: #111; flex-shrink: 0; position: relative;
}
.um-tp-header::before {
content: '';
position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
background: linear-gradient(180deg, #c01010 0%, #e61e1e 50%, #ff4757 100%);
}
.um-tp-header-left { display: flex; align-items: center; gap: 14px; padding-left: 14px; }
.um-tp-header-icon {
width: 44px; height: 44px; flex-shrink: 0;
background: rgba(230,30,30,.13); border: 1px solid rgba(230,30,30,.28);
border-radius: 12px; display: flex; align-items: center; justify-content: center;
font-size: 20px; color: #e61e1e;
}
.um-tp-header-title { font-size: 18px; font-weight: 700; color: #f1f1f1; line-height: 1.2; }
.um-tp-body {
flex: 1; overflow-y: auto; padding: 20px;
scrollbar-width: thin; scrollbar-color: #2a2a2a transparent;
}
.um-tp-body::-webkit-scrollbar { width: 4px; }
.um-tp-body::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 2px; }
.um-tp-footer {
padding: 14px 20px; border-top: 1px solid #222;
display: flex; justify-content: flex-end; gap: 8px; flex-shrink: 0;
}
.um-tf-primary-note {
display: flex; align-items: center; gap: 8px;
background: rgba(230,30,30,.07); border: 1px solid rgba(230,30,30,.18);
border-radius: 8px; padding: 9px 12px; margin-bottom: 16px;
font-size: 12px; color: #aaa;
}
.um-tf-primary-note i { color: #e61e1e; font-size: 12px; flex-shrink: 0; }
/* ── Progress ────────────────────────────────────────────── */
#progress-container-modal { display: none; margin: 16px 0 0; }
#progress-container-modal.active { display: block; }
.um-prog-track { background: #1e1e1e; border-radius: 4px; height: 4px; overflow: hidden; margin-bottom: 7px; }
.um-prog-fill { background: linear-gradient(90deg, #c01010, #e61e1e, #ff4757); height: 100%; width: 0; transition: width .35s ease; }
.um-prog-text { font-size: 12px; color: #555; text-align: center; display: block; }
/* ── Status ──────────────────────────────────────────────── */
.status-message-modal { display: none; padding: 11px 14px; border-radius: 9px; margin: 12px 0 0; font-size: 13px; }
.status-message-modal.success { display: block; background: rgba(34,197,94,.1); color: #4ade80; border: 1px solid rgba(34,197,94,.2); }
.status-message-modal.error { display: block; background: rgba(239,68,68,.1); color: #f87171; border: 1px solid rgba(239,68,68,.2); }
/* ── Submit ──────────────────────────────────────────────── */
.um-submit { width: 100%; justify-content: center; font-size: 14px; font-weight: 700; letter-spacing: .02em; padding: 13px 20px; margin-top: 16px; }
.um-submit:disabled { opacity: .45; cursor: not-allowed; }
</style>
<script>
// ── State ─────────────────────────────────────────────────────
let _uploadInProgress = false;
let _fileSelected = false;
let _umExtraCount = 0;
let _currentMode = 'generic'; // 'generic', 'music', 'match'
// ── Modal open / close ────────────────────────────────────────
function openUploadModal() {
if (window.innerWidth < 992) {
window.location.href = '{{ route("videos.create") }}';
return;
}
const modalEl = document.getElementById('uploadModal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
setTimeout(() => modalEl.classList.add('show'), 10);
}
function closeUploadModal(force) {
if (!force && _uploadInProgress) {
showToast('Upload in progress — please wait for it to complete.', 'error');
return;
}
if (!force && _fileSelected) { _showCloseConfirm(); return; }
_doCloseModal();
}
function _doCloseModal() {
const modalEl = document.getElementById('uploadModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
modalEl.addEventListener('hidden.bs.modal', function() {
resetUploadForm();
modalEl.classList.remove('show');
_hideCloseConfirm();
}, { once: true });
}
function _showCloseConfirm() { document.getElementById('upload-close-confirm').style.display = 'flex'; }
function _hideCloseConfirm() { document.getElementById('upload-close-confirm').style.display = 'none'; }
// ── Form reset ────────────────────────────────────────────────
function resetUploadForm() {
_uploadInProgress = false;
_fileSelected = false;
_umExtraCount = 0;
document.getElementById('upload-form-modal').reset();
// Restore GS dropdowns
_gsSetDefault('type', 'generic', 'bi-film', 'Generic');
_gsSetDefault('vis', 'public', 'bi-globe', 'Public');
_gsSetDefault('dl', 'disabled','bi-slash-circle','Off');
document.getElementById('type_modal').value = 'generic';
document.getElementById('gs-vis-val').value = 'public';
document.getElementById('gs-dl-val').value = 'disabled';
// Reset progress/status/submit
document.getElementById('progress-container-modal').classList.remove('active');
document.getElementById('progress-bar-modal').style.width = '0%';
document.getElementById('status-message-modal').className = 'status-message-modal';
document.getElementById('submit-btn-modal').disabled = false;
document.getElementById('submit-btn-modal').innerHTML = '<i class="bi bi-cloud-arrow-up-fill"></i> <span>Upload Video</span>';
// Close mini-popup and apply generic mode
document.getElementById('um-track-popup').style.display = 'none';
_applyMode('generic');
// Clear extra tracks
document.getElementById('um-tc-extra').innerHTML = '';
document.getElementById('um-tf-extra').innerHTML = '';
// Clear slides data
for (const key of Object.keys(_slidesData)) { _slidesData[key] = []; }
renderSlidesStrip('t1');
// Reset video dropzone
document.getElementById('um-t1-dz-idle').style.display = '';
const fic = document.getElementById('um-t1-file-info');
if (fic) fic.style.display = 'none';
document.getElementById('filename-modal').textContent = '';
document.getElementById('filesize-modal').textContent = '';
// Reset thumbnail
document.getElementById('thumbnail-default-modal').style.display = '';
const thi = document.getElementById('thumbnail-info-modal');
if (thi) thi.style.display = 'none';
// Reset primary language CSD
const plInput = document.getElementById('primary_language_modal');
if (plInput) {
plInput.value = '';
const csdWrap = plInput.closest('.csd-wrap');
if (csdWrap) {
const ico = csdWrap.querySelector('.csd-ico');
const val = csdWrap.querySelector('.csd-val');
if (ico) ico.innerHTML = '<span class="fi fi-xx lsd-flag"></span>';
if (val) { val.textContent = 'Select language'; val.classList.add('ph'); }
}
}
// Reset track 1 form
const t1Title = document.getElementById('lt-track1-title-modal');
const t1Desc = document.getElementById('lt-track1-desc-modal');
const t1Fname = document.getElementById('lt-track1-fname');
if (t1Title) t1Title.value = '';
if (t1Desc) { t1Desc.value = ''; t1Desc.dispatchEvent(new Event('rte:refresh')); }
if (t1Fname) t1Fname.textContent = 'Click to choose file…';
const audioBoxR = document.getElementById('um-tf-t1-audio-box');
if (audioBoxR) audioBoxR.style.borderColor = '';
// Reset track 1 card
const tc1Flag = document.getElementById('um-tc-flag-t1');
const tc1Title = document.getElementById('um-tc-title-t1');
if (tc1Flag) tc1Flag.className = 'fi fi-xx um-tc-flag';
if (tc1Title) tc1Title.textContent = 'Primary — click Edit to add details';
}
function _gsSetDefault(name, value, icon, label) {
const ico = document.getElementById('gs-' + name + '-ico');
const txt = document.getElementById('gs-' + name + '-txt');
const menu = document.getElementById('gs-' + name + '-menu');
const btn = document.getElementById('gs-' + name + '-btn');
if (ico) { ico.className = 'bi ' + icon + ' um-gs-ico'; }
if (txt) txt.textContent = label;
if (menu) menu.querySelectorAll('.um-gs-opt').forEach(li => {
li.classList.toggle('active', li.dataset.value === value);
});
if (btn) btn.classList.remove('open');
}
// ── Apply mode (generic/match vs music) ───────────────────────
function _applyMode(type) {
_currentMode = type;
const isMusic = type === 'music';
// Show/hide "Add Language Track" button
document.getElementById('um-add-track-btn').style.display = isMusic ? '' : 'none';
// Update tracks section label
const lbl = document.getElementById('um-tracks-section-label');
const sub = document.getElementById('um-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 on the track below to add your file and details';
}
// Show/hide fields in popup form (language always visible)
const videoZone = document.getElementById('um-tf-t1-video-zone');
const thumbWrap = document.getElementById('um-tf-t1-thumb-wrap');
const musicPair = document.getElementById('um-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';
}
// Update submit button label
document.getElementById('submit-btn-modal').innerHTML =
'<i class="bi bi-cloud-arrow-up-fill"></i> <span>' + (isMusic ? 'Upload Track' : 'Upload Video') + '</span>';
// Name attributes: route title/description to the right field
const outerTitle = document.getElementById('lt-track1-title-modal');
const outerDesc = document.getElementById('lt-track1-desc-modal');
if (isMusic) {
if (outerTitle) outerTitle.setAttribute('name', 'title');
if (outerDesc) outerDesc.setAttribute('name', 'description');
} else {
// For video mode, use hidden inputs or the same fields (set names below)
if (outerTitle) outerTitle.setAttribute('name', 'title');
if (outerDesc) outerDesc.setAttribute('name', 'description');
}
document.getElementById('type_modal').value = type;
}
// ── Helpers ───────────────────────────────────────────────────
function formatFileSizeModal(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];
}
// ── Global Settings dropdowns ─────────────────────────────────
document.querySelectorAll('.um-gs-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const name = this.id.replace('gs-', '').replace('-btn', '');
const menu = document.getElementById('gs-' + name + '-menu');
const isHidden = menu.hidden;
document.querySelectorAll('.um-gs-menu').forEach(m => { m.hidden = true; });
document.querySelectorAll('.um-gs-btn').forEach(b => b.classList.remove('open'));
menu.hidden = !isHidden;
if (isHidden) this.classList.add('open');
});
});
document.addEventListener('click', function(e) {
if (!e.target.closest('.um-gs-wrap')) {
document.querySelectorAll('.um-gs-menu').forEach(m => { m.hidden = true; });
document.querySelectorAll('.um-gs-btn').forEach(b => b.classList.remove('open'));
}
});
document.querySelectorAll('.um-gs-opt').forEach(opt => {
opt.addEventListener('click', function() {
const name = this.dataset.gs;
const value = this.dataset.value;
const icon = this.dataset.icon;
const label = this.dataset.label;
const ico = document.getElementById('gs-' + name + '-ico');
const txt = document.getElementById('gs-' + name + '-txt');
if (ico) ico.className = 'bi ' + icon + ' um-gs-ico';
if (txt) txt.textContent = label;
this.closest('.um-gs-menu').querySelectorAll('.um-gs-opt').forEach(li => {
li.classList.toggle('active', li === this);
});
this.closest('.um-gs-menu').hidden = true;
const btn = document.getElementById('gs-' + name + '-btn');
if (btn) btn.classList.remove('open');
if (name === 'type') {
_applyMode(value);
} else if (name === 'vis') {
document.getElementById('gs-vis-val').value = value;
} else if (name === 'dl') {
document.getElementById('gs-dl-val').value = value;
}
});
});
// ── Video/audio file handling ─────────────────────────────────
const videoInputModal = document.getElementById('video-modal');
videoInputModal.addEventListener('change', function() { handleVideoSelectModal(this); });
const AUDIO_EXTENSIONS = ['mp3', 'm4a', 'aac', 'flac', 'wav'];
function isAudioFile(file) {
const ext = file.name.split('.').pop().toLowerCase();
return AUDIO_EXTENSIONS.includes(ext) || file.type.startsWith('audio/');
}
function handleVideoSelectModal(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
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();
const audio = isAudioFile(file);
const validVideo = validVideoTypes.includes(file.type) || validVideoExts.includes(ext);
if (!audio && !validVideo) {
showToast('Invalid format. Accepted: MP4, MOV, AVI, WebM, MKV, MP3, M4A, AAC, FLAC, WAV.', 'error');
input.value = '';
return;
}
_fileSelected = true;
// Auto-switch mode based on file type
if (audio && _currentMode !== 'music') {
_applyMode('music');
document.getElementById('gs-type-ico').className = 'bi bi-music-note-beamed um-gs-ico';
document.getElementById('gs-type-txt').textContent = 'Music';
document.querySelectorAll('#gs-type-menu .um-gs-opt').forEach(li => {
li.classList.toggle('active', li.dataset.value === 'music');
});
} else if (!audio && _currentMode === 'music') {
_applyMode('generic');
document.getElementById('gs-type-ico').className = 'bi bi-film um-gs-ico';
document.getElementById('gs-type-txt').textContent = 'Generic';
document.querySelectorAll('#gs-type-menu .um-gs-opt').forEach(li => {
li.classList.toggle('active', li.dataset.value === 'generic');
});
}
// Auto-fill title from filename
const nameWithoutExt = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
const t1Title = document.getElementById('lt-track1-title-modal');
if (t1Title && !t1Title.value) t1Title.value = nameWithoutExt;
// Update UI
if (audio) {
const t1Fname = document.getElementById('lt-track1-fname');
if (t1Fname) t1Fname.textContent = file.name;
const audioBox = document.getElementById('um-tf-t1-audio-box');
if (audioBox) audioBox.style.borderColor = '#22c55e';
} else {
document.getElementById('filename-modal').textContent = file.name;
document.getElementById('filesize-modal').textContent = formatFileSizeModal(file.size);
document.getElementById('um-t1-dz-idle').style.display = 'none';
const fic = document.getElementById('um-t1-file-info');
if (fic) fic.style.display = 'flex';
}
// Update track 1 card
updateTrackCard('t1');
}
function removeVideoModal(e) {
e.preventDefault(); e.stopPropagation();
_fileSelected = false;
videoInputModal.value = '';
document.getElementById('um-t1-dz-idle').style.display = '';
const fic = document.getElementById('um-t1-file-info');
if (fic) fic.style.display = 'none';
const t1Fname = document.getElementById('lt-track1-fname');
if (t1Fname) t1Fname.textContent = 'Click to choose file…';
const audioBox2 = document.getElementById('um-tf-t1-audio-box');
if (audioBox2) audioBox2.style.borderColor = '';
const t1Title = document.getElementById('lt-track1-title-modal');
const t1Desc = document.getElementById('lt-track1-desc-modal');
if (t1Title) t1Title.value = '';
if (t1Desc) { t1Desc.value = ''; t1Desc.dispatchEvent(new Event('rte:refresh')); }
if (_currentMode === 'music') {
_applyMode('generic');
_gsSetDefault('type', 'generic', 'bi-film', 'Generic');
}
updateTrackCard('t1');
}
// ── Thumbnail (video mode) ────────────────────────────────────
const thumbnailDropzoneModal = document.getElementById('thumbnail-dropzone-modal');
let thumbnailInputModal = document.getElementById('thumbnail-modal');
let _thumbClickTsModal = 0;
thumbnailInputModal.addEventListener('change', function() { _thumbClickTsModal = 0; handleThumbnailSelectModal(this); });
thumbnailDropzoneModal.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
const now = Date.now();
if (now - _thumbClickTsModal < 800) return;
_thumbClickTsModal = now;
if (typeof window.openCropperModal_thumb_upload === 'function') {
window.openCropperModal_thumb_upload();
const internal = document.getElementById('tcInput_thumb_upload');
if (internal) internal.click();
} else {
thumbnailInputModal.click();
}
});
thumbnailDropzoneModal.addEventListener('dragover', e => { e.preventDefault(); thumbnailDropzoneModal.classList.add('dragover'); });
thumbnailDropzoneModal.addEventListener('dragleave', () => thumbnailDropzoneModal.classList.remove('dragover'));
thumbnailDropzoneModal.addEventListener('drop', e => {
e.preventDefault(); thumbnailDropzoneModal.classList.remove('dragover');
if (e.dataTransfer.files.length) {
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_upload === 'function') {
window.tcPreload_thumb_upload(droppedFile);
window.openCropperModal_thumb_upload();
} else {
thumbnailInputModal.files = e.dataTransfer.files;
handleThumbnailSelectModal(thumbnailInputModal);
}
}
});
function handleThumbnailSelectModal(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
document.getElementById('thumbnail-filename-modal').textContent = file.name;
document.getElementById('thumbnail-filesize-modal').textContent = formatFileSizeModal(file.size);
const reader = new FileReader();
reader.onload = e => { document.getElementById('thumbnail-preview-img').src = e.target.result; };
reader.readAsDataURL(file);
document.getElementById('thumbnail-default-modal').style.display = 'none';
const thi = document.getElementById('thumbnail-info-modal');
if (thi) thi.style.display = 'flex';
}
function removeThumbnailModal(e) {
e.preventDefault(); e.stopPropagation();
const fresh = thumbnailInputModal.cloneNode(false);
thumbnailInputModal.parentNode.replaceChild(fresh, thumbnailInputModal);
thumbnailInputModal = fresh;
thumbnailInputModal.addEventListener('change', function() { handleThumbnailSelectModal(this); });
document.getElementById('thumbnail-default-modal').style.display = '';
const thi = document.getElementById('thumbnail-info-modal');
if (thi) thi.style.display = 'none';
}
// ── Per-track slides ──────────────────────────────────────────
const _slidesData = {};
let _slidesDragSrc = null;
function slidesZoneClick(e, tid) { if (e.target.closest('button')) return; document.getElementById('slides-input-' + tid)?.click(); }
function slidesZoneDragover(e, tid) {
if (_slidesDragSrc) { e.preventDefault(); return; }
if (Array.from(e.dataTransfer.types||[]).includes('Files')) { e.preventDefault(); document.getElementById('slides-zone-'+tid)?.classList.add('dragover'); }
}
function slidesZoneDragleave(e, tid) { document.getElementById('slides-zone-' + tid)?.classList.remove('dragover'); }
function slidesZoneDrop(e, tid) {
e.preventDefault(); document.getElementById('slides-zone-'+tid)?.classList.remove('dragover');
if (_slidesDragSrc) return;
if (e.dataTransfer.files.length) handleSlidesForTrack(tid, e.dataTransfer.files);
}
function handleSlidesForTrack(tid, fileList) {
_slidesCropStart(fileList, tid);
}
// ── Slides crop queue: every added image is cropped before it enters the strip ──
let _slidesCropQueue = [];
let _slidesCropTid = null;
function _slidesCropStart(fileList, tid) {
const imgs = Array.from(fileList || []).filter(f => f.type && f.type.startsWith('image/'));
if (!imgs.length) return;
_slidesCropTid = tid;
_slidesCropQueue = imgs;
_slidesCropLoadNext();
}
function _slidesCropLoadNext() {
if (!_slidesCropQueue.length) { window.closeCropperModal('slides_upload'); return; }
const f = _slidesCropQueue.shift();
if (typeof window.tcPreload_slides_upload === 'function') {
window.tcPreload_slides_upload(f);
window.openCropperModal_slides_upload();
}
}
function uploadSlidesCropDone(file) {
const tid = _slidesCropTid;
if (tid && file) {
if (!_slidesData[tid]) _slidesData[tid] = [];
if (_slidesData[tid].length < 10) {
_slidesData[tid].push(file);
renderSlidesStrip(tid);
}
}
_slidesCropLoadNext();
}
function renderSlidesStrip(tid) {
const files = _slidesData[tid] || [];
const strip = document.getElementById('slides-strip-' + tid);
const ph = document.getElementById('slides-ph-' + tid);
const prev = document.getElementById('slides-preview-' + tid);
const cnt = document.getElementById('slides-count-' + tid);
const zone = document.getElementById('slides-zone-' + tid);
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 = tid;
wrap.addEventListener('dragstart', function(ev) { _slidesDragSrc = {tid, index: parseInt(this.dataset.index)}; ev.dataTransfer.effectAllowed='move'; setTimeout(()=>{this.style.opacity='.4';},0); });
wrap.addEventListener('dragend', function() { _slidesDragSrc = null; this.style.opacity=''; });
wrap.addEventListener('dragover', function(ev) { if (!_slidesDragSrc||_slidesDragSrc.tid!==tid) return; ev.preventDefault(); this.style.outline='2px solid #e61e1e'; });
wrap.addEventListener('dragleave', function() { this.style.outline=''; });
wrap.addEventListener('drop', function(ev) {
ev.preventDefault(); ev.stopPropagation(); this.style.outline='';
if (!_slidesDragSrc||_slidesDragSrc.tid!==tid) return;
const from=_slidesDragSrc.index, to=parseInt(this.dataset.index);
if (from===to) return;
const arr=_slidesData[tid]; const [moved]=arr.splice(from,1); arr.splice(to,0,moved);
renderSlidesStrip(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', function(ev) { ev.stopPropagation(); _slidesData[tid].splice(index,1); renderSlidesStrip(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='';
const reader = new FileReader(); reader.onload = ev2 => { img.src=ev2.target.result; }; reader.readAsDataURL(f);
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';
}
function clearSlidesForTrack(e, tid) {
e.preventDefault(); e.stopPropagation();
_slidesData[tid] = [];
const input = document.getElementById('slides-input-' + tid);
if (input) { const fresh=input.cloneNode(false); fresh.addEventListener('change',function(){handleSlidesForTrack(tid,this.files);}); input.parentNode.replaceChild(fresh,input); }
renderSlidesStrip(tid);
}
// ── Track popup ───────────────────────────────────────────────
function openTrackPopup(trackId) {
document.querySelectorAll('.um-track-form').forEach(f => f.style.display = 'none');
const form = document.getElementById('um-tf-' + trackId);
if (form) form.style.display = '';
const popup = document.getElementById('um-track-popup');
popup.style.opacity = '0';
popup.style.display = 'flex';
requestAnimationFrame(() => { popup.style.opacity = '1'; });
}
function closeTrackPopup() {
document.getElementById('um-track-popup').style.display = 'none';
updateTrackCard('t1');
for (let i = 1; i <= _umExtraCount; i++) {
if (document.getElementById('um-tc-e' + i)) updateTrackCard('e' + i);
}
}
function updateTrackCard(trackId) {
let langCode = '', title = '';
if (trackId === 't1') {
const li = document.getElementById('primary_language_modal');
const ti = document.getElementById('lt-track1-title-modal');
langCode = li ? li.value : '';
title = ti ? ti.value.trim() : '';
} else {
const n = trackId.replace('e', '');
const uid = 'ltme_' + n;
const li = document.getElementById('csd_v_' + uid);
const ti = document.getElementById('um-tf-title-' + trackId);
langCode = li ? li.value : '';
title = ti ? ti.value.trim() : '';
}
const lang = LANG_OPTIONS_MODAL.find(o => o.value === langCode);
const hasContent = !!(lang || title);
// For track 1: toggle between add button and card
if (trackId === 't1') {
const addBtn = document.getElementById('um-tc-t1-add-btn');
const card = document.getElementById('um-tc-t1-card');
if (addBtn) addBtn.style.display = hasContent ? 'none' : '';
if (card) card.style.display = hasContent ? '' : 'none';
}
const flagEl = document.getElementById('um-tc-flag-' + trackId);
const titleEl = document.getElementById('um-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_MODAL = @json(\App\Data\Languages::forLanguage());
function _ltBuildOptsHtml() {
return LANG_OPTIONS_MODAL.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 addExtraTrackModal() {
const n = ++_umExtraCount;
const trackNum = n + 1;
const uid = 'ltme_' + n;
const card = document.createElement('div');
card.className = 'um-track-card';
card.id = 'um-tc-e' + n;
card.innerHTML = `
<div class="um-tc-body">
<div class="um-tc-left">
<div class="um-tc-num">${trackNum}</div>
<span class="fi fi-xx um-tc-flag" id="um-tc-flag-e${n}"></span>
<div class="um-tc-info">
<span class="um-tc-title" id="um-tc-title-e${n}">Track — select language</span>
</div>
</div>
<div class="um-tc-right">
<button type="button" class="action-btn" onclick="openTrackPopup('e${n}')">
<i class="bi bi-pencil"></i> <span>Edit</span>
</button>
<button type="button" class="action-btn action-btn-danger icon-only" onclick="removeExtraTrackModal(${n})" title="Remove track">
<i class="bi bi-trash"></i>
</button>
</div>
</div>`;
document.getElementById('um-tc-extra').appendChild(card);
const formEl = document.createElement('div');
formEl.className = 'um-track-form';
formEl.id = 'um-tf-e' + n;
formEl.style.display = 'none';
formEl.innerHTML = `
<div class="um-tf-row2" style="margin-bottom:14px;">
<div>
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">Language <span class="um-req">*</span></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">${_ltBuildOptsHtml()}</ul>
<p class="csd-empty" hidden>No results</p>
</div>
<input type="hidden" name="extra_track_languages[]" id="csd_v_${uid}">
</div>
</div>
<div>
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
Title <span class="um-lbl-hint">optional</span>
</label>
<input type="text" name="extra_track_titles[]" id="um-tf-title-e${n}" class="um-input"
style="font-size:13px;padding:9px 12px;" placeholder="Title in this language…">
</div>
</div>
<div style="margin-bottom:14px;">
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
Description <span class="um-lbl-hint">optional</span>
</label>
<div class="rte-wrap" data-min-height="100px">
<textarea class="rte-source" name="extra_track_descriptions[]" id="um-tf-desc-e${n}"
placeholder="Description in this language…"></textarea>
</div>
</div>
<div style="margin-bottom:14px;">
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
<i class="bi bi-music-note-beamed"></i> Audio File
</label>
<label class="lta-file-label"
onmouseenter="this.style.borderColor='#e61e1e'"
onmouseleave="this.style.borderColor='#222'">
<i class="bi bi-music-note-beamed" style="color:#e61e1e;font-size:15px;flex-shrink:0;"></i>
<span class="lta-file-name" id="um-tf-fname-e${n}">Choose audio file…</span>
<input type="file" name="extra_track_files[]"
accept="audio/*,.mp3,.m4a,.aac,.flac,.wav" style="display:none;"
onchange="document.getElementById('um-tf-fname-e${n}').textContent=this.files[0]?.name||'Choose audio file…'">
</label>
</div>
<div>
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">
Cover Slides <span class="um-lbl-hint">optional · drag to reorder</span>
</label>
<div id="slides-zone-e${n}" class="um-slides-zone"
onclick="slidesZoneClick(event,'e${n}')"
ondragover="slidesZoneDragover(event,'e${n}')"
ondragleave="slidesZoneDragleave(event,'e${n}')"
ondrop="slidesZoneDrop(event,'e${n}')">
<input type="file" accept="image/*" multiple style="display:none" id="slides-input-e${n}"
onchange="handleSlidesForTrack('e${n}',this.files)">
<div class="um-slides-ph" id="slides-ph-e${n}">
<i class="bi bi-images"></i>
<span>Click or drag to add cover images</span>
</div>
<div id="slides-preview-e${n}" style="display:none;padding:10px 12px;width:100%;box-sizing:border-box;">
<div id="slides-strip-e${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-e${n}" style="font-size:12px;color:var(--text-secondary);"></span>
<button type="button" onclick="clearSlidesForTrack(event,'e${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>`;
document.getElementById('um-tf-extra').appendChild(formEl);
_slidesData['e' + n] = [];
if (window.CSD) new CSD(uid);
const hiddenInput = document.getElementById('csd_v_' + uid);
if (hiddenInput) {
hiddenInput.addEventListener('change', function() { updateTrackCard('e' + n); });
}
openTrackPopup('e' + n);
}
function removeExtraTrackModal(n) {
const card = document.getElementById('um-tc-e' + n);
const form = document.getElementById('um-tf-e' + n);
if (card) card.remove();
if (form) form.remove();
delete _slidesData['e' + n];
const popup = document.getElementById('um-track-popup');
if (popup.style.display !== 'none') {
const visible = document.querySelector('#um-track-popup .um-track-form[style*="display: block"], #um-track-popup .um-track-form:not([style*="display: none"])');
if (!visible || visible.id === 'um-tf-e' + n) {
popup.style.display = 'none';
}
}
}
// Wire primary language → update track 1 card
;(function() {
const plInput = document.getElementById('primary_language_modal');
if (plInput) {
plInput.addEventListener('change', function() { updateTrackCard('t1'); });
}
}());
// ── Form submission ───────────────────────────────────────────
document.getElementById('upload-form-modal').addEventListener('submit', function(e) {
e.preventDefault();
const videoInput = document.getElementById('video-modal');
const _isMusicMode = _currentMode === 'music';
const _hasFile = videoInput.files && videoInput.files[0];
if (!_hasFile) {
_showUploadError(_isMusicMode ? 'Please choose an audio file in Track 1' : 'Please select a video file');
if (!_isMusicMode) openTrackPopup('t1');
return;
}
const _titleEl = document.getElementById('lt-track1-title-modal');
if (!_titleEl || !_titleEl.value.trim()) {
_showUploadError('Please enter a title');
openTrackPopup('t1');
return;
}
if (_isMusicMode) {
const t1Slides = _slidesData['t1'] || [];
if (!t1Slides.length) {
_showUploadError('At least one cover image is required for audio tracks');
openTrackPopup('t1');
const z = document.getElementById('slides-zone-t1');
if (z) z.style.borderColor = '#e61e1e';
return;
}
}
const formData = new FormData(this);
if (_isMusicMode) {
(_slidesData['t1'] || []).forEach(f => formData.append('slides[]', f));
for (const [tid, files] of Object.entries(_slidesData)) {
if (tid === 't1') continue;
const n = parseInt(tid.replace('e', ''));
files.forEach(f => formData.append('extra_track_slides_' + n + '[]', f));
}
}
const xhr = new XMLHttpRequest();
xhr.timeout = 0;
_uploadInProgress = true;
_fileSelected = false;
document.getElementById('progress-container-modal').classList.add('active');
document.getElementById('submit-btn-modal').disabled = true;
document.getElementById('submit-btn-modal').innerHTML = '<i class="bi bi-arrow-repeat"></i> Uploading…';
document.getElementById('status-message-modal').className = 'status-message-modal';
xhr.upload.addEventListener('progress', function(ev) {
if (ev.lengthComputable) {
const pct = Math.round((ev.loaded / ev.total) * 100);
document.getElementById('progress-bar-modal').style.width = pct + '%';
document.getElementById('progress-text-modal').textContent = 'Uploading… ' + pct + '%';
}
});
xhr.addEventListener('load', function() {
_uploadInProgress = false;
document.getElementById('progress-bar-modal').style.width = '100%';
document.getElementById('progress-text-modal').textContent = 'Processing…';
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
document.getElementById('progress-container-modal').classList.remove('active');
const sm = document.getElementById('status-message-modal');
sm.innerHTML = '<i class="bi bi-check-circle-fill"></i> Upload successful! Taking you to your video…';
sm.className = 'status-message-modal success';
document.getElementById('submit-btn-modal').innerHTML = '<i class="bi bi-check-lg"></i> <span>Uploaded!</span>';
setTimeout(() => { _doCloseModal(); window.location.href = response.redirect; }, 1800);
} else {
_showUploadError(response.message || 'Upload failed');
}
} catch(ex) { _showUploadError('Invalid response from server'); }
} else if (xhr.status === 0) {
_showUploadError('Connection lost. Please check your internet connection.');
} else {
try {
const response = JSON.parse(xhr.responseText);
if (response.errors) {
const msgs = Object.entries(response.errors).map(([f, errs]) => `<b>${f}:</b> ${errs.join(', ')}`).join('<br>');
_showUploadError(msgs);
} else {
_showUploadError(response.message || `Server error (${xhr.status})`);
}
} catch(ex) { _showUploadError(`Upload failed (${xhr.status}): ${xhr.responseText.slice(0,300)}`); }
}
});
xhr.addEventListener('error', () => { _uploadInProgress = false; _showUploadError('Upload failed. Please check your connection.'); });
xhr.addEventListener('timeout', () => { _uploadInProgress = false; _showUploadError('Upload timed out.'); });
xhr.addEventListener('abort', () => { _uploadInProgress = false; _showUploadError('Upload was cancelled.'); });
xhr.open('POST', '{{ route("videos.store") }}');
xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}');
xhr.send(formData);
});
function _showUploadError(message) {
_uploadInProgress = false;
document.getElementById('progress-container-modal').classList.remove('active');
document.getElementById('status-message-modal').innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + message;
document.getElementById('status-message-modal').className = 'status-message-modal error';
document.getElementById('submit-btn-modal').disabled = false;
const isAudio = _currentMode === 'music';
document.getElementById('submit-btn-modal').innerHTML =
'<i class="bi bi-cloud-arrow-up-fill"></i> <span>' + (isAudio ? 'Upload Track' : 'Upload Video') + '</span>';
}
// Initialise generic mode on load
_applyMode('generic');
</script>
<x-image-cropper
id="thumb_upload"
:width="448"
:height="252"
shape="square"
target-input="thumbnail-modal"
preview-img="thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>
<x-image-cropper
id="slides_upload"
:width="448"
:height="252"
shape="square"
output-width="1280"
title="Crop Cover Slide"
result-callback="uploadSlidesCropDone"
/>