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

551 lines
26 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.

@extends('layouts.app')
@section('title', 'Edit ' . $video->title . ' | ' . config('app.name'))
@section('main_class', 'edit-page-only edit-page-responsive')
@section('extra_styles')
<style>
@media (max-width: 991px) {
.edit-page-only .yt-header, .edit-page-only .yt-sidebar { display: block !important; }
.edit-page-only .yt-main { display: block !important; margin-left: 0; padding-bottom: 80px; }
.edit-page-only.edit-page-responsive { display: block; background: transparent; padding: 0; }
.edit-page-only.edit-page-responsive .edit-page-standalone { max-width: 100%; border-radius: 0; box-shadow: none; background: var(--bg-primary); border: none; padding: 16px; }
.edit-page-only.edit-page-responsive .edit-page-body { padding: 0; background: transparent; }
.edit-page-only.edit-page-responsive .form-input,
.edit-page-only.edit-page-responsive .form-textarea { background: var(--bg-secondary); border-color: var(--border-color); color: var(--text-primary); }
.edit-page-only.edit-page-responsive .form-label { color: var(--text-primary); }
#edit-thumbnail-dropzone { padding: 20px 16px; }
.options-grid { flex-direction: column; }
.option-item { min-width: 100%; }
}
@media (min-width: 992px) {
.edit-page-only { min-height: auto; display: block; padding: 20px; background: #121212; }
}
.edit-page-standalone {
background: #1a1a1a;
border: none;
border-radius: 0;
box-shadow: none;
overflow: hidden;
width: 100%;
max-width: 700px;
margin: 0 auto;
padding: 20px;
}
.edit-page-body { padding: 0; background: transparent; }
/* Form Groups */
.form-group { margin-bottom: 20px; }
.form-label { display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; color: #e5e5e5; }
.form-input, .form-textarea { width: 100%; background: #121212; border: 1px solid #333; border-radius: 8px; padding: 12px 16px; color: var(--text-primary); font-size: 14px; }
.form-input:focus, .form-textarea:focus { outline: none; border-color: var(--brand-red); }
.form-textarea { resize: vertical; min-height: 100px; }
/* Current thumbnail */
#current-thumb-wrap {
width: 100%; height: 140px; border-radius: 10px; overflow: hidden;
background: #151515; border: 2px dashed #404040; position: relative; margin-bottom: 8px;
}
#current-thumb-wrap img { width: 100%; height: 100%; object-fit: cover; display: none; }
#current-thumb-wrap.has-thumb img { display: block; }
#current-thumb-placeholder {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; color: #666; gap: 8px;
}
#current-thumb-placeholder i { font-size: 32px; }
#current-thumb-placeholder span { font-size: 13px; }
#current-thumb-wrap.has-thumb #current-thumb-placeholder { display: none; }
/* Thumbnail Dropzone */
#edit-thumbnail-dropzone {
border: 2px dashed #404040;
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #151515;
position: relative;
}
#edit-thumbnail-dropzone:hover { border-color: var(--brand-red); }
#edit-thumbnail-dropzone.dragover { border-color: var(--brand-red); background: rgba(230,30,30,.05); }
#edit-thumbnail { display: none; }
#edit-thumbnail-default i { font-size: 28px; color: #666; margin-bottom: 6px; }
#edit-thumbnail-default p { color: #888; font-size: 13px; margin: 0; }
#edit-thumbnail-info { display: none; }
#edit-thumbnail-info.active { display: block; }
#edit-thumbnail-info img { max-width: 100%; max-height: 120px; border-radius: 8px; }
/* Options Grid */
.options-section { margin-bottom: 24px; }
.options-label { font-size: 14px; font-weight: 500; color: #e5e5e5; margin-bottom: 12px; display: block; }
.options-grid { display: flex; gap: 8px; flex-wrap: wrap; }
.option-item { flex: 1; min-width: 100px; cursor: pointer; }
.option-item input { display: none; }
.option-content { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: #1a1a1a; border: 2px solid #333; border-radius: 10px; transition: all 0.2s; }
.option-item:hover .option-content { border-color: #555; background: #1f1f1f; }
.option-item.active .option-content { border-color: var(--brand-red); background: rgba(230,30,30,.1); }
.option-content i { font-size: 18px; color: #666; width: 20px; text-align: center; }
.option-item.active .option-content i { color: var(--brand-red); }
.option-content span { font-size: 13px; color: #aaa; }
.option-item.active .option-content span { color: #fff; }
/* File info row (for thumbnail) */
.file-info-row {
display: flex; align-items: center; gap: 12px; padding: 12px;
background: #1f1f1f; border-radius: 8px; border: 1px solid #333;
}
.file-info-row .thumb-preview { width: 80px; height: 48px; overflow: hidden; border-radius: 6px; flex-shrink: 0; }
.file-info-row .thumb-preview img { width: 100%; height: 100%; object-fit: cover; }
.file-info-row .file-meta { flex: 1; text-align: left; min-width: 0; }
.file-info-row .file-meta .fn { color: #e5e5e5; font-weight: 500; font-size: 13px; margin: 0 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-info-row .file-meta .fs { color: #888; font-size: 12px; margin: 0; }
.btn-remove { width: 32px; height: 32px; border-radius: 50%; background: #333; border: none; color: #888; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.btn-remove:hover { background: var(--brand-red); color: white; }
/* Slides manager */
.ep-slides-strip {
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;
}
.ep-slides-empty {
display: flex; align-items: center; justify-content: center;
width: 100%; color: var(--text-secondary); font-size: 13px;
}
.ep-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;
}
.ep-item:hover { border-color: var(--brand-red); }
.ep-item img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
.ep-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;
}
.ep-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;
}
.ep-remove:hover { background: var(--brand-red); color: #fff; }
.ep-item.ep-dragging { opacity: .35; outline: 2px dashed var(--brand-red); }
/* Status */
#edit-status { display: none; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
#edit-status.success { display: block; background: rgba(34,197,94,.2); color: #4ade80; border: 1px solid rgba(34,197,94,.3); }
#edit-status.error { display: block; background: rgba(239,68,68,.2); color: #f87171; border: 1px solid rgba(239,68,68,.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; }
/* Danger zone */
.danger-zone { margin-top: 32px; padding-top: 20px; border-top: 1px solid #333; }
.danger-zone p { font-size: 13px; color: #888; margin-bottom: 12px; }
.danger-zone .action-btn { width: 100%; justify-content: center; }
@media (max-width: 600px) {
.options-grid { flex-direction: column; }
.option-item { min-width: 100%; }
}
</style>
@endsection
@section('content')
<div class="edit-page-standalone">
<div class="edit-page-body">
<form id="edit-form" enctype="multipart/form-data">
@csrf
@method('PUT')
@if(!$video->isAudioOnly())
{{-- Current Thumbnail (non-audio only) --}}
<div class="form-group">
<label class="form-label"><i class="bi bi-image"></i> Current Thumbnail</label>
<div id="current-thumb-wrap" class="{{ $video->thumbnail ? 'has-thumb' : '' }}">
<img src="{{ $video->thumbnail ? asset('storage/thumbnails/'.$video->thumbnail) : '' }}" alt="Thumbnail">
<div id="current-thumb-placeholder">
<i class="bi bi-card-image"></i>
<span>No thumbnail set</span>
</div>
</div>
</div>
@endif
{{-- Title --}}
<div class="form-group">
<label class="form-label">Title *</label>
<input type="text" name="title" id="edit-title" required class="form-input"
value="{{ $video->title }}" placeholder="Enter video title">
</div>
{{-- Description --}}
<div class="form-group">
<label class="form-label">Description</label>
<textarea name="description" id="edit-description" rows="4" class="form-textarea"
placeholder="Tell viewers about your video (Markdown supported)">{{ $video->description }}</textarea>
</div>
@if(!$video->isAudioOnly())
{{-- Thumbnail dropzone (non-audio) --}}
<div class="form-group">
<label class="form-label"><i class="bi bi-camera"></i> Change Thumbnail</label>
<div id="edit-thumbnail-dropzone">
<input type="file" name="thumbnail" id="edit-thumbnail" accept="image/*">
<div id="edit-thumbnail-default">
<i class="bi bi-card-image"></i>
<p>Click to select new thumbnail (optional, max 20MB)</p>
</div>
<div id="edit-thumbnail-info">
<div class="file-info-row">
<div class="thumb-preview"><img id="edit-thumb-preview-img" src="" alt=""></div>
<div class="file-meta">
<p class="fn" id="edit-thumb-filename"></p>
<p class="fs" id="edit-thumb-filesize"></p>
</div>
<button type="button" class="btn-remove" onclick="removeEditThumb(event)"><i class="bi bi-x-lg"></i></button>
</div>
</div>
</div>
</div>
@else
{{-- Slides manager (audio tracks only) --}}
<div class="form-group">
<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>
<input type="hidden" name="slides_order" id="ep-slides-order" value="[]">
<input type="file" name="slides_add[]" id="ep-slides-add-input" accept="image/*" multiple style="display:none;">
<div class="ep-slides-strip" id="ep-slides-strip">
<div class="ep-slides-empty" id="ep-slides-empty">No slides yet</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<button type="button" class="action-btn" onclick="document.getElementById('ep-slides-add-input').click()">
<i class="bi bi-plus-lg"></i> <span>Add Images</span>
</button>
<span id="ep-slides-hint" style="font-size:12px;color:var(--text-secondary);"></span>
</div>
</div>
@endif
{{-- Video Type --}}
<div class="options-section">
<span class="options-label">Video Type</span>
<div class="options-grid" id="edit-type-options">
@foreach([['generic','bi-film','Generic'],['music','bi-music-note','Music'],['match','bi-trophy','Match']] as [$val,$icon,$label])
<label class="option-item {{ ($video->type ?? 'generic') === $val ? 'active' : '' }}" data-type="{{ $val }}">
<input type="radio" name="type" value="{{ $val }}" {{ ($video->type ?? 'generic') === $val ? 'checked' : '' }}>
<div class="option-content">
<i class="bi {{ $icon }}"></i>
<span>{{ $label }}</span>
</div>
</label>
@endforeach
</div>
</div>
{{-- Privacy --}}
<div class="options-section">
<span class="options-label">Privacy</span>
<div class="options-grid" id="edit-visibility-options">
@foreach([['public','bi-globe','Public'],['unlisted','bi-link-45deg','Unlisted'],['private','bi-lock','Private']] as [$val,$icon,$label])
<label class="option-item {{ ($video->visibility ?? 'public') === $val ? 'active' : '' }}" data-privacy="{{ $val }}">
<input type="radio" name="visibility" value="{{ $val }}" {{ ($video->visibility ?? 'public') === $val ? 'checked' : '' }}>
<div class="option-content">
<i class="bi {{ $icon }}"></i>
<span>{{ $label }}</span>
</div>
</label>
@endforeach
</div>
</div>
{{-- Download Access --}}
<div class="options-section">
<span class="options-label"><i class="bi bi-download" style="color:var(--brand-red);margin-right:6px;"></i>Who can download?</span>
<div class="options-grid" id="edit-download-options">
@foreach([['disabled','bi-slash-circle','Disabled'],['everyone','bi-globe','Everyone'],['registered','bi-person-check','Members'],['subscribers','bi-star','Subscribers']] as [$val,$icon,$label])
<label class="option-item {{ ($video->download_access ?? 'disabled') === $val ? 'active' : '' }}" data-download="{{ $val }}">
<input type="radio" name="download_access" value="{{ $val }}" {{ ($video->download_access ?? 'disabled') === $val ? 'checked' : '' }}>
<div class="option-content">
<i class="bi {{ $icon }}"></i>
<span>{{ $label }}</span>
</div>
</label>
@endforeach
</div>
</div>
{{-- Status --}}
<div id="edit-status"></div>
{{-- Submit --}}
<button type="submit" id="edit-submit-btn" class="action-btn action-btn-primary btn-submit">
<i class="bi bi-check-lg"></i> <span>Save Changes</span>
</button>
</form>
{{-- Danger zone --}}
<div class="danger-zone">
<p>Danger Zone</p>
<form action="{{ route('videos.destroy', $video) }}" method="POST" id="delete-edit-form">
@csrf
@method('DELETE')
<button type="button" class="action-btn action-btn-danger"
onclick="showConfirm('Delete this video? This cannot be undone.', function(){ document.getElementById('delete-edit-form').submit(); }, 'Delete')">
<i class="bi bi-trash"></i> <span>Delete Video</span>
</button>
</form>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024, 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];
}
@if(!$video->isAudioOnly())
// ── Thumbnail dropzone (non-audio) ────────────────────────────
const editThumbInput = document.getElementById('edit-thumbnail');
const editThumbDropzone = document.getElementById('edit-thumbnail-dropzone');
editThumbDropzone.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove')) return;
if (typeof window.openCropperModal_thumb_edit_mobile === 'function') {
window.openCropperModal_thumb_edit_mobile();
} else {
editThumbInput.click();
}
});
editThumbDropzone.addEventListener('dragover', e => { e.preventDefault(); editThumbDropzone.classList.add('dragover'); });
editThumbDropzone.addEventListener('dragleave', () => editThumbDropzone.classList.remove('dragover'));
editThumbDropzone.addEventListener('drop', e => {
e.preventDefault(); editThumbDropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
const droppedFile = e.dataTransfer.files[0];
if (typeof window.tcPreload_thumb_edit_mobile === 'function') {
window.tcPreload_thumb_edit_mobile(droppedFile);
window.openCropperModal_thumb_edit_mobile();
} else {
editThumbInput.files = e.dataTransfer.files;
handleEditThumbSelect();
}
}
});
editThumbInput.addEventListener('change', handleEditThumbSelect);
function handleEditThumbSelect() {
if (!editThumbInput.files || !editThumbInput.files[0]) return;
const file = editThumbInput.files[0];
document.getElementById('edit-thumb-filename').textContent = file.name;
document.getElementById('edit-thumb-filesize').textContent = formatFileSize(file.size);
const reader = new FileReader();
reader.onload = e => { document.getElementById('edit-thumb-preview-img').src = e.target.result; };
reader.readAsDataURL(file);
document.getElementById('edit-thumbnail-default').style.display = 'none';
document.getElementById('edit-thumbnail-info').classList.add('active');
}
function removeEditThumb(e) {
e.preventDefault(); e.stopPropagation();
editThumbInput.value = '';
document.getElementById('edit-thumbnail-default').style.display = 'block';
document.getElementById('edit-thumbnail-info').classList.remove('active');
}
@else
// ── Slides manager (audio tracks only) ────────────────────────
let epSlidesData = @json($video->slides->map(fn($s) => ['id' => $s->id, 'url' => asset('storage/thumbnails/'.$s->filename)])->values());
let epDragSrc = null;
function _epSyncOrder() {
const keptIds = epSlidesData.filter(s => s.id).map(s => s.id);
document.getElementById('ep-slides-order').value = JSON.stringify(keptIds);
const count = epSlidesData.length;
document.getElementById('ep-slides-hint').textContent =
count === 1 ? '1 image — static cover' : count + ' images — will crossfade';
}
function epSlidesRefresh() {
const strip = document.getElementById('ep-slides-strip');
const empty = document.getElementById('ep-slides-empty');
strip.querySelectorAll('.ep-item').forEach(el => el.remove());
empty.style.display = epSlidesData.length === 0 ? 'flex' : 'none';
epSlidesData.forEach((item) => {
const div = document.createElement('div');
div.className = 'ep-item';
div.draggable = true;
div._epData = item; // reference on element — avoids stale-index issues during drag
div.innerHTML = `
<div class="ep-drag-handle" title="Drag to reorder"><i class="bi bi-grip-vertical"></i></div>
<img src="${item.url}" alt="">
<button type="button" class="ep-remove" title="Remove"><i class="bi bi-x-lg"></i></button>
`;
div.querySelector('.ep-remove').addEventListener('click', () => {
epSlidesData.splice(epSlidesData.indexOf(div._epData), 1);
epSlidesRefresh();
});
div.addEventListener('dragstart', e => {
epDragSrc = div;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => div.classList.add('ep-dragging'), 0);
});
// On dragend: sync epSlidesData from current DOM order, then update inputs
div.addEventListener('dragend', () => {
div.classList.remove('ep-dragging');
epDragSrc = null;
const newData = [];
strip.querySelectorAll('.ep-item').forEach(el => newData.push(el._epData));
epSlidesData = newData;
_epSyncOrder();
});
// On dragover: only move element in DOM — never re-render
div.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!epDragSrc || epDragSrc === div) return;
const items = [...strip.querySelectorAll('.ep-item')];
const srcPos = items.indexOf(epDragSrc);
const tgtPos = items.indexOf(div);
if (srcPos < tgtPos) div.after(epDragSrc);
else div.before(epDragSrc);
});
strip.appendChild(div);
});
_epSyncOrder();
}
document.getElementById('ep-slides-add-input').addEventListener('change', function() {
Array.from(this.files).forEach(file => {
const reader = new FileReader();
reader.onload = e => {
epSlidesData.push({ file, url: e.target.result });
epSlidesRefresh();
};
reader.readAsDataURL(file);
});
this.value = '';
});
// Initialise strip with existing slides
epSlidesRefresh();
@endif
// ── Option selectors ──────────────────────────────────────────
document.querySelectorAll('#edit-type-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#edit-type-options .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
document.querySelectorAll('#edit-visibility-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#edit-visibility-options .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
document.querySelectorAll('#edit-download-options .option-item').forEach(item => {
item.querySelector('input').addEventListener('change', () => {
document.querySelectorAll('#edit-download-options .option-item').forEach(o => o.classList.remove('active'));
item.classList.add('active');
});
});
// ── Form submission ───────────────────────────────────────────
document.getElementById('edit-form').addEventListener('submit', function(e) {
e.preventDefault();
const title = document.getElementById('edit-title').value.trim();
if (!title) { showEditError('Please enter a title.'); return; }
const btn = document.getElementById('edit-submit-btn');
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Saving...';
document.getElementById('edit-status').className = '';
let formData = new FormData(this);
@if($video->isAudioOnly())
// Append newly added slide files
epSlidesData.filter(s => s.file).forEach(s => formData.append('slides_add[]', s.file, s.file.name));
@endif
fetch('{{ route("videos.update", $video) }}', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
})
.then(r => r.json())
.then(data => {
if (data.success) {
const status = document.getElementById('edit-status');
status.innerHTML = '<i class="bi bi-check-circle-fill"></i> ' + data.message;
status.className = 'success';
setTimeout(() => window.location.href = '{{ route("videos.show", $video) }}', 1000);
} else {
showEditError(data.message || 'Update failed.');
}
})
.catch(() => showEditError('Something went wrong. Please try again.'));
});
function showEditError(message) {
const status = document.getElementById('edit-status');
status.innerHTML = '<i class="bi bi-exclamation-circle-fill"></i> ' + message;
status.className = 'error';
const btn = document.getElementById('edit-submit-btn');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-check-lg"></i> <span>Save Changes</span>';
}
</script>
<x-image-cropper
id="thumb_edit_mobile"
:width="448"
:height="252"
shape="square"
target-input="edit-thumbnail"
preview-img="edit-thumbnail-preview-img"
output-width="1280"
title="Crop Thumbnail"
/>
@endsection