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

+180 -82
View File
@@ -266,6 +266,15 @@
gap: 8px;
margin-bottom: 8px;
cursor: pointer;
border-radius: 8px;
padding: 6px;
transition: background .15s;
}
.sidebar-video-card:hover { background: var(--bg-secondary); }
.sidebar-video-card.current-video {
background: rgba(230,30,30,.10);
border-left: 3px solid var(--brand-red);
padding-left: 9px;
}
.sidebar-thumb {
@@ -430,7 +439,7 @@
<div class="video-container @if ($video->orientation === 'portrait') portrait @elseif($video->orientation === 'square') square @elseif($video->orientation === 'ultrawide') ultrawide @endif"
id="videoContainer">
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
<source src="{{ route('videos.stream', $video) }}" type="video/mp4">
</video>
</div>
@@ -459,7 +468,7 @@
@auth
<!-- Like Button -->
<form method="POST"
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}"
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video) : route('videos.like', $video) }}"
class="d-inline">
@csrf
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
@@ -471,7 +480,7 @@
<!-- Edit Button - Only for video owner -->
@if (Auth::id() === $video->user_id)
<button class="yt-action-btn" onclick="openEditVideoModal({{ $video->id }})">
<button class="yt-action-btn" onclick="openEditVideoModal('{{ $video->getRouteKey() }}')">
<i class="bi bi-pencil"></i> Edit
</button>
@endif
@@ -482,7 +491,7 @@
@endauth
@if ($video->isShareable())
<button class="yt-action-btn"
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')"><i
onclick="videoShare('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')"><i
class="bi bi-share"></i> Share</button>
@endif
@@ -492,7 +501,7 @@
</button>
@auth
<!-- Quick Watch Later Button -->
<form method="POST" action="{{ route('videos.watchLater', $video->id) }}" class="d-inline"
<form method="POST" action="{{ route('videos.watchLater', $video) }}" class="d-inline"
style="display: inline;">
@csrf
<button type="submit" class="yt-action-btn" title="Watch Later">
@@ -507,7 +516,7 @@
<div class="channel-row"
style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none"
<a href="{{ $video->user ? route('channel', $video->user->channel) : '#' }}" class="channel-info text-decoration-none"
style="color: inherit; display: flex; align-items: center; gap: 12px;">
@if ($video->user)
<img src="{{ $video->user->avatar_url }}" class="channel-avatar"
@@ -533,72 +542,136 @@
</div>
</div>
<!-- Description -->
@if ($video->description)
@php
$fullDescription = $video->description;
$shortDescription = Str::limit($fullDescription, 200);
$needsExpand = strlen($fullDescription) > 200;
@endphp
{{-- ── Description / Insights box ── --}}
@php
$isVideoOwner = Auth::check() && Auth::id() === $video->user_id;
$hasDesc = !empty($video->description);
$showBox = $hasDesc || $isVideoOwner;
@endphp
@if ($showBox)
@php
$fullDescription = $video->description ?? '';
$shortDescription = Str::limit($fullDescription, 200);
$needsExpand = strlen($fullDescription) > 200;
@endphp
<div class="video-description-box"
style="background: var(--bg-secondary); border-radius: 12px; padding: 12px; margin-top: 12px;">
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
<span>{{ number_format($video->view_count) }} views</span>
<span>•</span>
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
<div style="border-bottom: 1px solid var(--border-color); margin: 8px 0;"></div>
<div class="description-content">
@if ($needsExpand)
<div class="description-short">
<span class="description-text">{!! Str::markdown($shortDescription) !!}</span>
<span style="color: var(--text-secondary);">... </span>
</div>
<div class="description-full" style="display: none;">
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
</div>
<button onclick="toggleDescription()"
style="background: none; border: none; color: var(--text-primary); font-weight: 600; font-size: 14px; cursor: pointer; padding: 0; margin-top: 4px;">Show
more</button>
@else
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
@endif
</div>
<style>
/* ── Description box ── */
.vdb-wrap {
background: var(--bg-secondary);
border-radius: 12px;
margin-top: 12px;
overflow: hidden;
border: 1px solid var(--border-color);
}
.vdb-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
padding: 0 4px;
}
.vdb-tab {
background: none; border: none;
color: var(--text-secondary);
font-size: 13px; font-weight: 600;
padding: 0 16px; height: 44px;
cursor: pointer; position: relative;
transition: color .15s; white-space: nowrap;
display: flex; align-items: center; gap: 6px;
}
.vdb-tab:hover { color: var(--text-primary); }
.vdb-tab.active { color: var(--text-primary); }
.vdb-tab.active::after {
content: ''; position: absolute; bottom: 0; left: 0; right: 0;
height: 2px; background: #ef4444; border-radius: 2px 2px 0 0;
}
.vdb-panel { display: none; padding: 14px 16px 16px; }
.vdb-panel.active { display: block; }
/* About panel */
.vdb-meta { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 10px; display: flex; gap: 10px; flex-wrap: wrap; }
.vdb-desc-text { font-size: 14px; line-height: 1.6; color: var(--text-primary); white-space: pre-wrap; }
.vdb-desc-text p { margin-bottom: 8px; }
.vdb-desc-text a { color: #3ea6ff; }
.vdb-show-more { background: none; border: none; color: var(--text-primary); font-weight: 700; font-size: 13px; cursor: pointer; padding: 6px 0 0; }
</style>
<div class="vdb-wrap" id="vdbWrap">
{{-- Tab bar --}}
<div class="vdb-tabs">
<button class="vdb-tab active" data-panel="vdb-about" onclick="switchVdbTab('vdb-about',this)">
<i class="bi bi-card-text"></i> About
</button>
@if($isVideoOwner)
<button class="vdb-tab" data-panel="vdb-insights" onclick="switchVdbTab('vdb-insights',this)" id="insightsTabBtn">
<i class="bi bi-bar-chart-line-fill"></i> Insights
</button>
@endif
</div>
<script>
function toggleDescription() {
const short = document.querySelector('.description-short');
const full = document.querySelector('.description-full');
const btn = document.querySelector('.video-description-box button');
if (short.style.display !== 'none') {
short.style.display = 'none';
full.style.display = 'block';
btn.textContent = 'Show less';
} else {
short.style.display = 'block';
full.style.display = 'none';
btn.textContent = 'Show more';
}
}
</script>
{{-- About panel --}}
<div class="vdb-panel active" id="vdb-about">
<div class="vdb-meta">
<span><i class="bi bi-eye" style="margin-right:4px;"></i>{{ number_format($video->view_count) }} views</span>
<span>•</span>
<span>{{ $video->created_at->format('M d, Y') }}</span>
@if($video->duration)
<span>•</span>
<span><i class="bi bi-clock" style="margin-right:4px;"></i>{{ $video->formatted_duration }}</span>
@endif
</div>
@if($hasDesc)
<div id="vdbDescShort" class="vdb-desc-text">{{ $needsExpand ? $shortDescription : $fullDescription }}</div>
@if($needsExpand)
<div id="vdbDescFull" class="vdb-desc-text" style="display:none;">{{ $fullDescription }}</div>
<button class="vdb-show-more" onclick="toggleVdbDesc(this)">Show more</button>
@endif
@else
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No description added.</p>
@endif
</div>
<style>
.video-description-box .description-text {
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
}
{{-- Insights panel (owner only) --}}
@if($isVideoOwner)
<x-video-insights :video="$video" />
@endif
</div>
.video-description-box .description-text p {
margin-bottom: 8px;
}
<script>
// ── Tab switching ──────────────────────────────────────
function switchVdbTab(panelId, btn) {
document.querySelectorAll('.vdb-tab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.vdb-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(panelId).classList.add('active');
if (panelId === 'vdb-insights' && !window._insLoaded) loadInsights();
}
.video-description-box .description-text a {
color: #3ea6ff;
}
</style>
// ── Description expand/collapse ───────────────────────
function toggleVdbDesc(btn) {
const short = document.getElementById('vdbDescShort');
const full = document.getElementById('vdbDescFull');
if (full.style.display === 'none') {
short.style.display = 'none';
full.style.display = 'block';
btn.textContent = 'Show less';
} else {
short.style.display = 'block';
full.style.display = 'none';
btn.textContent = 'Show more';
}
}
// ── Share + track ──────────────────────────────────────
function videoShare(url, title, trackUrl) {
openShareModal(url, title);
fetch(trackUrl, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '', 'Accept': 'application/json' }
}).catch(() => {});
}
</script>
@endif
@include('components.video-comments', ['video' => $video])
@@ -617,9 +690,10 @@
</h3>
<div class="recommended-videos-list">
@foreach ($playlistVideos as $index => $playlistVideo)
@if ($playlistVideo->id !== $video->id)
<div class="sidebar-video-card{{ $playlistVideo->id === $video->id ? ' current-video' : '' }}"
onclick="window.location.href='{{ route('videos.show', $playlistVideo->id) }}?playlist={{ $playlist->id }}'">
@php $isCurrent = $playlistVideo->id === $video->id; @endphp
<div class="sidebar-video-card{{ $isCurrent ? ' current-video' : '' }}"
{{ $isCurrent ? '' : "onclick=\"window.location.href='".route('videos.show', $playlistVideo)."?playlist={$playlist->share_token}'\"" }}
style="{{ $isCurrent ? 'cursor:default;' : 'cursor:pointer;' }}">
<div class="sidebar-thumb" style="position: relative;">
@if ($playlistVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $playlistVideo->thumbnail) }}"
@@ -650,13 +724,16 @@
{{ Str::limit($playlistVideo->title, 60) }}
</div>
<div class="sidebar-meta">
<div>{{ $playlistVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($playlistVideo->view_count) }} views •
{{ $playlistVideo->created_at->diffForHumans() }}</div>
@if($isCurrent)
<div style="color:var(--brand-red);font-weight:600;font-size:11px;">▶ Now Playing</div>
@else
<div>{{ $playlistVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($playlistVideo->view_count) }} views •
{{ $playlistVideo->created_at->diffForHumans() }}</div>
@endif
</div>
</div>
</div>
@endif
@endforeach
</div>
@if ($playlist->canEdit(Auth::user()))
@@ -671,8 +748,8 @@
<div class="recommended-videos-list">
@foreach ($recommendedVideos as $recVideo)
<div class="sidebar-video-card"
onclick="window.location.href='{{ route('videos.show', $recVideo->id) }}'">
<div class="sidebar-thumb">
onclick="window.location.href='{{ route('videos.show', $recVideo) }}'">
<div class="sidebar-thumb" style="position: relative;">
@if ($recVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $recVideo->thumbnail) }}"
alt="{{ $recVideo->title }}">
@@ -718,7 +795,7 @@
<div class="mobile-bottom-bar">
@auth
<form method="POST"
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}"
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video) : route('videos.like', $video) }}"
class="d-inline" style="flex:1;">
@csrf
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}"
@@ -766,12 +843,22 @@
@auth
<script>
document.addEventListener('DOMContentLoaded', function() {
openEditVideoModal({{ $video->id }});
openEditVideoModal('{{ $video->getRouteKey() }}');
});
</script>
@endauth
@endif
@if(session('_auto_dl_video') && $video->isAudioOnly())
<script>
document.addEventListener('DOMContentLoaded', function () {
if (typeof startSlideshowDownload === 'function') {
startSlideshowDownload('{{ $video->getRouteKey() }}');
}
});
</script>
@endif
<script>
document.addEventListener('DOMContentLoaded', function() {
var videoPlayer = document.getElementById('videoPlayer');
@@ -779,12 +866,23 @@
videoPlayer.volume = 0.5;
var playPromise = videoPlayer.play();
if (playPromise !== undefined) {
playPromise.then(function() {
console.log('Video autoplayed');
}).catch(function(error) {
console.log('Autoplay blocked');
});
playPromise.then(function() {}).catch(function() {});
}
@if($nextVideo && $playlist)
// Auto-advance to next playlist video when this one ends
videoPlayer.addEventListener('ended', function() {
window.location.href = '{{ route('videos.show', $nextVideo) }}?playlist={{ $playlist->share_token }}';
});
@endif
}
// Scroll the currently playing video into view in the playlist sidebar
var currentCard = document.querySelector('.sidebar-video-card.current-video');
if (currentCard) {
setTimeout(function() {
currentCard.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}, 400);
}
});
</script>