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
@@ -26,14 +26,21 @@ $sizeClasses = match($size) {
|
||||
@endphp
|
||||
|
||||
<div class="yt-video-card {{ $sizeClasses }}" data-video-url="{{ $videoUrl }}">
|
||||
<a href="{{ $video ? route('videos.show', $video->id) : '#' }}">
|
||||
<div class="yt-video-thumb" onmouseenter="playVideo(this)" onmouseleave="stopVideo(this)">
|
||||
<a href="{{ $video ? route('videos.show', $video) : '#' }}">
|
||||
<div class="yt-video-thumb" onmouseenter="playVideo(this)" onmouseleave="stopVideo(this)"
|
||||
data-audio="{{ $video && $video->isAudioOnly() ? 'true' : 'false' }}">
|
||||
<img src="{{ $thumbnailUrl }}" alt="{{ $video->title ?? 'Video' }}">
|
||||
@if($videoUrl)
|
||||
<video preload="none">
|
||||
<source src="{{ $videoUrl }}" type="{{ $video->mime_type ?? 'video/mp4' }}">
|
||||
</video>
|
||||
@endif
|
||||
{{-- Equalizer overlay shown when audio-only track is previewing --}}
|
||||
<div class="audio-preview-overlay">
|
||||
<div class="audio-eq">
|
||||
<span></span><span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
@if($video && $video->duration)
|
||||
<span class="yt-video-duration">{{ gmdate('i:s', $video->duration) }}</span>
|
||||
@endif
|
||||
@@ -42,27 +49,44 @@ $sizeClasses = match($size) {
|
||||
<i class="bi bi-collection-play-fill"></i> SHORTS
|
||||
</span>
|
||||
@endif
|
||||
@if($isOwner && $video->visibility === 'private')
|
||||
<span class="yt-visibility-badge yt-visibility-private">
|
||||
<i class="bi bi-lock-fill"></i> Private
|
||||
</span>
|
||||
@elseif($isOwner && $video->visibility === 'unlisted')
|
||||
<span class="yt-visibility-badge yt-visibility-unlisted">
|
||||
<i class="bi bi-link-45deg"></i> Unlisted
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
<div class="yt-video-info">
|
||||
<div class="yt-channel-icon">
|
||||
<a href="{{ $video && $video->user ? route('channel', $video->user->channel) : '#' }}"
|
||||
class="yt-channel-icon" onclick="event.stopPropagation();">
|
||||
@if($video && $video->user && $video->user->avatar_url)
|
||||
<img src="{{ $video->user->avatar_url }}" alt="{{ $video->user->name }}" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%;">
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
<div class="yt-video-details">
|
||||
<h3 class="yt-video-title">
|
||||
<a href="{{ $video ? route('videos.show', $video->id) : '#' }}">
|
||||
<i class="bi {{ $typeIcon }}" style="color: #ef4444; margin-right: 6px;"></i>
|
||||
<a href="{{ $video ? route('videos.show', $video) : '#' }}">
|
||||
{{ $video->title ?? 'Untitled Video' }}
|
||||
</a>
|
||||
</h3>
|
||||
@if($video && $video->user)
|
||||
<div class="yt-channel-name">{{ $video->user->name }}</div>
|
||||
<a href="{{ route('channel', $video->user->channel) }}"
|
||||
class="yt-channel-name" onclick="event.stopPropagation();">{{ $video->user->name }}</a>
|
||||
@endif
|
||||
@if($video)
|
||||
<div class="yt-video-meta">
|
||||
{{ number_format($video->view_count) }} views • {{ $video->created_at->diffForHumans() }}
|
||||
@if($video->type && $video->type !== 'generic')
|
||||
<span class="yt-type-label yt-type-{{ $video->type }}">
|
||||
<i class="bi {{ $typeIcon }}"></i>
|
||||
{{ ucfirst($video->type === 'match' ? 'Sports' : $video->type) }}
|
||||
</span>
|
||||
·
|
||||
@endif
|
||||
{{ number_format($video->view_count) }} views · {{ $video->created_at->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -74,47 +98,49 @@ $sizeClasses = match($size) {
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark">
|
||||
@if($isOwner)
|
||||
<li>
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="openEditVideoModal('{{ $video->id }}')">
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="openEditVideoModal('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item text-danger" onclick="showDeleteModal('{{ $video->id }}', '{{ addslashes($video->title) }}')">
|
||||
<button type="button" class="dropdown-item text-danger" onclick="showDeleteModal('{{ $video->getRouteKey() }}', {{ json_encode($video->title) }})">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
@endif
|
||||
<li>
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="addToQueue('{{ $video->id }}')">
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="addToQueue('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-list-nested"></i> Add to queue
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="saveToWatchLater('{{ $video->id }}')">
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="saveToWatchLater('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-clock"></i> Save to Watch later
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="openPlaylistModal('{{ $video->id }}')">
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="openPlaylistModal('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-bookmark"></i> Save to playlist
|
||||
</a>
|
||||
</li>
|
||||
@if($video->allow_download)
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('videos.download', $video->id) }}">
|
||||
<a class="dropdown-item" href="{{ route('videos.download', $video) }}">
|
||||
<i class="bi bi-download"></i> Download
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if($video->isShareable())
|
||||
<li>
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="notInterested('{{ $video->id }}')">
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="notInterested('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-dash-circle"></i> Not interested
|
||||
</a>
|
||||
</li>
|
||||
@@ -124,7 +150,7 @@ $sizeClasses = match($size) {
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="reportVideo('{{ $video->id }}')">
|
||||
<a class="dropdown-item" href="javascript:void(0)" onclick="reportVideo('{{ $video->getRouteKey() }}')">
|
||||
<i class="bi bi-flag"></i> Report
|
||||
</a>
|
||||
</li>
|
||||
@@ -134,30 +160,31 @@ $sizeClasses = match($size) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php $vk = $video->getRouteKey() ?? ''; @endphp
|
||||
<!-- Cute Edit Video Modal -->
|
||||
<div class="modal fade" id="editVideoModal{{ $video->id ?? '' }}" tabindex="-1" aria-labelledby="editVideoModalLabel{{ $video->id ?? '' }}" aria-hidden="true">
|
||||
<div class="modal fade" id="editVideoModal{{ $vk }}" tabindex="-1" aria-labelledby="editVideoModalLabel{{ $vk }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered cute-edit-modal">
|
||||
<div class="modal-content cute-edit-content">
|
||||
<div class="cute-edit-header">
|
||||
<span class="cute-edit-icon">✏️</span>
|
||||
<h5>Edit Video</h5>
|
||||
<button type="button" class="btn-close-cute" onclick="closeEditVideoModal('{{ $video->id ?? '' }}')">×</button>
|
||||
<button type="button" class="btn-close-cute" onclick="closeEditVideoModal('{{ $vk }}')">×</button>
|
||||
</div>
|
||||
<div class="cute-edit-body">
|
||||
<form id="edit-video-form-{{ $video->id ?? '' }}" enctype="multipart/form-data">
|
||||
<form id="edit-video-form-{{ $vk }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Title -->
|
||||
<div class="cute-form-group">
|
||||
<label><i class="bi bi-card-heading"></i> Title</label>
|
||||
<input type="text" name="title" id="edit-title-{{ $video->id ?? '' }}" class="cute-input" placeholder="Video title">
|
||||
<input type="text" name="title" id="edit-title-{{ $vk }}" class="cute-input" placeholder="Video title">
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="cute-form-group">
|
||||
<label><i class="bi bi-text-paragraph"></i> Description</label>
|
||||
<textarea name="description" id="edit-description-{{ $video->id ?? '' }}" class="cute-textarea" rows="2" placeholder="Tell viewers about your video"></textarea>
|
||||
<textarea name="description" id="edit-description-{{ $vk }}" class="cute-textarea" rows="2" placeholder="Tell viewers about your video"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Video Type -->
|
||||
@@ -183,7 +210,7 @@ $sizeClasses = match($size) {
|
||||
<div class="cute-form-group">
|
||||
<label><i class="bi bi-lightning-charge-fill"></i> Shorts</label>
|
||||
<label class="cute-shorts-toggle">
|
||||
<input type="checkbox" name="is_shorts" id="edit-is-shorts-{{ $video->id ?? '' }}" value="1">
|
||||
<input type="checkbox" name="is_shorts" id="edit-is-shorts-{{ $vk }}" value="1">
|
||||
<span class="cute-shorts-slider"></span>
|
||||
<span class="cute-shorts-label">Mark as Short</span>
|
||||
</label>
|
||||
@@ -192,9 +219,9 @@ $sizeClasses = match($size) {
|
||||
<!-- Thumbnail -->
|
||||
<div class="cute-form-group">
|
||||
<label><i class="bi bi-image"></i> Thumbnail</label>
|
||||
<div class="cute-thumbnail-upload" onclick="document.getElementById('edit-thumbnail-{{ $video->id ?? '' }}').click()">
|
||||
<input type="file" name="thumbnail" id="edit-thumbnail-{{ $video->id ?? '' }}" accept="image/*" hidden>
|
||||
<div class="cute-thumbnail-preview" id="thumbnail-preview-{{ $video->id ?? '' }}">
|
||||
<div class="cute-thumbnail-upload" onclick="document.getElementById('edit-thumbnail-{{ $vk }}').click()">
|
||||
<input type="file" name="thumbnail" id="edit-thumbnail-{{ $vk }}" accept="image/*" hidden>
|
||||
<div class="cute-thumbnail-preview" id="thumbnail-preview-{{ $vk }}">
|
||||
<i class="bi bi-camera"></i>
|
||||
<span>Click to change</span>
|
||||
</div>
|
||||
@@ -221,11 +248,11 @@ $sizeClasses = match($size) {
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="cute-status" id="edit-status-{{ $video->id ?? '' }}"></div>
|
||||
<div class="cute-status" id="edit-status-{{ $vk }}"></div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="cute-edit-actions">
|
||||
<button type="button" class="cute-btn-cancel" onclick="closeEditVideoModal('{{ $video->id ?? '' }}')">Cancel</button>
|
||||
<button type="button" class="cute-btn-cancel" onclick="closeEditVideoModal('{{ $vk }}')">Cancel</button>
|
||||
<button type="submit" class="cute-btn-save">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -255,6 +282,10 @@ $sizeClasses = match($size) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.yt-video-card:hover .yt-video-thumb img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.yt-video-card .yt-video-thumb video {
|
||||
@@ -273,6 +304,52 @@ $sizeClasses = match($size) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Audio preview overlay (equalizer) */
|
||||
.audio-preview-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.yt-video-thumb.audio-playing .audio-preview-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.audio-eq {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
height: 24px;
|
||||
background: rgba(0,0,0,0.55);
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.audio-eq span {
|
||||
display: block;
|
||||
width: 3px;
|
||||
border-radius: 2px;
|
||||
background: #e61e1e;
|
||||
animation: eq-bar 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.audio-eq span:nth-child(1) { height: 6px; animation-delay: 0s; }
|
||||
.audio-eq span:nth-child(2) { height: 14px; animation-delay: 0.15s; }
|
||||
.audio-eq span:nth-child(3) { height: 20px; animation-delay: 0.05s; }
|
||||
.audio-eq span:nth-child(4) { height: 12px; animation-delay: 0.2s; }
|
||||
.audio-eq span:nth-child(5) { height: 8px; animation-delay: 0.1s; }
|
||||
|
||||
@keyframes eq-bar {
|
||||
0%, 100% { transform: scaleY(0.4); }
|
||||
50% { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
.yt-video-card .yt-video-duration {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
@@ -306,6 +383,31 @@ $sizeClasses = match($size) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-visibility-badge {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
padding: 3px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-visibility-private {
|
||||
background: rgba(220, 38, 38, 0.88);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-visibility-unlisted {
|
||||
background: rgba(30, 30, 30, 0.82);
|
||||
color: #facc15;
|
||||
border: 1px solid rgba(250, 204, 21, 0.4);
|
||||
}
|
||||
|
||||
.yt-video-card .yt-video-info {
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
@@ -343,6 +445,11 @@ $sizeClasses = match($size) {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-channel-icon {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.yt-video-card .yt-channel-name,
|
||||
.yt-video-card .yt-video-meta {
|
||||
color: #aaa;
|
||||
@@ -352,7 +459,23 @@ $sizeClasses = match($size) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* More button */
|
||||
.yt-video-card a.yt-channel-name {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
.yt-video-card a.yt-channel-name:hover { color: #fff; }
|
||||
|
||||
/* Type label in meta row */
|
||||
.yt-type-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
.yt-type-music { color: #c084fc; }
|
||||
.yt-type-match { color: #60a5fa; }
|
||||
|
||||
/* More button — visible only on hover (touch devices always show it) */
|
||||
.yt-video-card .yt-more-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -365,10 +488,15 @@ $sizeClasses = match($size) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.2s, opacity 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.yt-video-card .yt-more-btn { opacity: 0; }
|
||||
.yt-video-card:hover .yt-more-btn,
|
||||
.yt-video-card .yt-more-btn:focus,
|
||||
.yt-video-card .yt-more-btn[aria-expanded="true"] { opacity: 1; }
|
||||
}
|
||||
.yt-video-card .yt-more-btn:hover {
|
||||
background: #3f3f3f;
|
||||
}
|
||||
@@ -746,6 +874,7 @@ $sizeClasses = match($size) {
|
||||
}
|
||||
</style>
|
||||
|
||||
@once
|
||||
<script>
|
||||
// Global function to save to Watch Later
|
||||
function saveToWatchLater(videoId) {
|
||||
@@ -760,7 +889,7 @@ function saveToWatchLater(videoId) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message || 'Added to Watch Later');
|
||||
showToast(data.message || 'Added to Watch Later', 'success');
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
@@ -801,26 +930,32 @@ function openPlaylistModal(videoId) {
|
||||
|
||||
// Global function to add to queue
|
||||
function addToQueue(videoId) {
|
||||
alert('Queue feature coming soon!');
|
||||
showToast('Queue feature coming soon!', 'info');
|
||||
}
|
||||
|
||||
function playVideo(element) {
|
||||
const video = element.querySelector('video');
|
||||
if (video) {
|
||||
video.currentTime = 0;
|
||||
video.volume = 0.10;
|
||||
video.play().catch(function(e) {});
|
||||
if (!video) return;
|
||||
video.currentTime = 0;
|
||||
const isAudio = element.dataset.audio === 'true';
|
||||
video.volume = isAudio ? 0.4 : 0.10;
|
||||
if (isAudio) {
|
||||
// Keep thumbnail visible — just play audio, show equalizer
|
||||
video.play().catch(function() {});
|
||||
element.classList.add('audio-playing');
|
||||
} else {
|
||||
video.play().catch(function() {});
|
||||
video.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function stopVideo(element) {
|
||||
const video = element.querySelector('video');
|
||||
if (video) {
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
video.classList.remove('active');
|
||||
}
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
video.classList.remove('active');
|
||||
element.classList.remove('audio-playing');
|
||||
}
|
||||
|
||||
// Edit Modal Functions
|
||||
@@ -967,3 +1102,4 @@ document.addEventListener('submit', function(e) {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endonce
|
||||
Reference in new issue
Block a user