Compare commits
2 Commits
a4384113c2
...
6aae6f86b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aae6f86b6 | ||
|
|
d9959c4452 |
@ -184,6 +184,7 @@ Stored value: IANA timezone string (e.g. `"Asia/Bahrain"`).
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `resources/views/layouts/partials/upload-modal.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-modal"`); extra track language rows use `LANG_OPTIONS_MODAL` JS constant for inline dynamic CSD |
|
| `resources/views/layouts/partials/upload-modal.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-modal"`); extra track language rows use `LANG_OPTIONS_MODAL` JS constant for inline dynamic CSD |
|
||||||
| `resources/views/videos/create.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-create"`); extra track language rows use `LANG_OPTIONS_CREATE` JS constant for inline dynamic CSD |
|
| `resources/views/videos/create.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-create"`); extra track language rows use `LANG_OPTIONS_CREATE` JS constant for inline dynamic CSD |
|
||||||
|
| `resources/views/videos/create.blade.php` | `primary_language` (`id="video_language_create"`) | Video-mode language field inside `#basic-fields-create` (generic/match). `setAudioMode()` swaps `name="primary_language"` between this and `primary_language_create` so only the active mode's picker submits |
|
||||||
| `resources/views/components/track-editor-form.blade.php` | `$languageName` (prop) | Rendered inside the track editor form; used for primary track in edit-video-modal (prefix `t1`) |
|
| `resources/views/components/track-editor-form.blade.php` | `$languageName` (prop) | Rendered inside the track editor form; used for primary track in edit-video-modal (prefix `t1`) |
|
||||||
| `resources/views/videos/edit.blade.php` | `primary_language` | Inside `@else` (audio only) block; pre-populated with `value="{{ $video->language ?? '' }}"` |
|
| `resources/views/videos/edit.blade.php` | `primary_language` | Inside `@else` (audio only) block; pre-populated with `value="{{ $video->language ?? '' }}"` |
|
||||||
|
|
||||||
|
|||||||
@ -2176,6 +2176,47 @@ class VideoController extends Controller
|
|||||||
return response()->json(['url' => route('share.access', $token)]);
|
return response()->json(['url' => route('share.access', $token)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email a friend a properly-formatted share email, version-aware: the link and the
|
||||||
|
* email's title reflect the language track the sender chose. The URL is built
|
||||||
|
* server-side so outgoing mail can never carry an attacker-supplied link.
|
||||||
|
*/
|
||||||
|
public function shareByEmail(Request $request, Video $video)
|
||||||
|
{
|
||||||
|
if (! $video->isShareable() || ! $video->canView(Auth::user())) {
|
||||||
|
return response()->json(['error' => 'This video cannot be shared.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'email' => 'required|email|max:255',
|
||||||
|
'message' => 'nullable|string|max:500',
|
||||||
|
'track' => 'nullable|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$trackId = (int) ($data['track'] ?? 0);
|
||||||
|
$shareTitle = $video->title;
|
||||||
|
$shareUrl = $video->share_url;
|
||||||
|
if ($trackId && ($track = $video->audioTracks->firstWhere('id', $trackId))) {
|
||||||
|
if (! empty($track->title)) $shareTitle = $track->title;
|
||||||
|
$shareUrl .= (str_contains($shareUrl, '?') ? '&' : '?') . 'track=' . $trackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
\Mail::to($data['email'])->send(new \App\Mail\VideoShared(
|
||||||
|
$video,
|
||||||
|
$shareUrl,
|
||||||
|
Auth::user(),
|
||||||
|
$data['message'] ?? null,
|
||||||
|
$shareTitle,
|
||||||
|
));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Log::error('Share-by-email failed: ' . $e->getMessage(), ['video_id' => $video->id]);
|
||||||
|
return response()->json(['error' => 'Could not send the email right now. Please try again.'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
public function accessShare(Request $request, string $token)
|
public function accessShare(Request $request, string $token)
|
||||||
{
|
{
|
||||||
$share = \DB::table('video_shares')->where('token', $token)->first();
|
$share = \DB::table('video_shares')->where('token', $token)->first();
|
||||||
|
|||||||
46
app/Mail/VideoShared.php
Normal file
46
app/Mail/VideoShared.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Video;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Address;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class VideoShared extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public string $shareTitle;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public Video $video,
|
||||||
|
public string $shareUrl,
|
||||||
|
public User $sender,
|
||||||
|
public ?string $personalMessage = null,
|
||||||
|
?string $shareTitle = null,
|
||||||
|
) {
|
||||||
|
// Version-aware title (e.g. the English track's title) with a primary fallback.
|
||||||
|
$this->shareTitle = $shareTitle ?: $video->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: $this->sender->name . ' shared "' . $this->shareTitle . '" with you on ' . config('app.name'),
|
||||||
|
// Let the recipient reply straight to the friend who sent it.
|
||||||
|
replyTo: $this->sender->email
|
||||||
|
? [new Address($this->sender->email, $this->sender->name)]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(view: 'emails.video-shared');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -168,7 +168,7 @@
|
|||||||
|
|
||||||
@if ($video->isShareable())
|
@if ($video->isShareable())
|
||||||
<button class="action-btn desktop-action"
|
<button class="action-btn desktop-action"
|
||||||
onclick="shareCurrent('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
onclick="shareCurrent('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}', '{{ Auth::check() ? route('videos.shareEmail', $video) : '' }}')">
|
||||||
<i class="bi bi-share"></i><span>Share</span>
|
<i class="bi bi-share"></i><span>Share</span>
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
@ -271,7 +271,7 @@
|
|||||||
|
|
||||||
@if ($video->isShareable())
|
@if ($video->isShareable())
|
||||||
<button class="dropdown-item"
|
<button class="dropdown-item"
|
||||||
onclick="shareCurrent('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
onclick="shareCurrent('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}', '{{ Auth::check() ? route('videos.shareEmail', $video) : '' }}')">
|
||||||
<i class="bi bi-share"></i> Share
|
<i class="bi bi-share"></i> Share
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
@ -516,10 +516,10 @@ if (!window._slideshowDlInit) {
|
|||||||
|
|
||||||
// Share the version the viewer is on: append ?track={id} so the recipient's player
|
// Share the version the viewer is on: append ?track={id} so the recipient's player
|
||||||
// opens directly on that language (window._ytpTrackId; 0 = primary → plain link).
|
// opens directly on that language (window._ytpTrackId; 0 = primary → plain link).
|
||||||
window.shareCurrent = function (baseUrl, title, recordUrl) {
|
window.shareCurrent = function (baseUrl, title, recordUrl, emailUrl) {
|
||||||
var t = window._ytpTrackId || 0;
|
var t = window._ytpTrackId || 0;
|
||||||
var url = t ? baseUrl + (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'track=' + t : baseUrl;
|
var url = t ? baseUrl + (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'track=' + t : baseUrl;
|
||||||
if (typeof window.openShareModal === 'function') window.openShareModal(url, title, recordUrl);
|
if (typeof window.openShareModal === 'function') window.openShareModal(url, title, recordUrl, emailUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.startSlideshowDownload = function (routeKey) {
|
window.startSlideshowDownload = function (routeKey) {
|
||||||
|
|||||||
69
resources/views/emails/video-shared.blade.php
Normal file
69
resources/views/emails/video-shared.blade.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
@php
|
||||||
|
// Type-aware wording — a sport/match or regular video is NOT a song.
|
||||||
|
$isSong = $video->isAudioOnly() || $video->type === 'music';
|
||||||
|
$noun = $isSong ? 'song' : ($video->type === 'match' ? 'match' : 'video');
|
||||||
|
$cta = $isSong ? 'Listen now' : 'Watch now';
|
||||||
|
$icon = $isSong ? '🎵' : '▶'; // 🎵 vs ▶
|
||||||
|
$byLabel = $isSong ? 'Artist' : ($video->type === 'match' ? 'Posted by' : 'Channel');
|
||||||
|
@endphp
|
||||||
|
<x-emails.layout subject="{{ $sender->name }} shared a {{ $noun }} with you">
|
||||||
|
|
||||||
|
{{-- Icon --}}
|
||||||
|
<div style="width:64px;height:64px;border-radius:50%;background:rgba(230,30,30,.12);border:1px solid rgba(230,30,30,.25);margin:0 auto 24px;text-align:center;line-height:64px;font-size:28px;">{!! $icon !!}</div>
|
||||||
|
|
||||||
|
<h1 class="email-title">{{ $sender->name }} shared a {{ $noun }} with you</h1>
|
||||||
|
<p class="email-subtitle">They thought you'd enjoy this on {{ config('app.name') }}.</p>
|
||||||
|
|
||||||
|
@if($personalMessage)
|
||||||
|
{{-- Personal note --}}
|
||||||
|
<div class="email-infobox" style="border-left:3px solid #e61e1e;">
|
||||||
|
<div class="email-infobox-label">Message from {{ $sender->name }}</div>
|
||||||
|
<div style="color:#ddd;font-size:14px;line-height:1.6;white-space:pre-wrap;">{{ $personalMessage }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Cover --}}
|
||||||
|
<div class="email-thumb-wrap">
|
||||||
|
@if($video->thumbnail)
|
||||||
|
<a href="{{ $shareUrl }}">
|
||||||
|
<img src="{{ route('media.thumbnail', $video->thumbnail) }}" alt="{{ $shareTitle }}">
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<div class="email-thumb-placeholder">
|
||||||
|
<span style="font-size:32px;">{!! $icon !!}</span>
|
||||||
|
<span>{{ Str::limit($shareTitle, 40) }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Details --}}
|
||||||
|
<div class="email-infobox">
|
||||||
|
<div class="email-infobox-label">Details</div>
|
||||||
|
<div class="email-inforow">
|
||||||
|
<span class="email-inforow-key">Title</span>
|
||||||
|
<span class="email-inforow-val">{{ $shareTitle }}</span>
|
||||||
|
</div>
|
||||||
|
@if($video->formatted_duration)
|
||||||
|
<div class="email-inforow">
|
||||||
|
<span class="email-inforow-key">Duration</span>
|
||||||
|
<span class="email-inforow-val">{{ $video->formatted_duration }}</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="email-inforow">
|
||||||
|
<span class="email-inforow-key">{{ $byLabel }}</span>
|
||||||
|
<span class="email-inforow-val">{{ $video->user->name ?? config('app.name') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- CTA --}}
|
||||||
|
<div class="email-btn-wrap">
|
||||||
|
<a href="{{ $shareUrl }}" class="email-btn">{!! $isSong ? '▶ ' : '▶ ' !!}{{ $cta }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="email-divider">
|
||||||
|
|
||||||
|
<p class="email-note">
|
||||||
|
{{ $sender->name }} sent you this link from {{ config('app.name') }}. If you weren't expecting it, you can safely ignore this email.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</x-emails.layout>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
@auth
|
@auth
|
||||||
<!-- Create / Upload -->
|
<!-- Create / Upload -->
|
||||||
<button type="button" class="yt-upload-btn d-none d-md-flex"
|
<button type="button" class="yt-upload-btn d-none d-md-flex"
|
||||||
data-bs-toggle="modal" data-bs-target="#uploadModal">
|
onclick="openUploadChooser()">
|
||||||
<i class="bi bi-camera-video-fill"></i>
|
<i class="bi bi-camera-video-fill"></i>
|
||||||
<span>Create</span>
|
<span>Create</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -42,8 +42,27 @@
|
|||||||
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #0088cc; color: white; text-decoration: none;">
|
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #0088cc; color: white; text-decoration: none;">
|
||||||
<i class="bi bi-telegram"></i>
|
<i class="bi bi-telegram"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#" id="shareEmailBtn" class="social-share-btn" role="button"
|
||||||
|
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #6b7280; color: white; text-decoration: none;">
|
||||||
|
<i class="bi bi-envelope-fill"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Send by email --}}
|
||||||
|
<div id="shareEmailSection" style="display:none; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border-color);">
|
||||||
|
<p style="color: var(--text-secondary); font-size: 13px; margin-bottom: 12px;">
|
||||||
|
<i class="bi bi-envelope me-1"></i> Send to a friend by email:
|
||||||
|
</p>
|
||||||
|
<input type="email" id="shareEmailTo" class="form-control" placeholder="friend@example.com" autocomplete="off"
|
||||||
|
style="background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 10px 14px; border-radius: 8px;">
|
||||||
|
<textarea id="shareEmailMsg" class="form-control" rows="2" maxlength="500" placeholder="Add a short message (optional)"
|
||||||
|
style="background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 10px 14px; border-radius: 8px; margin-top: 8px; resize: vertical;"></textarea>
|
||||||
|
<button type="button" id="shareEmailSend" class="action-btn action-btn-primary" style="width:100%; justify-content:center; margin-top:10px;">
|
||||||
|
<i class="bi bi-send"></i> <span>Send email</span>
|
||||||
|
</button>
|
||||||
|
<div id="shareEmailStatus" style="display:none; margin-top:10px; font-size:13px; text-align:center;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,7 +89,11 @@ function _getLatestCsrf() {
|
|||||||
return (typeof csrf !== 'undefined') ? csrf : '';
|
return (typeof csrf !== 'undefined') ? csrf : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openShareModal(videoUrl, videoTitle, recordUrl) {
|
// Set per-open so the "Send by email" form knows the endpoint + which version to send.
|
||||||
|
var _shareEmailUrl = '';
|
||||||
|
var _shareTrack = '';
|
||||||
|
|
||||||
|
async function openShareModal(videoUrl, videoTitle, recordUrl, emailUrl) {
|
||||||
var csrfToken = _getLatestCsrf();
|
var csrfToken = _getLatestCsrf();
|
||||||
var shareUrl = videoUrl;
|
var shareUrl = videoUrl;
|
||||||
|
|
||||||
@ -78,6 +101,8 @@ async function openShareModal(videoUrl, videoTitle, recordUrl) {
|
|||||||
// share link replaces shareUrl below, so we re-attach it afterwards.
|
// share link replaces shareUrl below, so we re-attach it afterwards.
|
||||||
var trackParam = '';
|
var trackParam = '';
|
||||||
try { trackParam = (new URL(videoUrl, window.location.origin)).searchParams.get('track') || ''; } catch (e) {}
|
try { trackParam = (new URL(videoUrl, window.location.origin)).searchParams.get('track') || ''; } catch (e) {}
|
||||||
|
_shareEmailUrl = emailUrl || '';
|
||||||
|
_shareTrack = trackParam;
|
||||||
|
|
||||||
// Obtain a unique tracked share link from the server
|
// Obtain a unique tracked share link from the server
|
||||||
if (recordUrl) {
|
if (recordUrl) {
|
||||||
@ -130,6 +155,17 @@ function _populateShareModal(shareUrl, videoTitle) {
|
|||||||
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i> <span>Copy</span>';
|
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i> <span>Copy</span>';
|
||||||
copyBtn.classList.remove('copied');
|
copyBtn.classList.remove('copied');
|
||||||
document.getElementById('copySuccess').style.display = 'none';
|
document.getElementById('copySuccess').style.display = 'none';
|
||||||
|
|
||||||
|
// Reset the email form; the envelope button only works when an endpoint was provided.
|
||||||
|
var emailBtn = document.getElementById('shareEmailBtn');
|
||||||
|
var emailSec = document.getElementById('shareEmailSection');
|
||||||
|
if (emailBtn) emailBtn.style.display = _shareEmailUrl ? 'flex' : 'none';
|
||||||
|
if (emailSec) {
|
||||||
|
emailSec.style.display = 'none';
|
||||||
|
var to = document.getElementById('shareEmailTo'); if (to) to.value = '';
|
||||||
|
var msg = document.getElementById('shareEmailMsg'); if (msg) msg.value = '';
|
||||||
|
var st = document.getElementById('shareEmailStatus'); if (st) st.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _copyToClipboard(text) {
|
function _copyToClipboard(text) {
|
||||||
@ -174,6 +210,82 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
showToast('Could not copy — please copy the link manually.', 'error');
|
showToast('Could not copy — please copy the link manually.', 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Send by email ──────────────────────────────────────────────
|
||||||
|
var emailBtn = document.getElementById('shareEmailBtn');
|
||||||
|
var emailSec = document.getElementById('shareEmailSection');
|
||||||
|
var emailTo = document.getElementById('shareEmailTo');
|
||||||
|
var emailMsg = document.getElementById('shareEmailMsg');
|
||||||
|
var emailSend = document.getElementById('shareEmailSend');
|
||||||
|
var emailStat = document.getElementById('shareEmailStatus');
|
||||||
|
|
||||||
|
if (emailBtn && emailSec) {
|
||||||
|
emailBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
emailSec.style.display = (emailSec.style.display === 'none' || !emailSec.style.display) ? 'block' : 'none';
|
||||||
|
if (emailSec.style.display === 'block' && emailTo) emailTo.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showEmailStatus(msg, color) {
|
||||||
|
if (!emailStat) return;
|
||||||
|
emailStat.textContent = msg;
|
||||||
|
emailStat.style.color = color;
|
||||||
|
emailStat.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailSend) {
|
||||||
|
emailSend.addEventListener('click', function() {
|
||||||
|
var to = (emailTo && emailTo.value || '').trim();
|
||||||
|
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(to)) {
|
||||||
|
_showEmailStatus('Please enter a valid email address.', '#ef4444');
|
||||||
|
if (emailTo) emailTo.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_shareEmailUrl) { _showEmailStatus('Email sharing is unavailable here.', '#ef4444'); return; }
|
||||||
|
|
||||||
|
emailSend.disabled = true;
|
||||||
|
var _orig = emailSend.innerHTML;
|
||||||
|
emailSend.innerHTML = '<i class="bi bi-hourglass-split"></i> <span>Sending…</span>';
|
||||||
|
_showEmailStatus('Sending…', 'var(--text-secondary)');
|
||||||
|
|
||||||
|
var body = new URLSearchParams({
|
||||||
|
_token: '{{ csrf_token() }}',
|
||||||
|
email: to,
|
||||||
|
message: (emailMsg && emailMsg.value || ''),
|
||||||
|
track: _shareTrack || '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(_shareEmailUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': _getLatestCsrf(),
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
},
|
||||||
|
body: body.toString(),
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json().then(function(d){ return { ok: r.ok, d: d }; }); })
|
||||||
|
.then(function(res) {
|
||||||
|
emailSend.disabled = false;
|
||||||
|
emailSend.innerHTML = _orig;
|
||||||
|
if (res.ok && res.d.success) {
|
||||||
|
_showEmailStatus('Sent! Your friend will get the email shortly.', '#4caf50');
|
||||||
|
if (emailTo) emailTo.value = '';
|
||||||
|
if (emailMsg) emailMsg.value = '';
|
||||||
|
setTimeout(function(){ if (emailSec) emailSec.style.display = 'none'; }, 1800);
|
||||||
|
} else {
|
||||||
|
_showEmailStatus((res.d && res.d.error) || 'Could not send the email. Please try again.', '#ef4444');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
emailSend.disabled = false;
|
||||||
|
emailSend.innerHTML = _orig;
|
||||||
|
_showEmailStatus('Network error — please try again.', '#ef4444');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,41 @@
|
|||||||
@php use App\Data\Languages; @endphp
|
@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="chooseUploadType('match')">
|
||||||
|
<span class="utc-card-ico"><i class="bi bi-trophy"></i></span>
|
||||||
|
<span class="utc-card-title">Sports</span>
|
||||||
|
<span class="utc-card-desc">Matches with rounds & annotations</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 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">
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable um-dialog">
|
||||||
|
|
||||||
@ -47,7 +84,7 @@
|
|||||||
|
|
||||||
{{-- ── Global Settings ── --}}
|
{{-- ── Global Settings ── --}}
|
||||||
<div class="um-gs-row">
|
<div class="um-gs-row">
|
||||||
<div class="um-gs-wrap">
|
<div class="um-gs-wrap" id="gs-type-wrap">
|
||||||
<span class="um-gs-lbl">Content Type</span>
|
<span class="um-gs-lbl">Content Type</span>
|
||||||
<button type="button" class="um-gs-btn" id="gs-type-btn">
|
<button type="button" class="um-gs-btn" id="gs-type-btn">
|
||||||
<i class="bi bi-film um-gs-ico" id="gs-type-ico"></i>
|
<i class="bi bi-film um-gs-ico" id="gs-type-ico"></i>
|
||||||
@ -92,7 +129,7 @@
|
|||||||
<div class="um-rule"></div>
|
<div class="um-rule"></div>
|
||||||
|
|
||||||
{{-- ── Track Cards Section ── --}}
|
{{-- ── Track Cards Section ── --}}
|
||||||
<div class="um-tracks-header">
|
<div class="um-tracks-header" id="um-tracks-header">
|
||||||
<div>
|
<div>
|
||||||
<span class="um-tracks-title" id="um-tracks-section-label">Language Tracks</span>
|
<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>
|
<span class="um-tracks-sub" id="um-tracks-section-sub">Add audio tracks in different languages</span>
|
||||||
@ -107,7 +144,7 @@
|
|||||||
<div id="um-tc-t1">
|
<div id="um-tc-t1">
|
||||||
{{-- Empty state: just the Add button --}}
|
{{-- 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;">
|
<button type="button" class="action-btn action-btn-primary" id="um-tc-t1-add-btn" onclick="openTrackPopup('t1')" style="width:100%;justify-content:center;padding:16px;font-size:14px;border-radius:12px;">
|
||||||
<i class="bi bi-plus-circle"></i> <span>Add Language Track</span>
|
<i class="bi bi-plus-circle"></i> <span>Add Video Details</span>
|
||||||
</button>
|
</button>
|
||||||
{{-- Filled state: track card (hidden until lang or title is set) --}}
|
{{-- 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-track-card" id="um-tc-t1-card" style="display:none;">
|
||||||
@ -131,6 +168,9 @@
|
|||||||
<div id="um-tc-extra"></div>
|
<div id="um-tc-extra"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Inline host for the track-1 form (generic/match show fields here, no popup) --}}
|
||||||
|
<div id="um-t1-inline-host"></div>
|
||||||
|
|
||||||
{{-- Progress --}}
|
{{-- Progress --}}
|
||||||
<div id="progress-container-modal" class="um-prog">
|
<div id="progress-container-modal" class="um-prog">
|
||||||
<div class="um-prog-track">
|
<div class="um-prog-track">
|
||||||
@ -159,7 +199,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close btn-close-white" onclick="closeTrackPopup()" aria-label="Close"></button>
|
<button type="button" class="btn-close btn-close-white" onclick="closeTrackPopup()" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="um-tp-body">
|
<div class="um-tp-body" id="um-tp-body">
|
||||||
|
|
||||||
{{-- Track 1 form (Blade-rendered) --}}
|
{{-- Track 1 form (Blade-rendered) --}}
|
||||||
<div class="um-track-form" id="um-tf-t1" style="display:none;">
|
<div class="um-track-form" id="um-tf-t1" style="display:none;">
|
||||||
@ -181,7 +221,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">Title <span class="um-req">*</span></label>
|
<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"
|
<input type="text" id="lt-track1-title-modal" class="um-input"
|
||||||
style="font-size:13px;padding:9px 12px;"
|
|
||||||
placeholder="Track title…" autocomplete="off">
|
placeholder="Track title…" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -605,6 +644,174 @@
|
|||||||
/* ── Submit ──────────────────────────────────────────────── */
|
/* ── 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 { 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; }
|
.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>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -614,6 +821,47 @@ let _fileSelected = false;
|
|||||||
let _umExtraCount = 0;
|
let _umExtraCount = 0;
|
||||||
let _currentMode = 'generic'; // 'generic', 'music', 'match'
|
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 ────────────────────────────────────────
|
// ── Modal open / close ────────────────────────────────────────
|
||||||
function openUploadModal() {
|
function openUploadModal() {
|
||||||
if (window.innerWidth < 992) {
|
if (window.innerWidth < 992) {
|
||||||
@ -747,18 +995,38 @@ function _applyMode(type) {
|
|||||||
// Show/hide "Add Language Track" button
|
// Show/hide "Add Language Track" button
|
||||||
document.getElementById('um-add-track-btn').style.display = isMusic ? '' : 'none';
|
document.getElementById('um-add-track-btn').style.display = isMusic ? '' : 'none';
|
||||||
|
|
||||||
// Update tracks section label
|
// 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 lbl = document.getElementById('um-tracks-section-label');
|
||||||
const sub = document.getElementById('um-tracks-section-sub');
|
const sub = document.getElementById('um-tracks-section-sub');
|
||||||
if (isMusic) {
|
if (isMusic) {
|
||||||
if (lbl) lbl.textContent = 'Language Tracks';
|
if (lbl) lbl.textContent = 'Language Tracks';
|
||||||
if (sub) sub.textContent = 'Add audio tracks in different languages';
|
if (sub) sub.textContent = 'Add audio tracks in different languages';
|
||||||
} else {
|
|
||||||
if (lbl) lbl.textContent = type === 'match' ? 'Match Video' : 'Video Details';
|
|
||||||
if (sub) sub.textContent = 'Click Edit on the track below to add your file and details';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide fields in popup form (language always visible)
|
// 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 videoZone = document.getElementById('um-tf-t1-video-zone');
|
||||||
const thumbWrap = document.getElementById('um-tf-t1-thumb-wrap');
|
const thumbWrap = document.getElementById('um-tf-t1-thumb-wrap');
|
||||||
const musicPair = document.getElementById('um-tf-t1-music-pair');
|
const musicPair = document.getElementById('um-tf-t1-music-pair');
|
||||||
@ -790,6 +1058,35 @@ function _applyMode(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('type_modal').value = type;
|
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 ───────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────
|
||||||
@ -1108,6 +1405,12 @@ function clearSlidesForTrack(e, tid) {
|
|||||||
|
|
||||||
// ── Track popup ───────────────────────────────────────────────
|
// ── Track popup ───────────────────────────────────────────────
|
||||||
function openTrackPopup(trackId) {
|
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');
|
document.querySelectorAll('.um-track-form').forEach(f => f.style.display = 'none');
|
||||||
const form = document.getElementById('um-tf-' + trackId);
|
const form = document.getElementById('um-tf-' + trackId);
|
||||||
if (form) form.style.display = '';
|
if (form) form.style.display = '';
|
||||||
|
|||||||
@ -1386,7 +1386,7 @@ $headerSocialMap = [
|
|||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
<span>Edit channel</span>
|
<span>Edit channel</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="ch-btn-ghost" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
<button class="ch-btn-ghost" onclick="openUploadChooser()">
|
||||||
<i class="bi bi-camera-video"></i>
|
<i class="bi bi-camera-video"></i>
|
||||||
<span>Upload</span>
|
<span>Upload</span>
|
||||||
</button>
|
</button>
|
||||||
@ -1793,7 +1793,7 @@ $headerSocialMap = [
|
|||||||
<p>This channel hasn't uploaded any videos.</p>
|
<p>This channel hasn't uploaded any videos.</p>
|
||||||
@auth
|
@auth
|
||||||
@if(Auth::id() === $user->id)
|
@if(Auth::id() === $user->id)
|
||||||
<button class="ch-btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
<button class="ch-btn-primary" onclick="openUploadChooser()">
|
||||||
<i class="bi bi-cloud-upload"></i> Upload your first video
|
<i class="bi bi-cloud-upload"></i> Upload your first video
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@ -190,6 +190,14 @@
|
|||||||
|
|
||||||
<!-- Title + Description (video mode) -->
|
<!-- Title + Description (video mode) -->
|
||||||
<div id="basic-fields-create">
|
<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">
|
<div class="form-group">
|
||||||
<label class="form-label">Title *</label>
|
<label class="form-label">Title *</label>
|
||||||
<input type="text" name="title" id="video-title" class="form-input" placeholder="Enter video title">
|
<input type="text" name="title" id="video-title" class="form-input" placeholder="Enter video title">
|
||||||
@ -496,6 +504,18 @@
|
|||||||
innerTitle.removeAttribute('name'); innerDesc.removeAttribute('name');
|
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) {
|
if (audio) {
|
||||||
document.querySelectorAll('#type-options .option-item').forEach(o => o.classList.remove('active'));
|
document.querySelectorAll('#type-options .option-item').forEach(o => o.classList.remove('active'));
|
||||||
const musicOpt = document.querySelector('#type-options .option-item[data-type="music"]');
|
const musicOpt = document.querySelector('#type-options .option-item[data-type="music"]');
|
||||||
@ -1038,6 +1058,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Wire primary language select → update track 1 header
|
||||||
;(function() {
|
;(function() {
|
||||||
const plInput = document.getElementById('primary_language_create');
|
const plInput = document.getElementById('primary_language_create');
|
||||||
|
|||||||
@ -48,6 +48,7 @@ Route::get('/videos/{video}/slideshow/progress', [VideoController::class, 'slide
|
|||||||
Route::get('/videos/{video}/recommendations', [VideoController::class, 'recommendations'])->name('videos.recommendations');
|
Route::get('/videos/{video}/recommendations', [VideoController::class, 'recommendations'])->name('videos.recommendations');
|
||||||
Route::get('/videos/{video}/player-data', [VideoController::class, 'playerData'])->name('videos.playerData');
|
Route::get('/videos/{video}/player-data', [VideoController::class, 'playerData'])->name('videos.playerData');
|
||||||
Route::post('/videos/{video}/share', [VideoController::class, 'recordShare'])->name('videos.recordShare');
|
Route::post('/videos/{video}/share', [VideoController::class, 'recordShare'])->name('videos.recordShare');
|
||||||
|
Route::post('/videos/{video}/share/email', [VideoController::class, 'shareByEmail'])->name('videos.shareEmail')->middleware(['auth', 'throttle:10,1']);
|
||||||
Route::get('/videos/{video}/og-image', [VideoController::class, 'ogImage'])->name('videos.ogImage');
|
Route::get('/videos/{video}/og-image', [VideoController::class, 'ogImage'])->name('videos.ogImage');
|
||||||
Route::get('/s/{token}', [VideoController::class, 'accessShare'])->name('share.access');
|
Route::get('/s/{token}', [VideoController::class, 'accessShare'])->name('share.access');
|
||||||
Route::get('/videos/{video}/insights', [VideoController::class, 'insights'])->name('videos.insights')->middleware('auth');
|
Route::get('/videos/{video}/insights', [VideoController::class, 'insights'])->name('videos.insights')->middleware('auth');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user