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:
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"
|
||||
/>
|
||||
Reference in new issue
Block a user