ghassan f98e5415a3 Add lyrics pipeline, playlist views, admin toggles, and player polish
Lyrics pipeline (Whisper + Demucs + description alignment):
- New GenerateLyricsJob runs WhisperX with VAD filtering and forced word
  alignment, writes per-track JSON to NAS.
- New DecorateLyricsJob calls the active LLM provider to bake one to
  several emojis into each line (heavy decoration prompt).
- LyricsDescriptionParser strips heading content, section markers, and
  emoji-decoration from a song's description while preserving every
  language verbatim.
- correct_whisper_with_description aligner: strong-match anchors only,
  vocal-region-aware gap-fill so missing verses land on actual singing.
- Owner UI for generate/regenerate/edit/delete in the player gear.

Admin pages:
- /admin/lyrics    toggles for VAD, vocal gap-fill, Demucs, master
- /admin/gpu       extracted GPU section, encoder picker, FFmpeg path
- /admin/backup    extracted users-and-settings export/import
- /admin/settings  now AI/LLM only with provider list and Test button
- /admin/nas-storage hosts NAS settings, repair, disable flow, browser
- Shared partials/settings-styles for a uniform look across pages.

Playlist view tracking:
- Migration adds playlists.view_count and playlist_views dedup table.
- Playlist::bumpViewIfNew increments per device with a one-hour window.
- Tracked from /playlists/{id}, /playlists/share/{token}, /ps/{token},
  and /videos/{id}?playlist={token}.  Dispatched after-response so it
  never blocks the page render.
- Loading a playlist on the video page now runs one query instead of
  the four the old getNextVideo/getPreviousVideo path triggered.
- View counts shown on every playlist card and the playlist hero.

Player polish:
- Floating mini-player is draggable, persists its position in
  localStorage, clamps to viewport on resize.
- Mini disabled entirely on mobile (less than 768px).
- New gear-menu Mini Player toggle (persists in localStorage) lets the
  user disable both scroll-activation and SPA-nav-activation.
- Close button keeps media playing when used on the player's own page.
- SPA navigator now swaps a #page-scripts container so per-page JS
  (channel tabs, etc.) gets re-executed after content swaps.

