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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user