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) <noreply@anthropic.com>
1428 lines
76 KiB
PHP
1428 lines
76 KiB
PHP
@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>
|
||
<textarea id="lt-track1-desc-modal" class="um-input um-textarea"
|
||
rows="3" style="font-size:13px;padding:9px 12px;"
|
||
placeholder="Tell viewers about this content…"></textarea>
|
||
</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">1–10 · 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 = '';
|
||
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 = '';
|
||
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) {
|
||
if (!fileList || !fileList.length) return;
|
||
if (!_slidesData[tid]) _slidesData[tid] = [];
|
||
for (const f of Array.from(fileList)) {
|
||
if (_slidesData[tid].length >= 10) break;
|
||
_slidesData[tid].push(f);
|
||
}
|
||
renderSlidesStrip(tid);
|
||
}
|
||
|
||
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>
|
||
<textarea name="extra_track_descriptions[]" class="um-input um-textarea"
|
||
rows="3" style="font-size:13px;padding:9px 12px;"
|
||
placeholder="Description in this language…"></textarea>
|
||
</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"
|
||
/>
|