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>
1115 lines
63 KiB
PHP
1115 lines
63 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('main_class', 'upload-page-only upload-page-responsive')
|
|
|
|
@section('title', 'Upload Video | ' . config('app.name'))
|
|
|
|
@section('extra_styles')
|
|
<style>
|
|
/* Hide custom header on mobile */
|
|
.upload-page-header { display: none !important; }
|
|
|
|
@media (max-width: 991px) {
|
|
.upload-page-only .yt-header, .upload-page-only .yt-sidebar { display: block !important; }
|
|
.upload-page-only .yt-main { display: block !important; margin-left: 0; padding-bottom: 80px; }
|
|
.upload-page-only.upload-page-responsive { display: block; background: transparent; padding: 0; }
|
|
.upload-page-only.upload-page-responsive .upload-page-standalone { max-width: 100%; border-radius: 0; box-shadow: none; background: var(--bg-primary); border: none; padding: 16px; }
|
|
.upload-page-only.upload-page-responsive .upload-page-header { display: none !important; }
|
|
.upload-page-only.upload-page-responsive .upload-page-body { padding: 0; background: transparent; }
|
|
.upload-page-only.upload-page-responsive .form-input,
|
|
.upload-page-only.upload-page-responsive .form-textarea { background: var(--bg-secondary); border-color: var(--border-color); color: var(--text-primary); }
|
|
.upload-page-only.upload-page-responsive .form-label { color: var(--text-primary); }
|
|
#dropzone { padding: 40px 16px; }
|
|
.options-grid { flex-direction: column; }
|
|
.option-item { min-width: 100%; }
|
|
}
|
|
|
|
@media (min-width: 992px) {
|
|
.upload-page-only { min-height: auto; display: block; padding: 20px; background: #121212; }
|
|
}
|
|
|
|
.upload-page-standalone {
|
|
background: #1a1a1a;
|
|
border: none;
|
|
border-radius: 0;
|
|
box-shadow: none;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.upload-page-body { padding: 0; background: transparent; }
|
|
|
|
/* Form Groups */
|
|
.form-group { margin-bottom: 20px; }
|
|
.form-label { display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; color: #e5e5e5; }
|
|
.form-input, .form-textarea { width: 100%; background: #121212; border: 1px solid #333; border-radius: 8px; padding: 12px 16px; color: var(--text-primary); font-size: 14px; }
|
|
.form-input:focus, .form-textarea:focus { outline: none; border-color: var(--brand-red); }
|
|
.form-textarea { resize: vertical; min-height: 100px; }
|
|
|
|
/* Dropzone */
|
|
#dropzone {
|
|
border: 2px dashed var(--border-color);
|
|
border-radius: 12px;
|
|
padding: 60px 20px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background: #151515;
|
|
}
|
|
#dropzone:hover, #dropzone.dragover { border-color: var(--brand-red); background: rgba(230, 30, 30, 0.05); }
|
|
#dropzone .icon { font-size: 48px; color: var(--text-secondary); margin-bottom: 16px; }
|
|
#dropzone p { color: var(--text-secondary); margin: 8px 0; }
|
|
#dropzone .hint { font-size: 12px; color: var(--text-secondary); opacity: 0.7; }
|
|
|
|
#video { display: none; }
|
|
|
|
#file-info { display: none; }
|
|
#file-info.active { display: flex; align-items: center; gap: 12px; padding: 16px; background: #1f1f1f; border-radius: 12px; border: 1px solid #333; }
|
|
#file-info .file-icon { width: 56px; height: 56px; background: #333; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; flex-shrink: 0; }
|
|
#file-info .file-details { flex: 1; text-align: left; }
|
|
#file-info .filename { color: #e5e5e5; font-weight: 500; font-size: 14px; margin: 0 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
#file-info .filesize { color: #888; font-size: 13px; margin: 0; }
|
|
#file-info .btn-remove { width: 32px; height: 32px; border-radius: 50%; background: #333; border: none; color: #888; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
|
|
/* Options Grid */
|
|
.options-section { margin-bottom: 24px; }
|
|
.options-label { font-size: 14px; font-weight: 500; color: #e5e5e5; margin-bottom: 12px; display: block; }
|
|
.options-grid { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
.option-item { flex: 1; min-width: 100px; cursor: pointer; }
|
|
.option-item input { display: none; }
|
|
.option-content { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: #1a1a1a; border: 2px solid #333; border-radius: 10px; transition: all 0.2s; }
|
|
.option-item:hover .option-content { border-color: #555; background: #1f1f1f; }
|
|
.option-item.active .option-content { border-color: var(--brand-red); background: rgba(230, 30, 30, 0.1); }
|
|
.option-content i { font-size: 18px; color: #666; width: 20px; text-align: center; }
|
|
.option-item.active .option-content i { color: var(--brand-red); }
|
|
.option-content span { font-size: 13px; color: #aaa; }
|
|
.option-item.active .option-content span { color: #fff; }
|
|
|
|
/* Language track accordion (create page) */
|
|
.lta-create { display: flex; flex-direction: column; gap: 6px; margin-bottom: 0; }
|
|
.ltac-item { background: #151515; border: 1px solid #333; border-radius: 10px; overflow: hidden; transition: border-color .2s; }
|
|
.ltac-item.open { border-color: rgba(230,30,30,.45); }
|
|
.ltac-header { display: flex; align-items: center; justify-content: space-between; padding: 11px 14px; cursor: pointer; user-select: none; gap: 8px; }
|
|
.ltac-hd-left { display: flex; align-items: center; gap: 9px; flex: 1; min-width: 0; }
|
|
.ltac-num { width: 22px; height: 22px; border-radius: 50%; background: rgba(230,30,30,.15); border: 1px solid rgba(230,30,30,.3); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: var(--brand-red); flex-shrink: 0; }
|
|
.ltac-flag-cell { flex-shrink: 0; }
|
|
.ltac-name { font-size: 13px; color: #888; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.ltac-item.open .ltac-name { color: #e5e5e5; }
|
|
.ltac-arr { font-size: 12px; color: #555; transition: transform .2s, color .2s; flex-shrink: 0; }
|
|
.ltac-body { padding: 12px 14px 14px; border-top: 1px solid #2a2a2a; }
|
|
.ltac-item:not(.open) .ltac-body { display: none; }
|
|
.ltac-remove { background: none; border: none; color: #555; cursor: pointer; font-size: 13px; padding: 3px 6px; border-radius: 4px; transition: color .15s, background .15s; display: flex; align-items: center; }
|
|
.ltac-remove:hover { color: var(--brand-red); background: rgba(230,30,30,.1); }
|
|
.ltac-file-label { display: flex; align-items: center; gap: 8px; background: #121212; border: 1px solid #333; border-radius: 8px; padding: 10px 12px; cursor: pointer; transition: border-color .15s; }
|
|
.ltac-file-name { font-size: 12px; color: #888; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
.ltac-slides-zone { border: 1.5px dashed #333; border-radius: 10px; min-height: 70px; background: #121212; cursor: pointer; transition: border-color .2s; display: flex; align-items: center; justify-content: center; }
|
|
.ltac-slides-zone:hover { border-color: var(--brand-red); }
|
|
.ltac-slides-zone.dragover { border-color: var(--brand-red); background: rgba(230,30,30,.05); }
|
|
.ltac-slides-ph { display: flex; align-items: center; gap: 8px; color: #555; font-size: 13px; padding: 16px; }
|
|
.ltac-slides-ph i { color: #444; }
|
|
|
|
/* Two Column Layout */
|
|
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
|
|
/* Thumbnail Dropzone */
|
|
#thumbnail-dropzone {
|
|
border: 2px dashed #404040;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background: #151515;
|
|
position: relative;
|
|
}
|
|
#thumbnail-dropzone:hover { border-color: var(--brand-red); }
|
|
#thumbnail { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
|
|
#thumbnail-default i { font-size: 28px; color: #666; margin-bottom: 6px; }
|
|
#thumbnail-default p { color: #888; font-size: 13px; margin: 0; }
|
|
#thumbnail-info { display: none; }
|
|
#thumbnail-info.active { display: block; }
|
|
#thumbnail-info img { max-width: 100%; max-height: 120px; border-radius: 8px; }
|
|
|
|
/* Progress */
|
|
#progress-container { display: none; margin: 20px 0; }
|
|
#progress-container.active { display: block; }
|
|
.progress-bar-fill { background: var(--brand-red); height: 4px; width: 0%; transition: width 0.3s; border-radius: 2px; }
|
|
.progress-text { text-align: center; color: var(--text-secondary); font-size: 13px; margin-top: 8px; }
|
|
|
|
/* Status */
|
|
#status-message { display: none; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
|
|
#status-message.success { display: block; background: rgba(34, 197, 94, 0.2); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.3); }
|
|
#status-message.error { display: block; background: rgba(239, 68, 68, 0.2); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
|
|
|
|
/* Submit Button */
|
|
.btn-submit {
|
|
width: 100%;
|
|
justify-content: center;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
}
|
|
.btn-submit:disabled { background: #555 !important; border-color: #555 !important; cursor: not-allowed; opacity: 1; }
|
|
|
|
@media (max-width: 600px) {
|
|
.form-row { grid-template-columns: 1fr; }
|
|
.options-grid { flex-direction: column; }
|
|
.option-item { min-width: 100%; }
|
|
}
|
|
</style>
|
|
@endsection
|
|
|
|
@section('content')
|
|
<div class="upload-page-standalone">
|
|
<div class="upload-page-body">
|
|
<form id="upload-form" enctype="multipart/form-data">
|
|
@csrf
|
|
|
|
<!-- Video File Dropzone -->
|
|
<div class="form-group" id="file-dropzone-group">
|
|
<label class="form-label"><i class="bi bi-camera-video" id="file-label-icon"></i> <span id="file-label-text">Video or Music File *</span></label>
|
|
<div id="dropzone">
|
|
<input type="file" name="video" id="video" accept="video/*,audio/mpeg,audio/mp4,audio/aac,audio/flac,audio/wav,.mp3,.m4a,.aac,.flac,.wav" required>
|
|
<div id="dropzone-default">
|
|
<div class="icon"><i class="bi bi-cloud-arrow-up"></i></div>
|
|
<p>Click to select or drag file here</p>
|
|
<p class="hint">Video: MP4, MOV, AVI, WebM — Audio: MP3, M4A, AAC, FLAC</p>
|
|
</div>
|
|
<div id="file-info">
|
|
<div class="file-icon"><i class="bi bi-film" id="file-preview-icon"></i></div>
|
|
<div class="file-details">
|
|
<p class="filename" id="filename"></p>
|
|
<p class="filesize" id="filesize"></p>
|
|
</div>
|
|
<button type="button" class="btn-remove" onclick="removeVideo(event)"><i class="bi bi-x-lg"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Title + Description (video mode) -->
|
|
<div id="basic-fields-create">
|
|
<div class="form-group">
|
|
<x-language-select
|
|
name="primary_language"
|
|
id="video_language_create"
|
|
label="Language"
|
|
placeholder="Select language"
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Title *</label>
|
|
<input type="text" name="title" id="video-title" class="form-input" placeholder="Enter video title">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<x-rich-text-editor name="description" id="video-description" placeholder="Tell viewers about your video…" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Thumbnail (video) -->
|
|
<div class="form-group" id="thumbnail-section">
|
|
<label class="form-label"><i class="bi bi-image"></i> <span id="thumbnail-label-text">Thumbnail</span></label>
|
|
<div id="thumbnail-dropzone">
|
|
<input type="file" name="thumbnail" id="thumbnail" accept="image/*">
|
|
<div id="thumbnail-default">
|
|
<i class="bi bi-card-image"></i>
|
|
<p id="thumbnail-dropzone-hint">Click to select thumbnail (optional)</p>
|
|
</div>
|
|
<div id="thumbnail-info">
|
|
<div style="display:flex;align-items:center;gap:12px;padding:12px;background:#1f1f1f;border-radius:8px;border:1px solid #333;">
|
|
<div style="width:80px;height:48px;overflow:hidden;border-radius:6px;flex-shrink:0;">
|
|
<img id="thumbnail-preview-img" src="" alt="Thumbnail preview" style="width:100%;height:100%;object-fit:cover;">
|
|
</div>
|
|
<div style="flex:1;text-align:left;min-width:0;">
|
|
<p id="thumbnail-filename" style="color:#e5e5e5;font-weight:500;font-size:13px;margin:0 0 2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></p>
|
|
<p id="thumbnail-filesize" style="color:#888;font-size:12px;margin:0;"></p>
|
|
</div>
|
|
<button type="button" class="btn-remove" onclick="removeThumbnail(event)" style="flex-shrink:0;"><i class="bi bi-x-lg"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Language Tracks accordion (audio mode only) -->
|
|
<div class="form-group" id="lang-tracks-section-create" style="display:none;">
|
|
<label class="form-label"><i class="bi bi-translate"></i> Language Tracks</label>
|
|
|
|
<div id="lta-create" class="lta-create">
|
|
|
|
<!-- Track 1 — always present, primary -->
|
|
<div class="ltac-item open" id="ltac-t1">
|
|
<div class="ltac-header" onclick="ltToggleCreate('t1')">
|
|
<div class="ltac-hd-left">
|
|
<span class="ltac-num">1</span>
|
|
<span class="ltac-flag-cell" id="ltac-t1-flag">
|
|
<span class="fi fi-xx lsd-flag" style="width:22px;height:16px;"></span>
|
|
</span>
|
|
<span class="ltac-name" id="ltac-t1-name">Primary — Select language</span>
|
|
</div>
|
|
<i class="bi bi-chevron-up ltac-arr" style="color:var(--brand-red);" id="ltac-t1-arr"></i>
|
|
</div>
|
|
<div class="ltac-body" id="ltac-t1-body">
|
|
|
|
<!-- Audio File Picker -->
|
|
<div style="margin-bottom:12px;" id="ltc-track1-file-wrap">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;"><i class="bi bi-music-note-beamed"></i> Audio File *</label>
|
|
<label class="ltac-file-label"
|
|
onclick="document.getElementById('video').click()"
|
|
onmouseenter="this.style.borderColor='var(--brand-red)'"
|
|
onmouseleave="this.style.borderColor='#333'">
|
|
<i class="bi bi-music-note-beamed" style="color:var(--brand-red);font-size:16px;flex-shrink:0;"></i>
|
|
<span class="ltac-file-name" id="ltc-track1-fname">Choose audio file…</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Title (audio mode) -->
|
|
<div style="margin-bottom:12px;">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;">Title *</label>
|
|
<input type="text" id="lt-track1-title-create" class="form-input"
|
|
style="font-size:13px;" placeholder="Track title…" autocomplete="off">
|
|
</div>
|
|
|
|
<!-- Description (audio mode) -->
|
|
<div style="margin-bottom:14px;">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;">Description</label>
|
|
<x-rich-text-editor name="" id="lt-track1-desc-create" min-height="80px" placeholder="Tell viewers about this track…" />
|
|
</div>
|
|
|
|
<!-- Slides -->
|
|
<div style="margin-bottom:14px;">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
|
|
<i class="bi bi-images"></i> Cover Slides <span style="color:var(--brand-red);">*</span>
|
|
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:4px;">drag to reorder</span>
|
|
</label>
|
|
<div id="slides-zone-ct1" class="ltac-slides-zone"
|
|
onclick="cSlidesZoneClick(event,'ct1')"
|
|
ondragover="cSlidesZoneDragover(event,'ct1')"
|
|
ondragleave="cSlidesZoneDragleave(event,'ct1')"
|
|
ondrop="cSlidesZoneDrop(event,'ct1')">
|
|
<input type="file" accept="image/*" multiple style="display:none" id="slides-input-ct1"
|
|
onchange="handleCSlidesForTrack('ct1',this.files)">
|
|
<div class="ltac-slides-ph" id="slides-ph-ct1">
|
|
<i class="bi bi-images" style="font-size:22px;"></i>
|
|
<span>Click or drag to add cover images</span>
|
|
</div>
|
|
<div id="slides-preview-ct1" style="display:none;padding:10px 12px;width:100%;box-sizing:border-box;">
|
|
<div id="slides-strip-ct1" class="slides-strip" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;"></div>
|
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
<span id="slides-count-ct1" style="font-size:12px;color:var(--text-secondary);"></span>
|
|
<button type="button" onclick="clearCSlidesForTrack(event,'ct1')"
|
|
style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:0;display:flex;align-items:center;gap:4px;">
|
|
<i class="bi bi-x-lg"></i> Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Primary Language -->
|
|
<div>
|
|
<x-language-select
|
|
name="primary_language"
|
|
id="primary_language_create"
|
|
label="Language"
|
|
placeholder="Select language"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Extra tracks appended here by JS -->
|
|
<div id="ltac-extra"></div>
|
|
</div>
|
|
|
|
<button type="button" class="action-btn" onclick="addLangTrackCreate()"
|
|
style="font-size:13px;margin-top:8px;">
|
|
<i class="bi bi-plus-circle"></i> <span>Add Language Track</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Video Type -->
|
|
<div class="options-section">
|
|
<span class="options-label">Video Type</span>
|
|
<div class="options-grid" id="type-options">
|
|
<label class="option-item active" data-type="generic">
|
|
<input type="radio" name="type" value="generic" checked>
|
|
<div class="option-content">
|
|
<i class="bi bi-film"></i>
|
|
<span>Generic</span>
|
|
</div>
|
|
</label>
|
|
<label class="option-item" data-type="music">
|
|
<input type="radio" name="type" value="music">
|
|
<div class="option-content">
|
|
<i class="bi bi-music-note"></i>
|
|
<span>Music</span>
|
|
</div>
|
|
</label>
|
|
<label class="option-item" data-type="match">
|
|
<input type="radio" name="type" value="match">
|
|
<div class="option-content">
|
|
<i class="bi bi-trophy"></i>
|
|
<span>Match</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Privacy -->
|
|
<div class="options-section">
|
|
<span class="options-label">Privacy</span>
|
|
<div class="options-grid" id="visibility-options">
|
|
<label class="option-item active" data-privacy="public">
|
|
<input type="radio" name="visibility" value="public" checked>
|
|
<div class="option-content">
|
|
<i class="bi bi-globe"></i>
|
|
<span>Public</span>
|
|
</div>
|
|
</label>
|
|
<label class="option-item" data-privacy="unlisted">
|
|
<input type="radio" name="visibility" value="unlisted">
|
|
<div class="option-content">
|
|
<i class="bi bi-link-45deg"></i>
|
|
<span>Unlisted</span>
|
|
</div>
|
|
</label>
|
|
<label class="option-item" data-privacy="private">
|
|
<input type="radio" name="visibility" value="private">
|
|
<div class="option-content">
|
|
<i class="bi bi-lock"></i>
|
|
<span>Private</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden Shorts - Auto-detected -->
|
|
<input type="hidden" name="is_shorts" id="is_shorts" value="0">
|
|
|
|
<!-- Download Access -->
|
|
<div class="form-group" style="margin-bottom: 16px;">
|
|
<label class="form-label">
|
|
<i class="bi bi-download"></i> Who can download?
|
|
</label>
|
|
<div class="options-grid" id="download-options">
|
|
<label class="option-item active" data-download="disabled">
|
|
<input type="radio" name="download_access" value="disabled" checked>
|
|
<div class="option-content">
|
|
<i class="bi bi-slash-circle"></i>
|
|
<span>Disabled</span>
|
|
</div>
|
|
</label>
|
|
<label class="option-item" data-download="everyone">
|
|
<input type="radio" name="download_access" value="everyone">
|
|
<div class="option-content">
|
|
<i class="bi bi-globe"></i>
|
|
<span>Everyone</span>
|
|
</div>
|
|
</label>
|
|
<label class="option-item" data-download="registered">
|
|
<input type="radio" name="download_access" value="registered">
|
|
<div class="option-content">
|
|
<i class="bi bi-person-check"></i>
|
|
<span>Members</span>
|
|
</div>
|
|
</label>
|
|
<label class="option-item" data-download="subscribers">
|
|
<input type="radio" name="download_access" value="subscribers">
|
|
<div class="option-content">
|
|
<i class="bi bi-star"></i>
|
|
<span>Subscribers</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div id="progress-container">
|
|
<div class="progress-bar-wrapper">
|
|
<div id="progress-bar" class="progress-bar-fill"></div>
|
|
</div>
|
|
<p id="progress-text" class="progress-text">Uploading... 0%</p>
|
|
</div>
|
|
|
|
<!-- Status Message -->
|
|
<div id="status-message"></div>
|
|
|
|
<!-- Submit Button -->
|
|
<button type="submit" id="submit-btn" class="action-btn action-btn-primary btn-submit">
|
|
<i class="bi bi-cloud-arrow-up-fill"></i> <span>Upload Video</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@section('scripts')
|
|
<script>
|
|
const dropzone = document.getElementById('dropzone');
|
|
const fileInput = document.getElementById('video');
|
|
let thumbnailInput = document.getElementById('thumbnail');
|
|
const thumbnailDropzone = document.getElementById('thumbnail-dropzone');
|
|
let currentFile = null;
|
|
let _pageUploadInProgress = false;
|
|
|
|
// Warn before navigating away when a file is selected or upload is running
|
|
window.addEventListener('beforeunload', function(e) {
|
|
if (currentFile && !_pageUploadInProgress) {
|
|
e.preventDefault();
|
|
e.returnValue = '';
|
|
}
|
|
});
|
|
|
|
// ── Helpers ──────────────────────────────────────────────
|
|
function formatFileSize(bytes) {
|
|
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];
|
|
}
|
|
|
|
const AUDIO_EXTS = ['mp3', 'm4a', 'aac', 'flac', 'wav'];
|
|
function isAudioFile(file) {
|
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
return AUDIO_EXTS.includes(ext) || file.type.startsWith('audio/');
|
|
}
|
|
|
|
function setAudioMode(audio) {
|
|
document.getElementById('file-label-icon').className = audio ? 'bi bi-music-note-beamed' : 'bi bi-camera-video';
|
|
document.getElementById('file-label-text').textContent = audio ? 'Music File *' : 'Video or Music File *';
|
|
|
|
// Show/hide sections
|
|
document.getElementById('file-dropzone-group').style.display = audio ? 'none' : '';
|
|
document.getElementById('ltc-track1-file-wrap').style.display = audio ? '' : 'none';
|
|
document.getElementById('basic-fields-create').style.display = audio ? 'none' : '';
|
|
document.getElementById('thumbnail-section').style.display = audio ? 'none' : '';
|
|
document.getElementById('lang-tracks-section-create').style.display = audio ? '' : 'none';
|
|
if (audio) {
|
|
const z = document.getElementById('slides-zone-ct1');
|
|
if (z && !(_cSlidesData['ct1'] && _cSlidesData['ct1'].length)) z.style.borderColor = 'var(--brand-red)';
|
|
}
|
|
|
|
// Swap name attributes so only the active set submits
|
|
const outerTitle = document.getElementById('video-title');
|
|
const outerDesc = document.getElementById('video-description');
|
|
const innerTitle = document.getElementById('lt-track1-title-create');
|
|
const innerDesc = document.getElementById('lt-track1-desc-create');
|
|
if (audio) {
|
|
outerTitle.removeAttribute('name'); outerDesc.removeAttribute('name');
|
|
innerTitle.setAttribute('name', 'title'); innerDesc.setAttribute('name', 'description');
|
|
} else {
|
|
outerTitle.setAttribute('name', 'title'); outerDesc.setAttribute('name', 'description');
|
|
innerTitle.removeAttribute('name'); innerDesc.removeAttribute('name');
|
|
}
|
|
|
|
// Language: video mode submits the basic-fields picker; music mode submits the track-1 picker.
|
|
// Only one carries name="primary_language" at a time so the value never collides.
|
|
const audioLang = document.getElementById('primary_language_create');
|
|
const videoLang = document.getElementById('video_language_create');
|
|
if (audio) {
|
|
if (videoLang) videoLang.removeAttribute('name');
|
|
if (audioLang) audioLang.setAttribute('name', 'primary_language');
|
|
} else {
|
|
if (audioLang) audioLang.removeAttribute('name');
|
|
if (videoLang) videoLang.setAttribute('name', 'primary_language');
|
|
}
|
|
|
|
if (audio) {
|
|
document.querySelectorAll('#type-options .option-item').forEach(o => o.classList.remove('active'));
|
|
const musicOpt = document.querySelector('#type-options .option-item[data-type="music"]');
|
|
if (musicOpt) { musicOpt.classList.add('active'); musicOpt.querySelector('input').checked = true; }
|
|
}
|
|
|
|
document.getElementById('file-preview-icon').className = audio ? 'bi bi-music-note-beamed' : 'bi bi-film';
|
|
document.getElementById('submit-btn').innerHTML =
|
|
'<i class="bi bi-cloud-arrow-up-fill"></i> <span>' + (audio ? 'Upload Track' : 'Upload Video') + '</span>';
|
|
}
|
|
|
|
// ── Video/Audio File ─────────────────────────────────────
|
|
fileInput.addEventListener('change', function() { handleFileSelect(this); });
|
|
dropzone.addEventListener('click', function(e) {
|
|
if (e.target.closest('.btn-remove')) return;
|
|
fileInput.click();
|
|
});
|
|
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dragover'); });
|
|
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
|
|
dropzone.addEventListener('drop', e => {
|
|
e.preventDefault(); dropzone.classList.remove('dragover');
|
|
if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleFileSelect(fileInput); }
|
|
});
|
|
|
|
function handleFileSelect(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) {
|
|
showError('Invalid format. Accepted: MP4, MOV, AVI, WebM, MKV, MP3, M4A, AAC, FLAC, WAV.');
|
|
input.value = ''; return;
|
|
}
|
|
|
|
currentFile = file;
|
|
setAudioMode(audio);
|
|
|
|
const titleWithoutExt = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
|
|
if (audio) {
|
|
document.getElementById('lt-track1-title-create').value = titleWithoutExt;
|
|
} else {
|
|
document.getElementById('video-title').value = titleWithoutExt;
|
|
}
|
|
document.getElementById('filename').textContent = file.name;
|
|
document.getElementById('filesize').textContent = formatFileSize(file.size);
|
|
document.getElementById('dropzone-default').style.display = 'none';
|
|
document.getElementById('file-info').classList.add('active');
|
|
const cFname = document.getElementById('ltc-track1-fname');
|
|
if (cFname) cFname.textContent = file.name;
|
|
}
|
|
|
|
function removeVideo(e) {
|
|
e.preventDefault(); e.stopPropagation();
|
|
fileInput.value = ''; currentFile = null;
|
|
document.getElementById('dropzone-default').style.display = 'block';
|
|
document.getElementById('file-info').classList.remove('active');
|
|
const cFname = document.getElementById('ltc-track1-fname');
|
|
if (cFname) cFname.textContent = 'Choose audio file…';
|
|
document.getElementById('video-title').value = '';
|
|
document.getElementById('lt-track1-title-create').value = '';
|
|
['video-description','lt-track1-desc-create'].forEach(function(id){
|
|
const el = document.getElementById(id);
|
|
if (el) { el.value = ''; el.dispatchEvent(new Event('rte:refresh')); }
|
|
});
|
|
setAudioMode(false);
|
|
}
|
|
|
|
// ── Thumbnail / Cover Art ─────────────────────────────────
|
|
let _thumbClickTs = 0;
|
|
thumbnailInput.addEventListener('change', function() { _thumbClickTs = 0; handleThumbnailSelect(this); });
|
|
thumbnailDropzone.addEventListener('click', function(e) {
|
|
if (e.target.closest('.btn-remove')) return;
|
|
const now = Date.now();
|
|
if (now - _thumbClickTs < 800) return;
|
|
_thumbClickTs = now;
|
|
if (typeof window.openCropperModal_thumb_create_mobile === 'function') {
|
|
window.openCropperModal_thumb_create_mobile();
|
|
const internal = document.getElementById('tcInput_thumb_create_mobile');
|
|
if (internal) internal.click();
|
|
} else {
|
|
thumbnailInput.click();
|
|
}
|
|
});
|
|
thumbnailDropzone.addEventListener('dragover', e => { e.preventDefault(); thumbnailDropzone.classList.add('dragover'); });
|
|
thumbnailDropzone.addEventListener('dragleave', () => thumbnailDropzone.classList.remove('dragover'));
|
|
thumbnailDropzone.addEventListener('drop', e => {
|
|
e.preventDefault(); thumbnailDropzone.classList.remove('dragover');
|
|
if (e.dataTransfer.files.length) {
|
|
const droppedFile = e.dataTransfer.files[0];
|
|
if (typeof window.tcPreload_thumb_create_mobile === 'function') {
|
|
window.tcPreload_thumb_create_mobile(droppedFile);
|
|
window.openCropperModal_thumb_create_mobile();
|
|
} else {
|
|
thumbnailInput.files = e.dataTransfer.files;
|
|
handleThumbnailSelect(thumbnailInput);
|
|
}
|
|
}
|
|
});
|
|
|
|
function handleThumbnailSelect(input) {
|
|
if (!input.files || !input.files[0]) return;
|
|
const file = input.files[0];
|
|
document.getElementById('thumbnail-filename').textContent = file.name;
|
|
document.getElementById('thumbnail-filesize').textContent = formatFileSize(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').style.display = 'none';
|
|
document.getElementById('thumbnail-info').classList.add('active');
|
|
}
|
|
|
|
function removeThumbnail(e) {
|
|
e.preventDefault(); e.stopPropagation();
|
|
const fresh = thumbnailInput.cloneNode(false);
|
|
thumbnailInput.parentNode.replaceChild(fresh, thumbnailInput);
|
|
thumbnailInput = fresh;
|
|
thumbnailInput.addEventListener('change', function() { handleThumbnailSelect(this); });
|
|
document.getElementById('thumbnail-default').style.display = 'block';
|
|
document.getElementById('thumbnail-info').classList.remove('active');
|
|
}
|
|
|
|
// ── Per-track slides management (create page) ────────────
|
|
const _cSlidesData = {}; // trackId → File[]
|
|
let _cSlidesDragSrc = null;
|
|
|
|
function cSlidesZoneClick(e, trackId) {
|
|
if (e.target.closest('button')) return;
|
|
document.getElementById('slides-input-' + trackId)?.click();
|
|
}
|
|
function cSlidesZoneDragover(e, trackId) {
|
|
if (_cSlidesDragSrc) { e.preventDefault(); return; }
|
|
if (Array.from(e.dataTransfer.types || []).includes('Files')) {
|
|
e.preventDefault();
|
|
document.getElementById('slides-zone-' + trackId)?.classList.add('dragover');
|
|
}
|
|
}
|
|
function cSlidesZoneDragleave(e, trackId) {
|
|
document.getElementById('slides-zone-' + trackId)?.classList.remove('dragover');
|
|
}
|
|
function cSlidesZoneDrop(e, trackId) {
|
|
e.preventDefault();
|
|
document.getElementById('slides-zone-' + trackId)?.classList.remove('dragover');
|
|
if (_cSlidesDragSrc) return;
|
|
if (e.dataTransfer.files.length) handleCSlidesForTrack(trackId, e.dataTransfer.files);
|
|
}
|
|
|
|
function handleCSlidesForTrack(trackId, fileList) {
|
|
_cSlidesCropStart(fileList, trackId);
|
|
}
|
|
|
|
// ── Slides crop queue: every added image is cropped before it enters the strip ──
|
|
let _cSlidesCropQueue = [];
|
|
let _cSlidesCropTid = null;
|
|
|
|
function _cSlidesCropStart(fileList, trackId) {
|
|
const imgs = Array.from(fileList || []).filter(f => f.type && f.type.startsWith('image/'));
|
|
if (!imgs.length) return;
|
|
_cSlidesCropTid = trackId;
|
|
_cSlidesCropQueue = imgs;
|
|
_cSlidesCropLoadNext();
|
|
}
|
|
|
|
function _cSlidesCropLoadNext() {
|
|
if (!_cSlidesCropQueue.length) { window.closeCropperModal('slides_create_mobile'); return; }
|
|
const f = _cSlidesCropQueue.shift();
|
|
if (typeof window.tcPreload_slides_create_mobile === 'function') {
|
|
window.tcPreload_slides_create_mobile(f);
|
|
window.openCropperModal_slides_create_mobile();
|
|
}
|
|
}
|
|
|
|
function cSlidesCropDone(file) {
|
|
const trackId = _cSlidesCropTid;
|
|
if (trackId && file) {
|
|
if (!_cSlidesData[trackId]) _cSlidesData[trackId] = [];
|
|
if (_cSlidesData[trackId].length < 10) {
|
|
_cSlidesData[trackId].push(file);
|
|
renderCSlidesStrip(trackId);
|
|
}
|
|
}
|
|
_cSlidesCropLoadNext();
|
|
}
|
|
window.cSlidesCropDone = cSlidesCropDone;
|
|
|
|
function renderCSlidesStrip(trackId) {
|
|
const files = _cSlidesData[trackId] || [];
|
|
const strip = document.getElementById('slides-strip-' + trackId);
|
|
const ph = document.getElementById('slides-ph-' + trackId);
|
|
const prev = document.getElementById('slides-preview-' + trackId);
|
|
const cnt = document.getElementById('slides-count-' + trackId);
|
|
const zone = document.getElementById('slides-zone-' + trackId);
|
|
if (!strip) return;
|
|
strip.innerHTML = '';
|
|
if (!files.length) {
|
|
if (ph) ph.style.display = ''; if (prev) prev.style.display = 'none';
|
|
if (zone) zone.style.borderColor = ''; return;
|
|
}
|
|
files.forEach((f, index) => {
|
|
const wrap = document.createElement('div');
|
|
wrap.style.cssText = 'position:relative;flex-shrink:0;cursor:grab;';
|
|
wrap.draggable = true; wrap.dataset.index = index; wrap.dataset.trackId = trackId;
|
|
wrap.addEventListener('dragstart', function(ev) {
|
|
_cSlidesDragSrc = { trackId, index: parseInt(this.dataset.index) };
|
|
ev.dataTransfer.effectAllowed = 'move';
|
|
setTimeout(() => { this.style.opacity = '0.4'; }, 0);
|
|
});
|
|
wrap.addEventListener('dragend', function() { _cSlidesDragSrc = null; this.style.opacity = ''; });
|
|
wrap.addEventListener('dragover', function(ev) {
|
|
if (!_cSlidesDragSrc || _cSlidesDragSrc.trackId !== trackId) return;
|
|
ev.preventDefault(); this.style.outline = '2px solid var(--brand-red)';
|
|
});
|
|
wrap.addEventListener('dragleave', function() { this.style.outline = ''; });
|
|
wrap.addEventListener('drop', function(ev) {
|
|
ev.preventDefault(); ev.stopPropagation(); this.style.outline = '';
|
|
if (!_cSlidesDragSrc || _cSlidesDragSrc.trackId !== trackId) return;
|
|
const from = _cSlidesDragSrc.index, to = parseInt(this.dataset.index);
|
|
if (from === to) return;
|
|
const arr = _cSlidesData[trackId];
|
|
const [moved] = arr.splice(from, 1); arr.splice(to, 0, moved);
|
|
renderCSlidesStrip(trackId);
|
|
});
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.type = 'button';
|
|
removeBtn.style.cssText = 'position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:var(--brand-red);border:none;border-radius:50%;color:#fff;font-size:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;z-index:1;';
|
|
removeBtn.innerHTML = '<i class="bi bi-x"></i>';
|
|
removeBtn.addEventListener('click', function(ev) {
|
|
ev.stopPropagation(); _cSlidesData[trackId].splice(index, 1); renderCSlidesStrip(trackId);
|
|
});
|
|
const img = document.createElement('img');
|
|
img.style.cssText = 'width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.12);display:block;';
|
|
img.alt = '';
|
|
const reader = new FileReader();
|
|
reader.onload = ev2 => { img.src = ev2.target.result; };
|
|
reader.readAsDataURL(f);
|
|
wrap.appendChild(img); wrap.appendChild(removeBtn); strip.appendChild(wrap);
|
|
});
|
|
if (cnt) cnt.textContent = files.length === 1 ? '1 image — static cover' : files.length + ' images — will crossfade';
|
|
if (ph) ph.style.display = 'none'; if (prev) prev.style.display = '';
|
|
if (zone) zone.style.borderColor = '#22c55e';
|
|
}
|
|
|
|
function clearCSlidesForTrack(e, trackId) {
|
|
e.preventDefault(); e.stopPropagation();
|
|
_cSlidesData[trackId] = [];
|
|
const input = document.getElementById('slides-input-' + trackId);
|
|
if (input) {
|
|
const fresh = input.cloneNode(false);
|
|
fresh.addEventListener('change', function() { handleCSlidesForTrack(trackId, this.files); });
|
|
input.parentNode.replaceChild(fresh, input);
|
|
}
|
|
renderCSlidesStrip(trackId);
|
|
}
|
|
|
|
// ── Option selectors ─────────────────────────────────────
|
|
document.querySelectorAll('#type-options .option-item').forEach(item => {
|
|
item.addEventListener('click', function() {
|
|
document.querySelectorAll('#type-options .option-item').forEach(o => {
|
|
o.classList.toggle('active', o.dataset.type === this.dataset.type);
|
|
const inp = o.querySelector('input');
|
|
if (inp) inp.checked = (o.dataset.type === this.dataset.type);
|
|
});
|
|
setAudioMode(this.dataset.type === 'music');
|
|
});
|
|
});
|
|
document.querySelectorAll('#visibility-options .option-item').forEach(item => {
|
|
item.querySelector('input').addEventListener('change', () => {
|
|
document.querySelectorAll('#visibility-options .option-item').forEach(o => o.classList.remove('active'));
|
|
item.classList.add('active');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('#download-options .option-item').forEach(item => {
|
|
item.querySelector('input').addEventListener('change', () => {
|
|
document.querySelectorAll('#download-options .option-item').forEach(o => o.classList.remove('active'));
|
|
item.classList.add('active');
|
|
});
|
|
});
|
|
|
|
// ── Form submission ───────────────────────────────────────
|
|
document.getElementById('upload-form').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
if (!currentFile) { showError('Please select a video or audio file.'); return; }
|
|
|
|
const isAudio = isAudioFile(currentFile);
|
|
const titleEl = isAudio ? document.getElementById('lt-track1-title-create') : document.getElementById('video-title');
|
|
const title = titleEl.value.trim();
|
|
if (!title) { showError('Please enter a title.'); return; }
|
|
|
|
const _isAudioSubmitC = isAudioFile(currentFile);
|
|
if (_isAudioSubmitC) {
|
|
const t1Slides = _cSlidesData['ct1'] || [];
|
|
if (!t1Slides.length) {
|
|
showError('At least one cover image is required for audio tracks.');
|
|
const z = document.getElementById('slides-zone-ct1');
|
|
if (z) z.style.borderColor = 'var(--brand-red)';
|
|
return;
|
|
}
|
|
}
|
|
|
|
const formData = new FormData(this);
|
|
if (_isAudioSubmitC) {
|
|
(_cSlidesData['ct1'] || []).forEach(f => formData.append('slides[]', f));
|
|
// Walk extra-track sections in DOM order so slide index matches
|
|
// the positional `extra_track_files[]` order the backend iterates.
|
|
var extraEls = document.querySelectorAll('#ltac-extra .ltac-item');
|
|
extraEls.forEach(function (el, domIdx) {
|
|
var m = el.id.match(/^ltac-(e\d+)$/);
|
|
if (!m) return;
|
|
var tid = 'c' + m[1];
|
|
var files = _cSlidesData[tid] || [];
|
|
files.forEach(function (f) {
|
|
formData.append('extra_track_slides[' + domIdx + '][]', f);
|
|
});
|
|
});
|
|
}
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.timeout = 0; // no timeout — unlimited file size
|
|
|
|
_pageUploadInProgress = true;
|
|
document.getElementById('progress-container').classList.add('active');
|
|
document.getElementById('submit-btn').disabled = true;
|
|
document.getElementById('submit-btn').innerHTML = '<i class="bi bi-arrow-repeat"></i> Uploading...';
|
|
document.getElementById('status-message').className = '';
|
|
|
|
xhr.upload.addEventListener('progress', function(e) {
|
|
if (e.lengthComputable) {
|
|
const pct = Math.round((e.loaded / e.total) * 100);
|
|
document.getElementById('progress-bar').style.width = pct + '%';
|
|
document.getElementById('progress-text').textContent = 'Uploading... ' + pct + '%';
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('load', function() {
|
|
document.getElementById('progress-bar').style.width = '100%';
|
|
document.getElementById('progress-text').textContent = 'Processing...';
|
|
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try {
|
|
const res = JSON.parse(xhr.responseText);
|
|
if (res.success) {
|
|
document.getElementById('progress-container').classList.remove('active');
|
|
const sm = document.getElementById('status-message');
|
|
sm.innerHTML = '<i class="bi bi-check-circle-fill"></i> Upload successful! Taking you to your video…';
|
|
sm.className = 'success';
|
|
document.getElementById('submit-btn').innerHTML = '<i class="bi bi-check-lg"></i> <span>Uploaded!</span>';
|
|
setTimeout(() => { window.location.href = res.redirect; }, 1800);
|
|
} else {
|
|
showError(res.message || 'Upload failed.');
|
|
}
|
|
} catch(err) { showError('Invalid response from server.'); }
|
|
} else {
|
|
try {
|
|
const res = JSON.parse(xhr.responseText);
|
|
if (res.errors) {
|
|
const msgs = Object.entries(res.errors).map(([f, errs]) => `<b>${f}:</b> ${errs.join(', ')}`).join('<br>');
|
|
showError(msgs);
|
|
} else {
|
|
showError(res.message || `Server error (${xhr.status})`);
|
|
}
|
|
} catch(err) { showError(`Upload failed (${xhr.status}).`); }
|
|
}
|
|
});
|
|
|
|
xhr.addEventListener('error', () => showError('Upload failed. Check your connection.'));
|
|
xhr.addEventListener('timeout', () => showError('Upload timed out.'));
|
|
|
|
xhr.open('POST', '{{ route("videos.store") }}');
|
|
xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}');
|
|
xhr.send(formData);
|
|
});
|
|
|
|
function showError(message) {
|
|
_pageUploadInProgress = false;
|
|
const status = document.getElementById('status-message');
|
|
status.innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + message;
|
|
status.className = 'error';
|
|
document.getElementById('submit-btn').disabled = false;
|
|
const isAudio = document.querySelector('#type-options .option-item[data-type="music"]')?.classList.contains('active');
|
|
document.getElementById('submit-btn').innerHTML =
|
|
'<i class="bi bi-cloud-arrow-up-fill"></i> <span>' + (isAudio ? 'Upload Track' : 'Upload Video') + '</span>';
|
|
document.getElementById('progress-container').classList.remove('active');
|
|
}
|
|
|
|
// ── Language track accordion (create page) ───────────────
|
|
const LANG_OPTIONS_CREATE = @json(\App\Data\Languages::forLanguage());
|
|
let _ltCreateExtraCounter = 0;
|
|
|
|
function _ltcBuildOptsHtml() {
|
|
return LANG_OPTIONS_CREATE.map(o =>
|
|
`<li class="csd-opt" role="option" tabindex="-1" data-v="${o.value}" data-s="${o.search}" aria-selected="false">
|
|
<span class="csd-opt-ico"><span class="fi fi-${o.flag} lsd-flag"></span></span>
|
|
<span class="csd-opt-main">${o.label}</span>
|
|
<span class="csd-opt-sub">${o.native}</span>
|
|
</li>`
|
|
).join('');
|
|
}
|
|
|
|
function ltToggleCreate(trackId) {
|
|
const item = document.getElementById('ltac-' + trackId);
|
|
const arr = document.getElementById('ltac-' + trackId + '-arr');
|
|
if (!item) return;
|
|
const isOpen = item.classList.contains('open');
|
|
if (isOpen) {
|
|
item.classList.remove('open');
|
|
if (arr) { arr.style.transform = ''; arr.style.color = ''; }
|
|
} else {
|
|
item.classList.add('open');
|
|
if (arr) { arr.style.transform = 'rotate(180deg)'; arr.style.color = 'var(--brand-red)'; }
|
|
}
|
|
}
|
|
|
|
function updateLtacHeader(trackId, langCode) {
|
|
const lang = LANG_OPTIONS_CREATE.find(o => o.value === langCode);
|
|
if (!lang) return;
|
|
const flagEl = document.getElementById('ltac-' + trackId + '-flag');
|
|
const nameEl = document.getElementById('ltac-' + trackId + '-name');
|
|
if (flagEl) flagEl.innerHTML = `<span class="fi fi-${lang.flag} lsd-flag" style="width:22px;height:16px;"></span>`;
|
|
if (nameEl) {
|
|
const prefix = trackId === 't1' ? 'Primary' : 'Track ' + (parseInt(trackId.replace('e', '')) + 1);
|
|
nameEl.textContent = prefix + ' — ' + lang.label;
|
|
}
|
|
}
|
|
|
|
function ltRemoveCreate(e, trackId) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const item = document.getElementById('ltac-' + trackId);
|
|
if (item) item.remove();
|
|
delete _cSlidesData['c' + trackId];
|
|
}
|
|
|
|
function addLangTrackCreate() {
|
|
const n = ++_ltCreateExtraCounter;
|
|
const trackNum = n + 1;
|
|
const uid = 'ltce_' + n;
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'ltac-item open';
|
|
item.id = 'ltac-e' + n;
|
|
|
|
item.innerHTML = `
|
|
<div class="ltac-header" onclick="ltToggleCreate('e${n}')">
|
|
<div class="ltac-hd-left">
|
|
<span class="ltac-num">${trackNum}</span>
|
|
<span class="ltac-flag-cell" id="ltac-e${n}-flag">
|
|
<span class="fi fi-xx lsd-flag" style="width:22px;height:16px;"></span>
|
|
</span>
|
|
<span class="ltac-name" id="ltac-e${n}-name">Track ${trackNum} — Select language</span>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:6px;">
|
|
<button type="button" class="ltac-remove" onclick="ltRemoveCreate(event,'e${n}')" title="Remove track">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
<i class="bi bi-chevron-up ltac-arr" style="color:var(--brand-red);" id="ltac-e${n}-arr"></i>
|
|
</div>
|
|
</div>
|
|
<div class="ltac-body" id="ltac-e${n}-body">
|
|
<div style="margin-bottom:12px;">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;"><i class="bi bi-music-note-beamed"></i> Audio File</label>
|
|
<label class="ltac-file-label"
|
|
onmouseenter="this.style.borderColor='var(--brand-red)'"
|
|
onmouseleave="this.style.borderColor='#333'">
|
|
<i class="bi bi-music-note-beamed" style="color:var(--brand-red);font-size:16px;flex-shrink:0;"></i>
|
|
<span class="ltac-file-name" id="ltac-e${n}-fname">Choose audio file…</span>
|
|
<input type="file" name="extra_track_files[]"
|
|
accept="audio/*,.mp3,.m4a,.aac,.flac,.wav" style="display:none;"
|
|
onchange="document.getElementById('ltac-e${n}-fname').textContent=this.files[0]?.name||'Choose audio file…'">
|
|
</label>
|
|
</div>
|
|
<div style="margin-bottom:12px;">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
|
|
Localized Title <span style="font-weight:400;font-size:12px;color:var(--text-secondary);">optional</span>
|
|
</label>
|
|
<input type="text" name="extra_track_titles[]" class="form-input"
|
|
style="font-size:13px;" placeholder="Title in this language…">
|
|
</div>
|
|
<div style="margin-bottom:12px;">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
|
|
Localized Description <span style="font-weight:400;font-size:12px;color:var(--text-secondary);">optional</span>
|
|
</label>
|
|
<div class="rte-wrap" data-min-height="80px">
|
|
<textarea class="rte-source" name="extra_track_descriptions[]" id="lt-desc-ce${n}"
|
|
placeholder="Description in this language…"></textarea>
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom:12px;">
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
|
|
Cover Slides <span style="font-weight:400;font-size:12px;color:var(--text-secondary);">optional · drag to reorder</span>
|
|
</label>
|
|
<div id="slides-zone-ce${n}" class="ltac-slides-zone"
|
|
onclick="cSlidesZoneClick(event,'ce${n}')"
|
|
ondragover="cSlidesZoneDragover(event,'ce${n}')"
|
|
ondragleave="cSlidesZoneDragleave(event,'ce${n}')"
|
|
ondrop="cSlidesZoneDrop(event,'ce${n}')">
|
|
<input type="file" accept="image/*" multiple style="display:none" id="slides-input-ce${n}"
|
|
onchange="handleCSlidesForTrack('ce${n}',this.files)">
|
|
<div class="ltac-slides-ph" id="slides-ph-ce${n}">
|
|
<i class="bi bi-images" style="font-size:22px;"></i>
|
|
<span>Click or drag to add cover images</span>
|
|
</div>
|
|
<div id="slides-preview-ce${n}" style="display:none;padding:10px 12px;width:100%;box-sizing:border-box;">
|
|
<div id="slides-strip-ce${n}" class="slides-strip" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;"></div>
|
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
<span id="slides-count-ce${n}" style="font-size:12px;color:var(--text-secondary);"></span>
|
|
<button type="button" onclick="clearCSlidesForTrack(event,'ce${n}')"
|
|
style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:0;display:flex;align-items:center;gap:4px;">
|
|
<i class="bi bi-x-lg"></i> Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="form-label" style="font-size:13px;margin-bottom:6px;">Language</label>
|
|
<div class="csd-wrap" id="${uid}">
|
|
<button type="button" class="csd-btn" aria-haspopup="listbox" aria-expanded="false" aria-label="Select language">
|
|
<span class="csd-ico"><span class="fi fi-xx lsd-flag"></span></span>
|
|
<span class="csd-val ph">Select language</span>
|
|
<i class="bi bi-chevron-down csd-arr"></i>
|
|
</button>
|
|
<div class="csd-panel" hidden role="listbox">
|
|
<div class="csd-srch"><i class="bi bi-search"></i><input class="csd-sinput" type="text" placeholder="Search language…" autocomplete="off" spellcheck="false"></div>
|
|
<ul class="csd-list">${_ltcBuildOptsHtml()}</ul>
|
|
<p class="csd-empty" hidden>No results</p>
|
|
</div>
|
|
<input type="hidden" name="extra_track_languages[]" id="csd_v_${uid}">
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
document.getElementById('ltac-extra').appendChild(item);
|
|
_cSlidesData['ce' + n] = [];
|
|
|
|
if (window.CSD) new CSD(uid);
|
|
|
|
const hiddenInput = document.getElementById('csd_v_' + uid);
|
|
if (hiddenInput) {
|
|
hiddenInput.dataset.trackId = 'e' + n;
|
|
hiddenInput.addEventListener('change', function() {
|
|
updateLtacHeader('e' + n, this.value);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Default mode is generic (video): only the video-mode language picker submits primary_language
|
|
;(function() {
|
|
const audioLang = document.getElementById('primary_language_create');
|
|
if (audioLang) audioLang.removeAttribute('name');
|
|
})();
|
|
|
|
// Wire primary language select → update track 1 header
|
|
;(function() {
|
|
const plInput = document.getElementById('primary_language_create');
|
|
if (plInput) {
|
|
plInput.dataset.trackId = 't1';
|
|
plInput.addEventListener('change', function() {
|
|
updateLtacHeader('t1', this.value);
|
|
});
|
|
}
|
|
|
|
// Delegated fallback: catches any language hidden-input change within the tracks section
|
|
const section = document.getElementById('lang-tracks-section-create');
|
|
if (section) {
|
|
section.addEventListener('change', function(e) {
|
|
const tid = e.target.dataset && e.target.dataset.trackId;
|
|
if (tid && e.target.value) updateLtacHeader(tid, e.target.value);
|
|
});
|
|
}
|
|
}());
|
|
</script>
|
|
<x-image-cropper
|
|
id="thumb_create_mobile"
|
|
:width="448"
|
|
:height="252"
|
|
shape="square"
|
|
target-input="thumbnail"
|
|
preview-img="thumbnail-preview-img"
|
|
output-width="1280"
|
|
title="Crop Thumbnail"
|
|
/>
|
|
|
|
<x-image-cropper
|
|
id="slides_create_mobile"
|
|
:width="448"
|
|
:height="252"
|
|
shape="square"
|
|
output-width="1280"
|
|
title="Crop Cover Slide"
|
|
result-callback="cSlidesCropDone"
|
|
/>
|
|
@endsection
|