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

@@ -116,15 +116,17 @@
<div class="step-navigation">
<div></div>
<button type="button" class="btn-next" onclick="editNextStep(2)">
Next Step <i class="bi bi-arrow-right"></i>
<button type="button" class="action-btn action-btn-primary btn-next" onclick="editNextStep(2)">
<span>Next Step</span> <i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
<!-- Step 2: Thumbnail -->
<!-- Step 2: Thumbnail / Slides -->
<div class="edit-step-content" data-step="2">
<div class="form-group">
<!-- Standard thumbnail (non-audio) -->
<div id="edit-thumb-section" class="form-group">
<label class="form-label">
<i class="bi bi-camera"></i> Change Thumbnail
</label>
@@ -135,7 +137,7 @@
<i class="bi bi-card-image"></i>
</div>
<p class="dropzone-title">Click to select or drag thumbnail</p>
<p class="dropzone-hint">JPG, PNG, WebP up to 5MB</p>
<p class="dropzone-hint">JPG, PNG, WebP up to 20MB</p>
</div>
<div id="edit-thumbnail-info" class="file-info-modal">
<div class="file-preview thumbnail-preview">
@@ -145,20 +147,42 @@
<p class="filename" id="edit-thumbnail-filename"></p>
<p id="edit-thumbnail-filesize"></p>
</div>
<button type="button" class="btn-remove-file"
onclick="removeEditThumbnail(event)">
<button type="button" class="btn-remove-file" onclick="removeEditThumbnail(event)">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
<!-- Slides manager (audio tracks) -->
<div id="edit-slides-section" style="display:none;">
<label class="form-label"><i class="bi bi-images"></i> Cover Slides
<span style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-left:6px;">Drag to reorder · click × to remove</span>
</label>
<!-- Hidden inputs carrying order + new files -->
<input type="hidden" name="slides_order" id="edit-slides-order" value="[]">
<input type="file" name="slides_add[]" id="edit-slides-add-input" accept="image/*" multiple style="display:none;">
<div id="edit-slides-strip" style="display:flex;flex-wrap:wrap;gap:8px;min-height:72px;padding:10px;background:rgba(255,255,255,.03);border:2px dashed rgba(255,255,255,.1);border-radius:10px;margin-bottom:8px;">
<!-- Slide items injected by JS -->
<div id="edit-slides-empty" style="display:flex;align-items:center;justify-content:center;width:100%;color:var(--text-secondary);font-size:13px;">No slides yet</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button type="button" class="action-btn" onclick="document.getElementById('edit-slides-add-input').click()">
<i class="bi bi-plus-lg"></i> <span>Add Images</span>
</button>
<span id="edit-slides-hint" style="font-size:12px;color:var(--text-secondary);"></span>
</div>
</div>
<div class="step-navigation">
<button type="button" class="btn-prev" onclick="editPrevStep(1)">
<i class="bi bi-arrow-left"></i> Back
<button type="button" class="action-btn btn-prev" onclick="editPrevStep(1)">
<i class="bi bi-arrow-left"></i> <span>Back</span>
</button>
<button type="button" class="btn-next" onclick="editNextStep(3)">
Next Step <i class="bi bi-arrow-right"></i>
<button type="button" class="action-btn action-btn-primary btn-next" onclick="editNextStep(3)">
<span>Next Step</span> <i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
@@ -203,15 +227,52 @@
</div>
</div>
<!-- Download Access -->
<div class="form-group" style="margin: 16px 0 0;">
<label class="form-label">
<i class="bi bi-download"></i> Who can download?
</label>
<div class="options-grid" id="edit-download-options-modal">
<label class="option-item" data-download="disabled">
<input type="radio" name="download_access" value="disabled">
<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>
<!-- Status Message -->
<div id="edit-status-message" class="status-message-modal"></div>
<div class="step-navigation">
<button type="button" class="btn-prev" onclick="editPrevStep(2)">
<i class="bi bi-arrow-left"></i> Back
<button type="button" class="action-btn btn-prev" onclick="editPrevStep(2)">
<i class="bi bi-arrow-left"></i> <span>Back</span>
</button>
<button type="submit" class="btn-save-changes" id="edit-submit-btn">
<i class="bi bi-check-lg"></i> Save Changes
<button type="submit" class="action-btn action-btn-primary btn-save-changes" id="edit-submit-btn">
<i class="bi bi-check-lg"></i> <span>Save Changes</span>
</button>
</div>
</div>
@@ -604,13 +665,7 @@
}
.dropzone-modal input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
display: none;
}
.dropzone-icon {
@@ -795,51 +850,14 @@
border-top: 1px solid #2a2a2a;
}
.btn-prev,
.btn-next,
.btn-save-changes {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-prev {
background: transparent;
border: 1px solid #404040;
color: #aaa;
}
.btn-prev:hover {
border-color: #666;
color: #fff;
}
.btn-next,
.btn-save-changes {
background: linear-gradient(135deg, #e61e1e 0%, #ff4757 100%);
color: white;
box-shadow: 0 4px 15px rgba(230, 30, 30, 0.3);
}
.btn-next:hover,
.btn-save-changes:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(230, 30, 30, 0.4);
}
.btn-next:disabled,
.btn-save-changes:disabled {
background: #444;
background: #444 !important;
border-color: #444 !important;
cursor: not-allowed;
transform: none;
box-shadow: none;
opacity: 1;
}
/* Status Message */
@@ -869,6 +887,51 @@
border: 1px solid rgba(239, 68, 68, 0.3);
}
/* Slide items in edit modal */
.es-item {
position: relative;
width: 72px;
height: 72px;
border-radius: 8px;
overflow: hidden;
border: 2px solid rgba(255,255,255,.12);
cursor: grab;
flex-shrink: 0;
transition: border-color .2s, transform .15s;
}
.es-item:hover { border-color: #e61e1e; }
.es-item img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.es-drag-handle {
position: absolute;
top: 2px; left: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55);
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 11px; color: #ccc;
cursor: grab;
}
.es-remove {
position: absolute;
top: 2px; right: 2px;
width: 20px; height: 20px;
background: rgba(0,0,0,.55);
border: none;
border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 10px; color: #ccc;
cursor: pointer;
padding: 0;
transition: background .15s, color .15s;
}
.es-remove:hover { background: #e61e1e; color: #fff; }
.es-item.es-dragging { opacity: .35; outline: 2px dashed #e61e1e; }
/* Responsive */
@media (max-width: 576px) {
.edit-modal-body {
@@ -908,6 +971,12 @@
// Open modal and load video data
function openEditVideoModal(videoId) {
// On mobile redirect to the full edit page
if (window.innerWidth < 992) {
window.location.href = `/videos/${videoId}/edit`;
return;
}
window.currentVideoId = videoId;
// Fetch video data
@@ -960,11 +1029,31 @@
}
});
// Set download_access radio cards
const dlValue = video.download_access || 'disabled';
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(opt => {
opt.classList.remove('active');
const radio = opt.querySelector('input[type="radio"]');
if (radio.value === dlValue) {
radio.checked = true;
opt.classList.add('active');
} else {
radio.checked = false;
}
});
// Reset file inputs
document.getElementById('edit-thumbnail-default').style.display = 'block';
document.getElementById('edit-thumbnail-info').classList.remove('active');
document.getElementById('edit-status-message').className = 'status-message-modal';
// Populate slides or thumbnail section
populateEditSlides(video.slides || [], video.is_audio || false);
// Update step 2 label dynamically
const step2Label = document.querySelector('.edit-step[data-step="2"] .step-label');
if (step2Label) step2Label.textContent = video.is_audio ? 'Slides' : 'Thumbnail';
// Reset step to 1
window.currentEditStep = 1;
updateEditStepDisplay();
@@ -980,12 +1069,12 @@
document.getElementById('editVideoModal').classList.add('show');
}, 10);
} else {
alert('Failed to load video data');
showToast('Failed to load video data', 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to load video data');
showToast('Failed to load video data', 'error');
});
}
@@ -1011,7 +1100,7 @@
if (step === 2) {
const title = document.getElementById('edit-video-title').value.trim();
if (!title) {
alert('Please enter a title for your video');
showToast('Please enter a title for your video', 'error');
return;
}
}
@@ -1066,6 +1155,15 @@
});
if (editThumbnailDropzone) {
editThumbnailDropzone.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-file')) return;
if (typeof window.openCropperModal_thumb_edit === 'function') {
window.openCropperModal_thumb_edit();
} else {
editThumbnailInput.click();
}
});
editThumbnailDropzone.addEventListener('dragover', (e) => {
e.preventDefault();
editThumbnailDropzone.classList.add('dragover');
@@ -1079,8 +1177,14 @@
e.preventDefault();
editThumbnailDropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
editThumbnailInput.files = e.dataTransfer.files;
handleEditThumbnailSelect(editThumbnailInput);
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_edit === 'function') {
window.tcPreload_thumb_edit(droppedFile);
window.openCropperModal_thumb_edit();
} else {
editThumbnailInput.files = e.dataTransfer.files;
handleEditThumbnailSelect(editThumbnailInput);
}
}
});
}
@@ -1113,6 +1217,109 @@
document.getElementById('edit-thumbnail-info').classList.remove('active');
}
// ── Slides manager (audio tracks) ────────────────────────────
let editSlidesData = []; // [{id, url}] for existing; {file, url} for new additions
let editDragSrc = null;
function _esSyncOrder() {
const strip = document.getElementById('edit-slides-strip');
const keptIds = editSlidesData.filter(s => s.id).map(s => s.id);
document.getElementById('edit-slides-order').value = JSON.stringify(keptIds);
const count = editSlidesData.length;
document.getElementById('edit-slides-hint').textContent =
count === 1 ? '1 image — static cover' : count + ' images — will crossfade';
}
function editSlidesRefresh() {
const strip = document.getElementById('edit-slides-strip');
const empty = document.getElementById('edit-slides-empty');
strip.querySelectorAll('.es-item').forEach(el => el.remove());
empty.style.display = editSlidesData.length === 0 ? 'flex' : 'none';
editSlidesData.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'es-item';
div.draggable = true;
div._esData = item; // reference stored on element — avoids stale-index issues during drag
div.innerHTML = `
<div class="es-drag-handle" title="Drag to reorder"><i class="bi bi-grip-vertical"></i></div>
<img src="${item.url}" alt="">
<button type="button" class="es-remove" title="Remove"><i class="bi bi-x-lg"></i></button>
`;
div.querySelector('.es-remove').addEventListener('click', () => {
editSlidesData.splice(editSlidesData.indexOf(div._esData), 1);
editSlidesRefresh();
});
div.addEventListener('dragstart', e => {
editDragSrc = div;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => div.classList.add('es-dragging'), 0);
});
// On dragend: sync editSlidesData from current DOM order, then update inputs
div.addEventListener('dragend', () => {
div.classList.remove('es-dragging');
editDragSrc = null;
const newData = [];
strip.querySelectorAll('.es-item').forEach(el => newData.push(el._esData));
editSlidesData = newData;
_esSyncOrder();
});
// On dragover: just move the element in the DOM — never re-render
div.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!editDragSrc || editDragSrc === div) return;
const items = [...strip.querySelectorAll('.es-item')];
const srcPos = items.indexOf(editDragSrc);
const tgtPos = items.indexOf(div);
if (srcPos < tgtPos) div.after(editDragSrc);
else div.before(editDragSrc);
});
strip.appendChild(div);
});
_esSyncOrder();
}
// "Add Images" picker
document.getElementById('edit-slides-add-input').addEventListener('change', function() {
Array.from(this.files).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
editSlidesData.push({ file, url: e.target.result });
editSlidesRefresh();
};
reader.readAsDataURL(file);
});
// Reset input so same files can be re-added after removal
this.value = '';
});
function populateEditSlides(slides, isAudio) {
document.getElementById('edit-thumb-section').style.display = isAudio ? 'none' : '';
document.getElementById('edit-slides-section').style.display = isAudio ? '' : 'none';
if (! isAudio) return;
editSlidesData = slides.map(s => ({ id: s.id, url: s.url }));
editSlidesRefresh();
}
// Inject new file inputs into the form on submit (FormData can't handle dynamic files otherwise)
function buildEditSlidesFormData(formData) {
// Remove existing slides_add from formData (the file input may have stale data)
// Instead append each new file individually
const newFiles = editSlidesData.filter(s => s.file);
newFiles.forEach(s => formData.append('slides_add[]', s.file, s.file.name));
return formData;
}
// Video type options
const videoTypeOptions = document.querySelectorAll('.video-type-option');
videoTypeOptions.forEach(option => {
@@ -1133,11 +1340,21 @@
});
});
// Download access options
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(item => {
item.querySelector('input[type="radio"]').addEventListener('change', function() {
document.querySelectorAll('#edit-download-options-modal .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// Form submission
document.getElementById('edit-video-form-modal').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
let formData = new FormData(this);
// Append any newly added slide files (not captured by FormData from hidden input)
formData = buildEditSlidesFormData(formData);
const submitBtn = document.getElementById('edit-submit-btn');
// Disable button and show loading
@@ -1187,3 +1404,14 @@
});
});
</script>
<x-image-cropper
id="thumb_edit"
:width="448"
:height="252"
shape="square"
target-input="edit-thumbnail-input"
preview-img="edit-thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>