Storage layout:
- Runtime data moved from /storage/* to /data/* and gitignored.
- /ml/venv, /ml/cache, /ml/__pycache__ excluded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 22:01:47 +03:00

1774 lines
91 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
{{-- ── Upload type chooser (desktop) shown before the upload modal ── --}}
<div id="upload-type-chooser" class="utc-overlay" onclick="if(event.target===this)closeUploadChooser()">
<div class="utc-box" role="dialog" aria-modal="true" aria-labelledby="utc-title">
<button type="button" class="btn-close btn-close-white utc-close" onclick="closeUploadChooser()" aria-label="Close"></button>
<div class="utc-head">
<div class="utc-head-icon"><i class="bi bi-stars"></i></div>
<h3 class="utc-title" id="utc-title">What are you creating?</h3>
<p class="utc-sub">Pick a type to get started you can change it later.</p>
</div>
<div class="utc-grid">
<button type="button" class="utc-card" data-accent="generic" onclick="chooseUploadType('generic')">
<span class="utc-card-ico"><i class="bi bi-film"></i></span>
<span class="utc-card-title">Generic</span>
<span class="utc-card-desc">Videos, vlogs &amp; anything else</span>
<i class="bi bi-arrow-right-short utc-card-arr"></i>
</button>
<button type="button" class="utc-card" data-accent="music" onclick="chooseUploadType('music')">
<span class="utc-card-ico"><i class="bi bi-music-note-beamed"></i></span>
<span class="utc-card-title">Music</span>
<span class="utc-card-desc">Songs with cover art &amp; languages</span>
<i class="bi bi-arrow-right-short utc-card-arr"></i>
</button>
<button type="button" class="utc-card" data-accent="match" onclick="closeUploadChooser(); openSportsMatchModal();">
<span class="utc-card-ico"><i class="bi bi-trophy"></i></span>
<span class="utc-card-title">Sports</span>
<span class="utc-card-desc">Create a sports match record</span>
<i class="bi bi-arrow-right-short utc-card-arr"></i>
</button>
</div>
</div>
</div>
<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" id="gs-type-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" id="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 Video Details</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>
{{-- Inline host for the track-1 form (generic/match show fields here, no popup) --}}
<div id="um-t1-inline-host"></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" id="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"
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; }
/* ── Upload type chooser ─────────────────────────────────────── */
.utc-overlay {
position: fixed; inset: 0; z-index: 1065;
background: rgba(8,8,8,.78); backdrop-filter: blur(8px);
display: none; align-items: center; justify-content: center; padding: 20px;
opacity: 0; transition: opacity .22s ease;
}
.utc-overlay.show { display: flex; opacity: 1; }
.utc-box {
position: relative; width: 100%; max-width: 640px;
background: #181818; border: 1px solid #262626; border-radius: 22px;
padding: 30px 30px 34px;
box-shadow: 0 32px 80px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.04);
transform: translateY(14px) scale(.97); transition: transform .26s cubic-bezier(.22,1,.36,1);
}
.utc-overlay.show .utc-box { transform: translateY(0) scale(1); }
.utc-close { position: absolute; top: 18px; right: 18px; }
.utc-head { text-align: center; margin-bottom: 24px; }
.utc-head-icon {
width: 52px; height: 52px; margin: 0 auto 14px;
background: rgba(230,30,30,.13); border: 1px solid rgba(230,30,30,.28);
border-radius: 15px; display: flex; align-items: center; justify-content: center;
font-size: 24px; color: #e61e1e;
}
.utc-title { font-size: 21px; font-weight: 800; color: #f1f1f1; margin: 0 0 6px; letter-spacing: -.01em; }
.utc-sub { font-size: 13px; color: #666; margin: 0; }
.utc-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 560px) { .utc-grid { grid-template-columns: 1fr; } }
.utc-card {
position: relative; display: flex; flex-direction: column; align-items: center; text-align: center;
gap: 4px; padding: 24px 16px 20px;
background: #111; border: 1.5px solid #242424; border-radius: 16px;
color: inherit; font-family: inherit; cursor: pointer;
transition: transform .18s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease;
--accent: #e61e1e;
}
.utc-card[data-accent="generic"] { --accent: #3b82f6; }
.utc-card[data-accent="music"] { --accent: #a855f7; }
.utc-card[data-accent="match"] { --accent: #f59e0b; }
.utc-card:hover {
transform: translateY(-4px); background: #161616;
border-color: color-mix(in srgb, var(--accent) 60%, transparent);
box-shadow: 0 14px 32px rgba(0,0,0,.5);
}
.utc-card-ico {
width: 56px; height: 56px; margin-bottom: 10px;
display: flex; align-items: center; justify-content: center;
border-radius: 16px; font-size: 26px;
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
transition: transform .2s ease;
}
.utc-card:hover .utc-card-ico { transform: scale(1.08) rotate(-4deg); }
.utc-card-title { font-size: 15px; font-weight: 700; color: #f1f1f1; }
.utc-card-desc { font-size: 11.5px; color: #666; line-height: 1.4; max-width: 150px; }
.utc-card-arr {
position: absolute; top: 12px; right: 12px; font-size: 20px;
color: var(--accent); opacity: 0; transform: translateX(-4px);
transition: opacity .18s ease, transform .18s ease;
}
.utc-card:hover .utc-card-arr { opacity: 1; transform: translateX(0); }
/* ════════════════════════════════════════════════════════════════
Unified control system — one consistent look for every field,
dropdown and picker in the upload modal. Scoped to #uploadModal
(which contains the track popup too) so the styling follows the
track-1 form whether it is shown inline or inside the popup, and
the shared .csd-* component is untouched elsewhere in the app.
════════════════════════════════════════════════════════════════ */
/* ── Labels: identical size / weight / colour everywhere ── */
#uploadModal .um-gs-lbl,
#uploadModal .um-lbl,
#uploadModal .csd-lbl {
font-size: 10px; font-weight: 700; letter-spacing: .06em;
text-transform: uppercase; color: #8a8a8a; margin-bottom: 7px;
}
#uploadModal .um-lbl i { color: #e61e1e; font-size: 12px; }
/* ── Buttons, selects & inputs: same height / radius / border / bg ── */
#uploadModal .um-gs-btn,
#uploadModal .csd-btn,
#uploadModal .um-input {
min-height: 50px;
background: #161616;
border: 1px solid #2c2c2c;
border-radius: 12px;
font-size: 14px;
color: #f1f1f1;
box-sizing: border-box;
transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
}
#uploadModal .um-gs-btn,
#uploadModal .csd-btn {
display: flex; align-items: center; gap: 10px;
padding: 0 15px; line-height: 1.2;
}
#uploadModal .um-input { padding: 13px 15px; }
/* Hover / focus / open — red accent matching the modal theme */
#uploadModal .um-gs-btn:hover, #uploadModal .um-gs-btn.open,
#uploadModal .csd-btn:hover, #uploadModal .csd-btn[aria-expanded="true"],
#uploadModal .um-input:focus {
border-color: #e61e1e;
background: #1b1414;
outline: none;
}
#uploadModal .um-gs-btn.open,
#uploadModal .csd-btn[aria-expanded="true"],
#uploadModal .um-input:focus { box-shadow: 0 0 0 3px rgba(230,30,30,.13); }
/* Icons & chevrons — sized up; chevron picks up the red accent on open */
#uploadModal .um-gs-ico { font-size: 16px; color: #b85656; }
#uploadModal .csd-ico { font-size: 18px; }
#uploadModal .um-gs-arr,
#uploadModal .csd-arr { font-size: 11px; color: #777; margin-left: auto; }
#uploadModal .um-gs-btn.open .um-gs-arr,
#uploadModal .csd-btn[aria-expanded="true"] .csd-arr { color: #e61e1e; }
#uploadModal .um-gs-txt,
#uploadModal .csd-val { flex: 1; color: #f1f1f1; }
#uploadModal .csd-val.ph { color: #5f5f5f; }
/* ── Dropdown panels: dark surface tuned to the modal, red-accented options ── */
#uploadModal .um-gs-menu,
#uploadModal .csd-panel {
background: #1a1a1a;
border: 1px solid #2c2c2c;
border-radius: 13px;
box-shadow: 0 18px 44px rgba(0,0,0,.65), 0 0 0 1px rgba(230,30,30,.04);
padding: 7px;
}
#uploadModal .um-gs-opt,
#uploadModal .csd-opt {
border-radius: 9px;
padding: 11px 12px;
font-size: 14px;
color: #cfcfcf;
gap: 10px;
}
#uploadModal .um-gs-opt:hover,
#uploadModal .csd-opt:hover {
background: rgba(230,30,30,.10);
color: #ffffff;
}
#uploadModal .um-gs-opt.active,
#uploadModal .csd-opt[aria-selected="true"] {
background: rgba(230,30,30,.16);
color: #ff6b6b;
}
#uploadModal .um-gs-opt i { font-size: 16px; width: 18px; color: inherit; }
/* Language search box inside the picker — match the surface */
#uploadModal .csd-srch { border-bottom: 1px solid #2c2c2c; padding: 11px 13px; }
#uploadModal .csd-list { padding: 5px; }
#uploadModal .csd-sinput { font-size: 14px; }
/* ── Rich-text editor (description) — align border & radius only ── */
#uploadModal .rte-wrap {
border-color: #2b2b2b !important;
border-radius: 11px !important;
overflow: hidden;
}
#uploadModal .rte-wrap:focus-within { border-color: #e61e1e !important; }
</style>
<script>
// ── State ─────────────────────────────────────────────────────
let _uploadInProgress = false;
let _fileSelected = false;
let _umExtraCount = 0;
let _currentMode = 'generic'; // 'generic', 'music', 'match'
// ── Type chooser (desktop) ────────────────────────────────────
const _UTC_META = {
generic: { icon: 'bi-film', label: 'Generic' },
music: { icon: 'bi-music-note-beamed', label: 'Music' },
match: { icon: 'bi-trophy', label: 'Match' },
};
function openUploadChooser() {
// Mobile keeps the existing full-page create flow (with its own type picker)
if (window.innerWidth < 992) {
window.location.href = '{{ route("videos.create") }}';
return;
}
document.getElementById('upload-type-chooser').classList.add('show');
}
function closeUploadChooser() {
document.getElementById('upload-type-chooser').classList.remove('show');
}
function chooseUploadType(type) {
const meta = _UTC_META[type] || _UTC_META.generic;
closeUploadChooser();
openUploadModal();
// Apply the chosen content type once the modal is on screen
setTimeout(() => {
_gsSetDefault('type', type, meta.icon, meta.label);
_applyMode(type);
// Type was already picked from the chooser cards — hide the redundant dropdown
const typeWrap = document.getElementById('gs-type-wrap');
if (typeWrap) typeWrap.style.display = 'none';
}, 60);
}
// Close chooser on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && document.getElementById('upload-type-chooser')?.classList.contains('show')) {
closeUploadChooser();
}
});
// ── 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';
// The tracks-section header (label + subtitle) is only meaningful for music's
// language-track list — hide it for generic/match where fields are shown inline.
const tracksHeader = document.getElementById('um-tracks-header');
if (tracksHeader) tracksHeader.style.display = isMusic ? '' : 'none';
// Update tracks section label (music only)
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';
}
// Empty-state primary button label — "Language Track" wording is exclusive to music
const t1AddLbl = document.querySelector('#um-tc-t1-add-btn span');
if (t1AddLbl) {
t1AddLbl.textContent = isMusic ? 'Add Language Track'
: (type === 'match' ? 'Add Match Details' : 'Add Video Details');
}
// The single "Language" field is universal metadata (what language the content is in) —
// shown for every type. Only the multi-track "Add Language Track" feature above is music-only.
const langWrap = document.getElementById('um-tf-t1-lang-wrap');
if (langWrap) langWrap.style.display = '';
// "Primary track" wording is a music concept — hide it for generic/match
const primaryNote = document.querySelector('#um-tf-t1 .um-tf-primary-note');
const primaryBadge = document.querySelector('#um-tc-t1-card .um-tc-primary');
if (primaryNote) primaryNote.style.display = isMusic ? '' : 'none';
if (primaryBadge) primaryBadge.style.display = isMusic ? '' : 'none';
// Show/hide fields in popup form
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;
// Generic/match: show the track-1 fields inline in the modal (no button/card/popup).
// Music: keep the track-card + Track Editor popup workflow (multiple language tracks).
_positionT1Form(isMusic);
}
// Relocate the track-1 form between the inline host (generic/match) and the popup (music)
function _positionT1Form(isMusic) {
const form = document.getElementById('um-tf-t1');
const host = document.getElementById('um-t1-inline-host');
const tcList = document.getElementById('um-tc-list');
const tpBody = document.getElementById('um-tp-body');
const extra = document.getElementById('um-tf-extra');
if (!form) return;
if (isMusic) {
// Form lives in the popup, hidden until the user opens a track for editing
if (tpBody && form.parentElement !== tpBody) tpBody.insertBefore(form, extra);
form.style.display = 'none';
if (host) host.style.display = 'none';
if (tcList) tcList.style.display = '';
updateTrackCard('t1');
} else {
// Fields are shown directly in the modal body
if (host && form.parentElement !== host) host.appendChild(form);
form.style.display = '';
if (host) host.style.display = '';
if (tcList) tcList.style.display = 'none';
}
}
// ── 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) {
// Generic/match: track-1 fields are inline in the modal — bring them into view instead of a popup
if (trackId === 't1' && _currentMode !== 'music') {
const host = document.getElementById('um-t1-inline-host');
if (host) host.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
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));
// Walk extra-track form sections in DOM order so the slide index lines up
// with the positional `extra_track_files[]` the form already submits.
var extraForms = document.querySelectorAll('#um-tf-extra .um-track-form');
extraForms.forEach(function (el, domIdx) {
var m = el.id.match(/^um-tf-(e\d+)$/);
if (!m) return;
var tid = m[1];
var files = _slidesData[tid] || [];
files.forEach(function (f) {
formData.append('extra_track_slides[' + domIdx + '][]', 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"
/>