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 <[email protected]>
This commit is contained in:
ghassanandClaude Sonnet 4.6 committed 2026-05-13 13:24:32 +03:00
1 parent d44490dfe0
commit 0b2e95ea65
161 files changed
+27496 -7250

No files matched your search

@@ -22,19 +22,20 @@
<form id="upload-form-modal" enctype="multipart/form-data">
@csrf
<!-- Video File -->
<!-- Video / Audio File -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-camera-video"></i> Video File *
<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/*" required>
<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 video here</p>
<p class="dropzone-hint">MP4, MOV, AVI, WebM and more</p>
<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">
@@ -71,10 +72,10 @@
placeholder="Tell viewers about your video (Markdown supported)"></textarea>
</div>
<!-- Thumbnail -->
<div class="form-group">
<label class="form-label">
<i class="bi bi-image"></i> Thumbnail
<!-- 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/*">
@@ -82,7 +83,7 @@
<div class="dropzone-icon thumbnail-icon">
<i class="bi bi-card-image"></i>
</div>
<p class="dropzone-title">Click to select thumbnail (optional)</p>
<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">
@@ -99,6 +100,30 @@
</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 1–10 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">
@@ -162,6 +187,43 @@
<!-- 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">
@@ -174,8 +236,8 @@
<div id="status-message-modal" class="status-message-modal"></div>
<!-- Submit Button -->
<button type="submit" id="submit-btn-modal" class="btn-submit">
<i class="bi bi-cloud-arrow-up-fill"></i> Upload Video
<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>
@@ -324,13 +386,7 @@
}
.dropzone-modal input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
display: none;
}
.dropzone-icon {
@@ -437,6 +493,7 @@
.btn-remove-file {
width: 28px;
height: 28px;
padding: 0;
border-radius: 50%;
background: #333;
border: none;
@@ -569,28 +626,16 @@
/* Submit Button */
.btn-submit {
width: 100%;
background: #e61e1e;
color: white;
border: none;
padding: 14px 24px;
border-radius: 8px;
justify-content: center;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-submit:hover {
background: #cc1a1a;
}
.btn-submit:disabled {
background: #555;
background: #555 !important;
border-color: #555 !important;
cursor: not-allowed;
opacity: 1;
}
/* Responsive */
@@ -673,6 +718,15 @@ function resetUploadForm() {
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
@@ -693,7 +747,8 @@ videoInputModal.addEventListener('change', function() {
});
videoDropzoneModal.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-remove-file')) return;
if (e.target.closest('.btn-remove-file')) return;
videoInputModal.click();
});
videoDropzoneModal.addEventListener('dragover', (e) => {
@@ -714,23 +769,56 @@ videoDropzoneModal.addEventListener('drop', (e) => {
}
});
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
// Validate file type
const validTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo', 'video/x-flv', 'video/x-matroska'];
const validExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv'];
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();
let isValidType = validTypes.includes(file.type);
let fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!isValidType) {
isValidType = validExtensions.includes(fileExtension);
}
const audio = isAudioFile(file);
const validVideo = validVideoTypes.includes(file.type) || validVideoExtensions.includes(ext);
if (!isValidType) {
alert('Invalid video format. Please select a valid video file (MP4, MOV, AVI, WebM, OGG, WMV, FLV, MKV).');
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');
@@ -738,14 +826,16 @@ function handleVideoSelectModal(input) {
}
if (file.size > maxSize) {
alert('File size exceeds 512MB limit. Please select a smaller video file.');
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;
}
// Auto-fill title from filename (like create page)
setAudioMode(audio);
// Auto-fill title from filename
const fileNameWithoutExt = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
document.getElementById('upload-video-title').value = fileNameWithoutExt;
@@ -763,14 +853,26 @@ function removeVideoModal(e) {
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');
const thumbnailInputModal = document.getElementById('thumbnail-modal');
let thumbnailInputModal = document.getElementById('thumbnail-modal');
thumbnailInputModal.addEventListener('change', function() {
handleThumbnailSelectModal(this);
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) => {
@@ -786,8 +888,14 @@ thumbnailDropzoneModal.addEventListener('drop', (e) => {
e.preventDefault();
thumbnailDropzoneModal.classList.remove('dragover');
if (e.dataTransfer.files.length) {
thumbnailInputModal.files = e.dataTransfer.files;
handleThumbnailSelectModal(thumbnailInputModal);
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);
}
}
});
@@ -812,11 +920,69 @@ function handleThumbnailSelectModal(input) {
function removeThumbnailModal(e) {
e.preventDefault();
e.stopPropagation();
thumbnailInputModal.value = '';
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() {
@@ -833,6 +999,14 @@ document.querySelectorAll('#visibility-options-modal .option-item').forEach(item
});
});
// 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();
@@ -847,10 +1021,19 @@ document.getElementById('upload-form-modal').addEventListener('submit', function
// Validate title
const title = document.getElementById('upload-video-title').value.trim();
if (!title) {
showErrorModal('Please enter a title for your video');
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();
@@ -895,9 +1078,17 @@ document.getElementById('upload-form-modal').addEventListener('submit', function
} else {
try {
const response = JSON.parse(xhr.responseText);
showErrorModal(response.message || `Server error (${xhr.status})`);
// 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 with status ${xhr.status}`);
showErrorModal(`Upload failed (${xhr.status}): ${xhr.responseText.slice(0, 300)}`);
}
}
});
@@ -927,3 +1118,14 @@ function showErrorModal(message) {
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"
/>