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>
1774 lines
91 KiB
PHP
1774 lines
91 KiB
PHP
@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 & 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 & 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">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; }
|
||
|
||
/* ── 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"
|
||
/>
|