ghassan 0b2e95ea65 Add NAS file manager integration and all pending platform changes
- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
  settings panel, country/phone/timezone components, posts, slideshow,
  playlist shares, video downloads/shares, comment likes, notifications,
  social links, and more

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:24:32 +03:00

1132 lines
40 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Upload Modal - Single Page Form (like create.blade.php) -->
<div class="modal fade" id="uploadModal" tabindex="-1" aria-labelledby="uploadModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content upload-modal-content">
<!-- Header -->
<div class="modal-header upload-modal-header">
<div class="d-flex align-items-center gap-3">
<div class="upload-icon-wrapper">
<i class="bi bi-cloud-arrow-up-fill"></i>
</div>
<div>
<h5 class="modal-title" id="uploadModalLabel">Upload Video</h5>
<span class="upload-subtitle">Share your creativity</span>
</div>
</div>
<button type="button" class="btn-close btn-close-white" onclick="closeUploadModal()" aria-label="Close"></button>
</div>
<!-- Body -->
<div class="modal-body upload-modal-body">
<!-- Form - Single Page (no steps) -->
<form id="upload-form-modal" enctype="multipart/form-data">
@csrf
<!-- Video / Audio File -->
<div class="form-group">
<label class="form-label" id="file-label-modal">
<i class="bi bi-camera-video" id="file-label-icon"></i>
<span id="file-label-text">Video or Music File *</span>
</label>
<div id="video-dropzone-modal" class="dropzone-modal">
<input type="file" name="video" id="video-modal" accept="video/*,audio/mpeg,audio/mp4,audio/aac,audio/flac,audio/wav,.mp3,.m4a,.aac,.flac,.wav" required>
<div id="dropzone-default-modal">
<div class="dropzone-icon">
<i class="bi bi-cloud-arrow-up"></i>
</div>
<p class="dropzone-title">Click to select or drag file here</p>
<p class="dropzone-hint">Video: MP4, MOV, AVI, WebM Audio: MP3, M4A, AAC, FLAC</p>
</div>
<div id="file-info-modal" class="file-info-modal">
<div class="file-preview">
<i class="bi bi-film"></i>
</div>
<div class="file-details">
<p class="filename" id="filename-modal"></p>
<p id="filesize-modal"></p>
</div>
<button type="button" class="btn-remove-file" onclick="removeVideoModal(event)">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
<!-- Title -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-card-heading"></i> Title *
</label>
<input type="text" name="title" id="upload-video-title" required
class="form-input"
placeholder="Enter video title">
</div>
<!-- Description -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-text-paragraph"></i> Description
</label>
<textarea name="description" rows="3"
class="form-textarea"
placeholder="Tell viewers about your video (Markdown supported)"></textarea>
</div>
<!-- Thumbnail (video) / Slides (audio) -->
<div class="form-group" id="thumbnail-section-modal">
<label class="form-label" id="thumbnail-label-modal">
<i class="bi bi-image"></i> <span id="thumbnail-label-text">Thumbnail</span>
</label>
<div id="thumbnail-dropzone-modal" class="dropzone-modal thumbnail-dropzone">
<input type="file" name="thumbnail" id="thumbnail-modal" accept="image/*">
<div id="thumbnail-default-modal">
<div class="dropzone-icon thumbnail-icon">
<i class="bi bi-card-image"></i>
</div>
<p class="dropzone-title" id="thumbnail-dropzone-hint">Click to select thumbnail (optional)</p>
</div>
<div id="thumbnail-info-modal" class="file-info-modal thumbnail-info">
<div class="file-preview thumbnail-preview">
<img id="thumbnail-preview-img" src="" alt="Thumbnail preview">
</div>
<div class="file-details">
<p class="filename" id="thumbnail-filename-modal"></p>
<p id="thumbnail-filesize-modal"></p>
</div>
<button type="button" class="btn-remove-file" onclick="removeThumbnailModal(event)">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
<!-- Slides dropzone (audio only) -->
<div class="form-group" id="slides-section-modal" style="display:none;">
<label class="form-label">
<i class="bi bi-images"></i> Slides <span style="color:#f00;">*</span>
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:6px;">Select 110 images multiple images will crossfade</span>
</label>
<div id="slides-dropzone-modal" class="dropzone-modal thumbnail-dropzone" style="min-height:80px;">
<input type="file" name="slides[]" id="slides-modal" accept="image/*" multiple style="display:none;">
<div id="slides-default-modal">
<div class="dropzone-icon thumbnail-icon"><i class="bi bi-images"></i></div>
<p class="dropzone-title">Click or drag to add cover images</p>
</div>
<div id="slides-preview-modal" style="display:none;padding:10px 12px;">
<div id="slides-strip-modal" 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-modal" style="font-size:12px;color:var(--text-secondary);"></span>
<button type="button" onclick="removeSlidesModal(event)" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:0;">
<i class="bi bi-x-lg"></i> Clear
</button>
</div>
</div>
</div>
</div>
<!-- Video Type -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-collection-play"></i> Video Type
</label>
<div class="options-grid" id="type-options-modal">
<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="form-group">
<label class="form-label">
<i class="bi bi-shield-lock"></i> Privacy
</label>
<div class="options-grid" id="visibility-options-modal">
<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 -->
<input type="hidden" name="is_shorts" id="is_shorts_modal" 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-modal">
<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-modal" class="progress-container-modal">
<div class="progress-bar-wrapper">
<div id="progress-bar-modal" class="progress-bar-fill"></div>
</div>
<p id="progress-text-modal" class="progress-text">Uploading... 0%</p>
</div>
<!-- Status Message -->
<div id="status-message-modal" class="status-message-modal"></div>
<!-- Submit Button -->
<button type="submit" id="submit-btn-modal" 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>
</div>
</div>
<style>
/* Modal Base Styles */
.upload-modal-content {
background: #1a1a1a;
border: none;
border-radius: 12px;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
overflow: hidden;
}
/* Modal Animation */
#uploadModal .modal-dialog {
max-width: 640px;
transform: scale(0.9) translateY(20px);
opacity: 0;
transition: all 0.3s ease;
}
#uploadModal.show .modal-dialog {
transform: scale(1) translateY(0);
opacity: 1;
}
/* Header */
.upload-modal-header {
background: linear-gradient(135deg, #e61e1e 0%, #ff4757 100%);
border-bottom: none;
padding: 20px 24px;
position: relative;
overflow: hidden;
}
.upload-modal-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
pointer-events: none;
}
.upload-modal-header .btn-close {
position: relative;
z-index: 1;
}
.upload-icon-wrapper {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
backdrop-filter: blur(10px);
}
.upload-modal-header .modal-title {
font-size: 20px;
font-weight: 600;
color: white;
margin: 0;
}
.upload-subtitle {
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
}
/* Body */
.upload-modal-body {
padding: 24px;
background: #1a1a1a;
}
/* Form Elements */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
color: #e5e5e5;
}
.form-label i {
color: #e61e1e;
margin-right: 6px;
}
.form-input, .form-textarea {
width: 100%;
background: #121212;
border: 1px solid #333;
border-radius: 8px;
padding: 12px 16px;
color: #f1f1f1;
font-size: 14px;
transition: all 0.2s;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: #e61e1e;
box-shadow: 0 0 0 3px rgba(230, 30, 30, 0.15);
}
.form-input::placeholder, .form-textarea::placeholder {
color: #666;
}
/* Dropzone */
.dropzone-modal {
border: 2px dashed #404040;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
background: #151515;
}
.dropzone-modal:hover {
border-color: #e61e1e;
background: rgba(230, 30, 30, 0.05);
}
.dropzone-modal.dragover {
border-color: #e61e1e;
background: rgba(230, 30, 30, 0.1);
}
.dropzone-modal input[type="file"] {
display: none;
}
.dropzone-icon {
font-size: 48px;
color: #666;
margin-bottom: 12px;
}
.dropzone-icon.thumbnail-icon {
font-size: 28px;
}
.dropzone-title {
color: #aaa;
font-size: 14px;
margin: 8px 0;
}
.dropzone-hint {
color: #666;
font-size: 12px;
margin: 0;
}
/* Thumbnail Dropzone - Smaller */
.thumbnail-dropzone {
padding: 20px;
}
.thumbnail-dropzone .dropzone-icon {
font-size: 28px;
margin-bottom: 6px;
}
.thumbnail-dropzone .dropzone-title {
font-size: 13px;
margin: 0;
}
/* File Info */
.file-info-modal {
display: none;
align-items: center;
gap: 12px;
padding: 12px;
background: #1f1f1f;
border-radius: 8px;
border: 1px solid #333;
}
.file-info-modal.active {
display: flex;
}
.file-preview {
width: 48px;
height: 48px;
background: #333;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: white;
flex-shrink: 0;
}
.file-preview.thumbnail-preview {
background: none;
overflow: hidden;
width: 80px;
height: 48px;
}
.file-preview.thumbnail-preview img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
.file-details {
flex: 1;
text-align: left;
}
.file-details .filename {
color: #e5e5e5;
font-weight: 500;
font-size: 13px;
margin: 0 0 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.file-details p {
color: #888;
font-size: 12px;
margin: 0;
}
.btn-remove-file {
width: 28px;
height: 28px;
padding: 0;
border-radius: 50%;
background: #333;
border: none;
color: #888;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.btn-remove-file:hover {
background: #e61e1e;
color: white;
}
/* Options Grid */
.options-grid {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.option-item {
flex: 1;
min-width: 80px;
cursor: pointer;
}
.option-item input {
display: none;
}
.option-content {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: #1a1a1a;
border: 2px solid #333;
border-radius: 8px;
transition: all 0.2s;
}
.option-item:hover .option-content {
border-color: #555;
background: #1f1f1f;
}
.option-item.active .option-content {
border-color: #e61e1e;
background: rgba(230, 30, 30, 0.1);
}
.option-content i {
font-size: 16px;
color: #666;
width: 18px;
text-align: center;
}
.option-item.active .option-content i {
color: #e61e1e;
}
.option-content span {
font-size: 13px;
color: #aaa;
}
.option-item.active .option-content span {
color: #fff;
}
/* Progress Bar */
.progress-container-modal {
display: none;
margin: 20px 0;
}
.progress-container-modal.active {
display: block;
}
.progress-bar-wrapper {
background: #2a2a2a;
border-radius: 4px;
height: 4px;
overflow: hidden;
}
.progress-bar-fill {
background: #e61e1e;
height: 100%;
width: 0%;
transition: width 0.3s;
}
.progress-text {
text-align: center;
color: #888;
font-size: 13px;
margin-top: 8px;
}
/* Status Message */
.status-message-modal {
display: none;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 13px;
}
.status-message-modal.success {
display: block;
background: rgba(34, 197, 94, 0.2);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.3);
}
.status-message-modal.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;
}
/* Responsive */
@media (max-width: 576px) {
.upload-modal-body {
padding: 16px;
}
.options-grid {
flex-direction: column;
}
.option-item {
min-width: 100%;
}
}
</style>
<script>
// Initialize modal functions
function openUploadModal() {
// Check if mobile device - redirect to create page on mobile
if (window.innerWidth < 992) {
window.location.href = '{{ route("videos.create") }}';
return;
}
const modalEl = document.getElementById('uploadModal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
// Add show class for animation
setTimeout(() => {
modalEl.classList.add('show');
}, 10);
}
function closeUploadModal() {
const modalEl = document.getElementById('uploadModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) {
modal.hide();
}
// Reset form after hidden
modalEl.addEventListener('hidden.bs.modal', function() {
resetUploadForm();
modalEl.classList.remove('show');
}, { once: true });
}
function resetUploadForm() {
document.getElementById('upload-form-modal').reset();
// Reset file previews
document.getElementById('dropzone-default-modal').style.display = 'block';
document.getElementById('file-info-modal').classList.remove('active');
document.getElementById('thumbnail-default-modal').style.display = 'block';
document.getElementById('thumbnail-info-modal').classList.remove('active');
// Reset progress
document.getElementById('progress-container-modal').classList.remove('active');
document.getElementById('progress-bar-modal').style.width = '0%';
document.getElementById('status-message-modal').className = 'status-message-modal';
document.getElementById('submit-btn-modal').disabled = false;
document.getElementById('submit-btn-modal').innerHTML = '<i class="bi bi-cloud-arrow-up-fill"></i> Upload Video';
// Reset option items
document.querySelectorAll('#type-options-modal .option-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.type === 'generic') {
item.classList.add('active');
}
});
document.querySelectorAll('#visibility-options-modal .option-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.privacy === 'public') {
item.classList.add('active');
}
});
document.querySelectorAll('#download-options-modal .option-item').forEach(item => {
item.classList.remove('active');
item.querySelector('input').checked = false;
if (item.dataset.download === 'disabled') {
item.classList.add('active');
item.querySelector('input').checked = true;
}
});
}
// Format file size
function formatFileSizeModal(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const 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];
}
// Video Dropzone
const videoDropzoneModal = document.getElementById('video-dropzone-modal');
const videoInputModal = document.getElementById('video-modal');
videoInputModal.addEventListener('change', function() {
handleVideoSelectModal(this);
});
videoDropzoneModal.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
videoInputModal.click();
});
videoDropzoneModal.addEventListener('dragover', (e) => {
e.preventDefault();
videoDropzoneModal.classList.add('dragover');
});
videoDropzoneModal.addEventListener('dragleave', () => {
videoDropzoneModal.classList.remove('dragover');
});
videoDropzoneModal.addEventListener('drop', (e) => {
e.preventDefault();
videoDropzoneModal.classList.remove('dragover');
if (e.dataTransfer.files.length) {
videoInputModal.files = e.dataTransfer.files;
handleVideoSelectModal(videoInputModal);
}
});
const AUDIO_EXTENSIONS = ['mp3', 'm4a', 'aac', 'flac', 'wav'];
function isAudioFile(file) {
const ext = file.name.split('.').pop().toLowerCase();
return AUDIO_EXTENSIONS.includes(ext) || file.type.startsWith('audio/');
}
function setAudioMode(audio) {
// Label
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 *';
// Toggle thumbnail vs slides section
document.getElementById('thumbnail-section-modal').style.display = audio ? 'none' : '';
document.getElementById('slides-section-modal').style.display = audio ? '' : 'none';
document.getElementById('slides-dropzone-modal').style.borderColor = audio ? '#e61e1e' : '';
// Auto-select Music type when audio
if (audio) {
document.querySelectorAll('#type-options-modal .option-item').forEach(o => o.classList.remove('active'));
const musicOption = document.querySelector('#type-options-modal .option-item[data-type="music"]');
if (musicOption) {
musicOption.classList.add('active');
musicOption.querySelector('input').checked = true;
}
}
// Update file preview icon
const previewIcon = document.querySelector('#file-info-modal .file-preview i');
if (previewIcon) previewIcon.className = audio ? 'bi bi-music-note-beamed' : 'bi bi-film';
// Update submit label
document.getElementById('submit-btn-modal').innerHTML =
'<i class="bi bi-cloud-arrow-up-fill"></i> <span>' + (audio ? 'Upload Track' : 'Upload Video') + '</span>';
}
function handleVideoSelectModal(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
const maxSize = 512 * 1024 * 1024; // 512MB
const validVideoTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo', 'video/x-flv', 'video/x-matroska'];
const validVideoExtensions = ['.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) || validVideoExtensions.includes(ext);
if (!audio && !validVideo) {
showToast('Invalid format. Accepted: MP4, MOV, AVI, WebM, MKV, MP3, M4A, AAC, FLAC, WAV.', 'error');
input.value = '';
document.getElementById('dropzone-default-modal').style.display = 'block';
document.getElementById('file-info-modal').classList.remove('active');
return;
}
if (file.size > maxSize) {
showToast('File size exceeds 512MB limit.', 'error');
input.value = '';
document.getElementById('dropzone-default-modal').style.display = 'block';
document.getElementById('file-info-modal').classList.remove('active');
return;
}
setAudioMode(audio);
// Auto-fill title from filename
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
document.getElementById('upload-video-title').value = fileNameWithoutExt;
document.getElementById('filename-modal').textContent = file.name;
document.getElementById('filesize-modal').textContent = formatFileSizeModal(file.size);
document.getElementById('dropzone-default-modal').style.display = 'none';
document.getElementById('file-info-modal').classList.add('active');
}
}
function removeVideoModal(e) {
e.preventDefault();
e.stopPropagation();
videoInputModal.value = '';
document.getElementById('dropzone-default-modal').style.display = 'block';
document.getElementById('file-info-modal').classList.remove('active');
document.getElementById('upload-video-title').value = '';
setAudioMode(false);
}
// Thumbnail Dropzone
const thumbnailDropzoneModal = document.getElementById('thumbnail-dropzone-modal');
let thumbnailInputModal = document.getElementById('thumbnail-modal');
let _thumbClickTsModal = 0;
thumbnailInputModal.addEventListener('change', function() { _thumbClickTsModal = 0; handleThumbnailSelectModal(this); });
thumbnailDropzoneModal.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
const now = Date.now();
if (now - _thumbClickTsModal < 800) return;
_thumbClickTsModal = now;
if (typeof window.openCropperModal_thumb_upload === 'function') {
window.openCropperModal_thumb_upload();
} else {
thumbnailInputModal.click();
}
});
thumbnailDropzoneModal.addEventListener('dragover', (e) => {
e.preventDefault();
thumbnailDropzoneModal.classList.add('dragover');
});
thumbnailDropzoneModal.addEventListener('dragleave', () => {
thumbnailDropzoneModal.classList.remove('dragover');
});
thumbnailDropzoneModal.addEventListener('drop', (e) => {
e.preventDefault();
thumbnailDropzoneModal.classList.remove('dragover');
if (e.dataTransfer.files.length) {
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_upload === 'function') {
window.tcPreload_thumb_upload(droppedFile);
window.openCropperModal_thumb_upload();
} else {
thumbnailInputModal.files = e.dataTransfer.files;
handleThumbnailSelectModal(thumbnailInputModal);
}
}
});
function handleThumbnailSelectModal(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
document.getElementById('thumbnail-filename-modal').textContent = file.name;
document.getElementById('thumbnail-filesize-modal').textContent = formatFileSizeModal(file.size);
// Show thumbnail preview
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('thumbnail-preview-img').src = e.target.result;
};
reader.readAsDataURL(file);
document.getElementById('thumbnail-default-modal').style.display = 'none';
document.getElementById('thumbnail-info-modal').classList.add('active');
}
}
function removeThumbnailModal(e) {
e.preventDefault();
e.stopPropagation();
const fresh = thumbnailInputModal.cloneNode(false);
thumbnailInputModal.parentNode.replaceChild(fresh, thumbnailInputModal);
thumbnailInputModal = fresh;
thumbnailInputModal.addEventListener('change', function() { handleThumbnailSelectModal(this); });
document.getElementById('thumbnail-default-modal').style.display = 'block';
document.getElementById('thumbnail-info-modal').classList.remove('active');
}
// Slides dropzone (audio mode)
const slidesDropzoneModal = document.getElementById('slides-dropzone-modal');
let slidesInputModal = document.getElementById('slides-modal');
function handleSlidesSelectModal(input) {
if (!input.files || !input.files.length) return;
const files = Array.from(input.files);
const strip = document.getElementById('slides-strip-modal');
strip.innerHTML = '';
files.forEach(f => {
const reader = new FileReader();
reader.onload = e => {
const img = document.createElement('img');
img.src = e.target.result;
img.style.cssText = 'width:56px;height:56px;object-fit:cover;border-radius:6px;border:1px solid rgba(255,255,255,.15);';
strip.appendChild(img);
};
reader.readAsDataURL(f);
});
const count = files.length;
document.getElementById('slides-count-modal').textContent =
count === 1 ? '1 image — will show as static cover' : count + ' images — will crossfade during playback';
document.getElementById('slides-default-modal').style.display = 'none';
document.getElementById('slides-preview-modal').style.display = '';
slidesDropzoneModal.style.borderColor = '#22c55e';
}
slidesDropzoneModal.addEventListener('click', function(e) {
if (e.target.closest('button')) return;
slidesInputModal.click();
});
slidesInputModal.addEventListener('change', function() { handleSlidesSelectModal(this); });
slidesDropzoneModal.addEventListener('dragover', e => { e.preventDefault(); slidesDropzoneModal.classList.add('dragover'); });
slidesDropzoneModal.addEventListener('dragleave', () => slidesDropzoneModal.classList.remove('dragover'));
slidesDropzoneModal.addEventListener('drop', e => {
e.preventDefault();
slidesDropzoneModal.classList.remove('dragover');
if (e.dataTransfer.files.length) {
slidesInputModal.files = e.dataTransfer.files;
handleSlidesSelectModal(slidesInputModal);
}
});
function removeSlidesModal(e) {
e.preventDefault();
e.stopPropagation();
const fresh = slidesInputModal.cloneNode(false);
slidesInputModal.parentNode.replaceChild(fresh, slidesInputModal);
slidesInputModal = fresh;
slidesInputModal.addEventListener('change', function() { handleSlidesSelectModal(this); });
document.getElementById('slides-default-modal').style.display = 'block';
document.getElementById('slides-preview-modal').style.display = 'none';
slidesDropzoneModal.style.borderColor = '#e61e1e';
}
// Type options
document.querySelectorAll('#type-options-modal .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', function() {
document.querySelectorAll('#type-options-modal .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// Visibility options
document.querySelectorAll('#visibility-options-modal .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', function() {
document.querySelectorAll('#visibility-options-modal .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// Download options
document.querySelectorAll('#download-options-modal .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', function() {
document.querySelectorAll('#download-options-modal .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// Form submission
document.getElementById('upload-form-modal').addEventListener('submit', function(e) {
e.preventDefault();
// Validate video file
const videoInput = document.getElementById('video-modal');
if (!videoInput.files || !videoInput.files[0]) {
showErrorModal('Please select a video file');
return;
}
// Validate title
const title = document.getElementById('upload-video-title').value.trim();
if (!title) {
showErrorModal('Please enter a title');
return;
}
// At least one slide image is required for audio uploads
if (isAudioFile(videoInput.files[0])) {
if (!slidesInputModal.files || !slidesInputModal.files.length) {
showErrorModal('At least one cover image is required for audio tracks');
document.getElementById('slides-dropzone-modal').style.borderColor = '#e61e1e';
return;
}
}
const formData = new FormData(this);
const xhr = new XMLHttpRequest();
// Set timeout for large file uploads (30 minutes)
xhr.timeout = 1800000;
// Show progress
document.getElementById('progress-container-modal').classList.add('active');
document.getElementById('submit-btn-modal').disabled = true;
document.getElementById('submit-btn-modal').innerHTML = '<i class="bi bi-arrow-repeat"></i> Uploading...';
document.getElementById('status-message-modal').className = 'status-message-modal';
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
document.getElementById('progress-bar-modal').style.width = percent + '%';
document.getElementById('progress-text-modal').textContent = 'Uploading... ' + percent + '%';
}
});
xhr.addEventListener('load', function() {
document.getElementById('progress-bar-modal').style.width = '100%';
document.getElementById('progress-text-modal').textContent = 'Processing...';
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
document.getElementById('progress-text-modal').textContent = 'Done! Redirecting...';
setTimeout(() => {
closeUploadModal();
window.location.href = response.redirect;
}, 1000);
} else {
showErrorModal(response.message || 'Upload failed');
}
} catch(e) {
showErrorModal('Invalid response from server');
}
} else if (xhr.status === 0) {
showErrorModal('Connection lost. Please check your internet connection.');
} else {
try {
const response = JSON.parse(xhr.responseText);
// Laravel validation errors come back as { errors: { field: ['msg'] } }
if (response.errors) {
const msgs = Object.entries(response.errors)
.map(([field, errs]) => `<b>${field}:</b> ${errs.join(', ')}`)
.join('<br>');
showErrorModal(msgs);
} else {
showErrorModal(response.message || `Server error (${xhr.status})`);
}
} catch(e) {
showErrorModal(`Upload failed (${xhr.status}): ${xhr.responseText.slice(0, 300)}`);
}
}
});
xhr.addEventListener('error', function() {
showErrorModal('Upload failed. Please check your internet connection.');
});
xhr.addEventListener('timeout', function() {
showErrorModal('Upload timed out. The file may be too large.');
});
xhr.addEventListener('abort', function() {
showErrorModal('Upload was cancelled.');
});
xhr.open('POST', '{{ route("videos.store") }}');
xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}');
xhr.send(formData);
});
function showErrorModal(message) {
document.getElementById('progress-container-modal').classList.remove('active');
document.getElementById('status-message-modal').innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + message;
document.getElementById('status-message-modal').className = 'status-message-modal error';
document.getElementById('submit-btn-modal').disabled = false;
document.getElementById('submit-btn-modal').innerHTML = '<i class="bi bi-cloud-arrow-up-fill"></i> Upload Video';
}
</script>
<x-image-cropper
id="thumb_upload"
:width="448"
:height="252"
shape="square"
target-input="thumbnail-modal"
preview-img="thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>