latest update

This commit is contained in:
ghassan committed 2026-03-11 11:21:33 +03:00
1 parent 9ad842dcd5
commit 062c0e896f
42 files changed
+7052 -2921

No files matched your search

File diff suppressed because it is too large. Load diff
+353
View File
@@ -0,0 +1,353 @@
@extends('layouts.app')
@section('title', 'Shorts - ' . config('app.name'))
@section('content')
<div class="yt-main">
<div class="yt-content">
<!-- Shorts Header -->
<div class="shorts-header">
<div class="shorts-brand">
<div class="shorts-logo-icon">
<i class="bi bi-play-fill"></i>
</div>
<h1>Shorts</h1>
</div>
</div>
<!-- Shorts Grid - YouTube Style -->
<div class="shorts-container">
@forelse($videos as $video)
<div class="shorts-card">
<a href="{{ route('videos.show', $video->id) }}" class="shorts-link">
<div class="shorts-thumbnail-wrapper">
<img src="{{ $video->thumbnail_url }}" alt="{{ $video->title }}" class="shorts-thumb">
<div class="shorts-overlay">
<span class="shorts-views">
<i class="bi bi-play-fill"></i> {{ number_format($video->view_count) }}
</span>
</div>
@if($video->duration)
<span class="shorts-time">{{ gmdate('i:s', $video->duration) }}</span>
@endif
</div>
<div class="shorts-details">
<h3 class="shorts-title">{{ $video->title }}</h3>
<div class="shorts-channel-info">
@if($video->user)
<div class="shorts-channel-row">
@if($video->user->avatar_url)
<img src="{{ $video->user->avatar_url }}" class="shorts-avatar" alt="{{ $video->user->name }}">
@else
<div class="shorts-avatar-placeholder">{{ substr($video->user->name, 0, 1) }}</div>
@endif
<span class="shorts-channel-name">{{ $video->user->name }}</span>
</div>
@endif
<span class="shorts-ago">{{ $video->created_at->diffForHumans() }}</span>
</div>
</div>
</a>
</div>
@empty
<div class="shorts-empty-state">
<div class="shorts-empty-icon">
<i class="bi bi-collection-play"></i>
</div>
<h2>No Shorts yet</h2>
<p>Be the first to upload a Short!</p>
@auth
<a href="{{ route('videos.create') }}" class="shorts-upload-btn">
<i class="bi bi-plus-lg"></i> Upload Short
</a>
@else
<a href="{{ route('login') }}" class="shorts-upload-btn">
<i class="bi bi-box-arrow-in-right"></i> Login to Upload
</a>
@endauth
</div>
@endforelse
</div>
<!-- Pagination -->
@if($videos->hasPages())
<div class="shorts-pagination">
{{ $videos->links() }}
</div>
@endif
</div>
</div>
<style>
/* Shorts Page - YouTube Shorts Style */
.shorts-header {
margin-bottom: 20px;
padding: 12px 0;
}
.shorts-brand {
display: flex;
align-items: center;
gap: 12px;
}
.shorts-logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #ff0050, #ff6b6b);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
.shorts-brand h1 {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
/* Shorts Grid - 4 columns on desktop */
.shorts-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
/* Shorts Card */
.shorts-card {
background: transparent;
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s;
}
.shorts-card:hover {
transform: scale(1.02);
}
.shorts-link {
text-decoration: none;
color: inherit;
display: block;
}
.shorts-thumbnail-wrapper {
position: relative;
aspect-ratio: 9/16;
border-radius: 12px;
overflow: hidden;
background: #1a1a1a;
}
.shorts-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
.shorts-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
opacity: 0;
transition: opacity 0.2s;
}
.shorts-card:hover .shorts-overlay {
opacity: 1;
}
.shorts-views {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 500;
color: white;
}
.shorts-time {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0,0,0,0.8);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.shorts-details {
padding: 10px 4px;
}
.shorts-title {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.3;
}
.shorts-channel-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.shorts-channel-row {
display: flex;
align-items: center;
gap: 8px;
}
.shorts-avatar, .shorts-avatar-placeholder {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.shorts-avatar-placeholder {
background: #666;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
}
.shorts-channel-name {
font-size: 12px;
color: var(--text-secondary);
}
.shorts-ago {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.7;
}
/* Empty State */
.shorts-empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
background: var(--bg-secondary);
border-radius: 16px;
}
.shorts-empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: linear-gradient(135deg, #ff0050, #ff6b6b);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.shorts-empty-icon i {
font-size: 36px;
color: white;
}
.shorts-empty-state h2 {
font-size: 20px;
color: var(--text-primary);
margin: 0 0 8px;
}
.shorts-empty-state p {
color: var(--text-secondary);
margin: 0 0 20px;
}
.shorts-upload-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #ff0050;
color: white;
border-radius: 20px;
text-decoration: none;
font-weight: 500;
font-size: 14px;
transition: background 0.2s;
}
.shorts-upload-btn:hover {
background: #e60048;
}
/* Pagination */
.shorts-pagination {
margin-top: 30px;
display: flex;
justify-content: center;
}
.shorts-pagination .pagination {
gap: 4px;
}
.shorts-pagination .page-link {
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-primary);
padding: 6px 12px;
font-size: 13px;
}
.shorts-pagination .page-item.active .page-link {
background: #ff0050;
border-color: #ff0050;
}
/* Responsive */
@media (max-width: 1200px) {
.shorts-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.shorts-container {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
}
@media (max-width: 600px) {
.shorts-container {
grid-template-columns: 1fr;
max-width: 320px;
margin: 0 auto;
}
.shorts-header {
text-align: center;
}
.shorts-brand {
justify-content: center;
}
}
</style>
@endsection
File diff suppressed because it is too large. Load diff
+6 -23
View File
@@ -177,13 +177,13 @@
<div class="trending-header">
<h1><i class="bi bi-fire trending-icon"></i> Trending</h1>
<div class="trending-filters">
<a href="{{ route('videos.trending', ['hours' => 24]) }}" class="{{ == 24 ? 'active' : '' }}">Today</a>
<a href="{{ route('videos.trending', ['hours' => 48]) }}" class="{{ == 48 ? 'active' : '' }}">This Week</a>
<a href="{{ route('videos.trending', ['hours' => 168]) }}" class="{{ == 168 ? 'active' : '' }}">This Month</a>
<a href="{{ route('videos.trending', ['hours' => 24]) }}" class="{{ $hours == 24 ? 'active' : '' }}">Today</a>
<a href="{{ route('videos.trending', ['hours' => 48]) }}" class="{{ $hours == 48 ? 'active' : '' }}">This Week</a>
<a href="{{ route('videos.trending', ['hours' => 168]) }}" class="{{ $hours == 168 ? 'active' : '' }}">This Month</a>
</div>
</div>
@if(())
@if($videos->isEmpty())
<div class="empty-trending">
<i class="bi bi-play-circle"></i>
<h3>No trending videos yet</h3>
@@ -191,25 +191,8 @@
</div>
@else
<div class="yt-video-grid">
@foreach( as )
<a href="{{ route('videos.show', ) }}" class="yt-video-card">
<div class="yt-video-thumb">
<img src="{{ }}" alt="{{ }}" loading="lazy">
<span class="trending-badge"><i class="bi bi-fire"></i> Trending</span>
@if()<span class="yt-video-duration">{{ gmdate('i:s', ) }}</span>@endif
</div>
<div class="yt-video-meta">
@if()<img src="{{ ->avatar_url }}" alt="{{ ->name }}" class="yt-video-avatar">@endif
<div class="yt-video-info">
<h3 class="yt-video-title">{{ }}</h3>
<div class="yt-video-channel"><a href="#">{{ ->name ?? 'Unknown' }}</a></div>
<div class="yt-video-stats">
{{ number_format() }} views
@if( > 0) • {{ number_format() }} likes @endif
</div>
</div>
</div>
</a>
@foreach($videos as $video)
<x-video-card :video="$video" />
@endforeach
</div>
@endif
File diff suppressed because it is too large. Load diff
+354 -66
View File
@@ -6,7 +6,7 @@
<style>
/* Video Section */
.yt-video-section { flex: 1; min-width: 0; }
/* Video Player */
.video-container {
position: relative;
@@ -20,20 +20,122 @@
align-items: center;
width: 100%;
}
.video-container.portrait,
.video-container.square,
.video-container.portrait,
.video-container.square,
.video-container.ultrawide {
margin: 0 auto;
width: auto;
}
.video-container.portrait { aspect-ratio: 9/16; max-width: 50vh; }
.video-container.square { aspect-ratio: 1/1; max-width: 70vh; }
.video-container.ultrawide { aspect-ratio: 21/9; max-width: 100%; }
.video-container video { width: 100%; height: 100%; object-fit: contain; }
/* Playlist Navigation Controls */
.playlist-controls {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
background: rgba(0, 0, 0, 0.8);
padding: 8px 16px;
border-radius: 24px;
z-index: 10;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-container:hover .playlist-controls,
.playlist-controls.visible { opacity: 1; }
.playlist-nav-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.playlist-nav-btn:hover { background: rgba(255, 255, 255, 0.2); }
.playlist-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.playlist-nav-btn:disabled:hover { background: transparent; }
.playlist-nav-btn i { font-size: 20px; }
.playlist-nav-label {
font-size: 12px;
color: #aaa;
max-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Autoplay Toggle */
.autoplay-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
.autoplay-toggle label {
font-size: 12px;
color: #aaa;
cursor: pointer;
white-space: nowrap;
}
.autoplay-switch { position: relative; width: 36px; height: 20px; }
.autoplay-switch input { opacity: 0; width: 0; height: 0; }
.autoplay-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #666;
transition: 0.3s;
border-radius: 20px;
}
.autoplay-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.autoplay-switch input:checked + .autoplay-slider { background-color: var(--brand-red); }
.autoplay-switch input:checked + .autoplay-slider:before { transform: translateX(16px); }
/* Keyboard hint */
.keyboard-hint {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
background: rgba(0, 0, 0, 0.6);
padding: 4px 8px;
border-radius: 4px;
}
/* Video Info */
.video-title {
font-size: 20px;
@@ -41,7 +143,7 @@
margin: 16px 0 8px;
line-height: 1.3;
}
.video-stats-row {
display: flex;
align-items: center;
@@ -51,15 +153,15 @@
flex-wrap: wrap;
gap: 12px;
}
.video-stats-left { display: flex; align-items: center; gap: 16px; color: var(--text-secondary); }
.video-actions {
display: flex;
align-items: center;
gap: 8px;
}
.yt-action-btn {
display: flex;
align-items: center;
@@ -73,11 +175,11 @@
font-size: 14px;
font-weight: 500;
}
.yt-action-btn:hover { background: var(--border-color); }
.yt-action-btn.liked { color: var(--brand-red); }
/* Channel Row */
.channel-row {
display: flex;
@@ -85,27 +187,27 @@
justify-content: space-between;
padding: 16px 0;
}
.channel-info {
display: flex;
align-items: center;
gap: 12px;
}
.channel-avatar {
width: 48px; height: 48px; border-radius: 50%; background: #555;
}
.channel-name {
font-size: 16px;
font-weight: 500;
}
.channel-subs {
font-size: 14px;
color: var(--text-secondary);
}
.subscribe-btn {
background: white;
color: black;
@@ -117,7 +219,7 @@
cursor: pointer;
white-space: nowrap;
}
/* Description */
.video-description {
background: var(--bg-secondary);
@@ -125,26 +227,26 @@
padding: 16px;
margin-top: 16px;
}
.description-text {
white-space: pre-wrap;
font-size: 14px;
line-height: 1.5;
}
/* Sidebar */
.yt-sidebar-container {
width: 400px;
flex-shrink: 0;
}
.sidebar-video-card {
display: flex;
gap: 8px;
margin-bottom: 8px;
cursor: pointer;
}
.sidebar-thumb {
width: 168px;
aspect-ratio: 16/9;
@@ -153,11 +255,11 @@
background: #1a1a1a;
flex-shrink: 0;
}
.sidebar-thumb img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-info { flex: 1; min-width: 0; }
.sidebar-title {
font-size: 14px;
font-weight: 500;
@@ -167,21 +269,20 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
.sidebar-meta { font-size: 12px; color: var(--text-secondary); }
/* Responsive */
@media (max-width: 1300px) {
.yt-sidebar-container { width: 300px; }
}
@media (max-width: 991px) {
.yt-main { margin-left: 0; flex-direction: column; }
.yt-sidebar-container { width: 100%; }
.yt-header-center { display: none; }
.sidebar-video-card { flex-direction: column; }
.sidebar-thumb { width: 100%; }
.video-layout-container {
flex-direction: column !important;
}
@@ -194,12 +295,12 @@
margin-top: 16px;
}
}
@media (max-width: 576px) {
.video-stats-row { flex-direction: column; align-items: flex-start; }
.video-actions { width: 100%; overflow-x: auto; justify-content: flex-start; }
.yt-main { padding: 12px !important; }
.video-container {
max-height: 50vh !important;
border-radius: 0 !important;
@@ -241,14 +342,55 @@
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
</video>
<!-- Playlist Navigation Controls (only show when in playlist) -->
@if($nextVideo || $previousVideo)
<div class="playlist-controls" id="playlistControls">
<!-- Previous Video -->
@if($previousVideo)
<button class="playlist-nav-btn" onclick="goToPreviousVideo()" title="Previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
<span class="playlist-nav-label">{{ Str::limit($previousVideo->title, 20) }}</span>
@else
<button class="playlist-nav-btn" disabled title="No previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
@endif
<!-- Autoplay Toggle -->
<div class="autoplay-toggle">
<label for="autoplayToggle">Autoplay</label>
<label class="autoplay-switch">
<input type="checkbox" id="autoplayToggle" checked>
<span class="autoplay-slider"></span>
</label>
</div>
<!-- Next Video -->
@if($nextVideo)
<span class="playlist-nav-label">{{ Str::limit($nextVideo->title, 20) }}</span>
<button class="playlist-nav-btn" onclick="goToNextVideo()" title="Next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@else
<button class="playlist-nav-btn" disabled title="No next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@endif
</div>
<div class="keyboard-hint">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-right"></i>
</div>
@endif
</div>
<!-- Video Title with Trophy Icon (Match Type) -->
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px; margin: 8px 0 6px;">
<i class="bi bi-trophy" style="color: #ef4444;"></i>
<span>{{ $video->title }}</span>
</h1>
<!-- Stats Row - Hidden, shown in description box -->
<div class="video-stats-row" style="display: none;">
<div class="video-stats-left">
@@ -257,7 +399,7 @@
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
</div>
<!-- Channel Row - All in one line -->
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 0; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
@@ -273,7 +415,7 @@
</div>
</a>
</div>
<div class="video-actions" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
@auth
@if(Auth::id() !== $video->user_id)
@@ -286,7 +428,7 @@
@else
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
@endauth
@auth
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
@csrf
@@ -300,7 +442,7 @@
<i class="bi bi-hand-thumbs-up"></i> Like
</a>
@endauth
@if($video->isShareable())
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
<i class="bi bi-share"></i> Share
@@ -308,7 +450,7 @@
@endif
</div>
</div>
<!-- Description Box -->
@if($video->description)
@php
@@ -316,7 +458,7 @@
$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-bottom: 16px;">
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
<span>{{ number_format($video->view_count) }} views</span>
@@ -341,13 +483,13 @@
@endif
</div>
</div>
<script>
function toggleDescription() {
const descShort = document.getElementById('descShort');
const descFull = document.getElementById('descFull');
const toggleBtn = document.getElementById('descToggleBtn');
if (descShort.style.display !== 'none') {
descShort.style.display = 'none';
descFull.style.display = 'block';
@@ -359,27 +501,27 @@
}
}
</script>
<style>
.video-description-box .description-text { font-size: 14px; line-height: 1.5; color: var(--text-primary); }
.video-description-box .description-text p { margin-bottom: 8px; }
.video-description-box .description-text a { color: #3ea6ff; }
</style>
@endif
<!-- Comment Section -->
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
</h3>
@auth
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
<div style="flex: 1;">
<textarea
id="commentBody"
class="form-control"
<textarea
id="commentBody"
class="form-control"
placeholder="Add a comment... Use @ to mention someone"
rows="3"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"
@@ -395,7 +537,7 @@
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
</div>
@endauth
<div id="commentsList">
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
@include('videos.partials.comment', ['comment' => $comment])
@@ -404,12 +546,12 @@
@endforelse
</div>
</div>
<script>
function submitComment(videoId) {
const body = document.getElementById('commentBody').value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -426,10 +568,10 @@
}
});
}
function deleteComment(commentId) {
if (!confirm('Are you sure you want to delete this comment?')) return;
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
@@ -443,7 +585,7 @@
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const commentTexts = document.querySelectorAll('.comment-body');
commentTexts.forEach(text => {
@@ -451,12 +593,12 @@
text.innerHTML = html;
});
});
function submitReply(videoId, parentId) {
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
const body = textarea.value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -474,18 +616,99 @@
}
</script>
</div>
<!-- Sidebar -->
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
<div class="text-secondary">More videos coming soon...</div>
@if($playlist && $playlistVideos && $playlistVideos->count() > 0)
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<i class="bi bi-collection-play" style="margin-right: 8px;"></i>
{{ $playlist->name }}
<span style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }} videos)</span>
</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 }}'">
<div class="sidebar-thumb" style="position: relative;">
@if ($playlistVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $playlistVideo->thumbnail) }}" alt="{{ $playlistVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $playlistVideo->id }}/320/180" alt="{{ $playlistVideo->title }}">
@endif
@if ($playlistVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $playlistVideo->duration) }}</span>
@endif
@if ($playlistVideo->is_shorts)
<span class="yt-shorts-badge" style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;">
<i class="bi bi-collection-play-fill"></i> SHORTS
</span>
@endif
<!-- Playlist position indicator -->
<span style="position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: 500;">
{{ $index + 1 }}
</span>
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match ($playlistVideo->type) {'music' => 'bi-music-note','match' => 'bi-trophy',default => 'bi-film'} }}"
style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ 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>
</div>
</div>
</div>
@endif
@endforeach
</div>
@if($playlist->canEdit(Auth::user()))
<a href="{{ route('playlists.show', $playlist->id) }}" class="yt-action-btn" style="margin-top: 12px; display: inline-block;">
<i class="bi bi-pencil"></i> Edit Playlist
</a>
@endif
@else
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
@if($recommendedVideos && $recommendedVideos->count() > 0)
<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">
@if($recVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $recVideo->thumbnail) }}" alt="{{ $recVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $recVideo->id }}/320/180" alt="{{ $recVideo->title }}">
@endif
@if($recVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $recVideo->duration) }}</span>
@endif
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match($recVideo->type) { 'music' => 'bi-music-note', 'match' => 'bi-trophy', default => 'bi-film' } }}" style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ Str::limit($recVideo->title, 60) }}
</div>
<div class="sidebar-meta">
<div>{{ $recVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($recVideo->view_count) }} views • {{ $recVideo->created_at->diffForHumans() }}</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-secondary">More videos coming soon...</div>
@endif
@endif
</div>
</div>
@include('layouts.partials.share-modal')
@include('layouts.partials.edit-video-modal')
@if(Session::has('openEditModal') && Session::get('openEditModal'))
@auth
<script>
@@ -495,17 +718,82 @@
</script>
@endauth
@endif
<script>
// Playlist navigation URLs
const previousVideoUrl = @json($previousVideo ? route('videos.show', $previousVideo->id) . '?playlist=' . $playlist->id : null);
const nextVideoUrl = @json($nextVideo ? route('videos.show', $nextVideo->id) . '?playlist=' . $playlist->id : null);
// Autoplay management
let autoplayEnabled = true;
document.addEventListener('DOMContentLoaded', function() {
var videoPlayer = document.getElementById('videoPlayer');
var playlistControls = document.getElementById('playlistControls');
var autoplayToggle = document.getElementById('autoplayToggle');
if (videoPlayer) {
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'); });
}
// Handle video ended - autoplay next
if (nextVideoUrl) {
videoPlayer.addEventListener('ended', function() {
if (autoplayEnabled) {
console.log('Video ended, going to next...');
window.location.href = nextVideoUrl;
}
});
}
}
// Show playlist controls on hover
if (playlistControls) {
const videoContainer = document.getElementById('videoContainer');
videoContainer.addEventListener('mouseenter', function() {
playlistControls.classList.add('visible');
});
videoContainer.addEventListener('mouseleave', function() {
playlistControls.classList.remove('visible');
});
// Initially show for a few seconds
setTimeout(function() { playlistControls.classList.add('visible'); }, 2000);
setTimeout(function() { playlistControls.classList.remove('visible'); }, 6000);
}
// Autoplay toggle handler
if (autoplayToggle) {
autoplayToggle.addEventListener('change', function() {
autoplayEnabled = this.checked;
localStorage.setItem('playlistAutoplay', autoplayEnabled ? '1' : '0');
});
// Load saved preference
const savedAutoplay = localStorage.getItem('playlistAutoplay');
if (savedAutoplay !== null) {
autoplayEnabled = savedAutoplay === '1';
autoplayToggle.checked = autoplayEnabled;
}
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; }
if (e.key === 'ArrowLeft') { e.preventDefault(); goToPreviousVideo(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); goToNextVideo(); }
});
});
function goToPreviousVideo() {
if (previousVideoUrl) { window.location.href = previousVideoUrl; }
}
function goToNextVideo() {
if (nextVideoUrl) { window.location.href = nextVideoUrl; }
}
</script>
@endsection
+359 -66
View File
@@ -6,7 +6,7 @@
<style>
/* Video Section */
.yt-video-section { flex: 1; min-width: 0; }
/* Video Player */
.video-container {
position: relative;
@@ -20,20 +20,122 @@
align-items: center;
width: 100%;
}
.video-container.portrait,
.video-container.square,
.video-container.portrait,
.video-container.square,
.video-container.ultrawide {
margin: 0 auto;
width: auto;
}
.video-container.portrait { aspect-ratio: 9/16; max-width: 50vh; }
.video-container.square { aspect-ratio: 1/1; max-width: 70vh; }
.video-container.ultrawide { aspect-ratio: 21/9; max-width: 100%; }
.video-container video { width: 100%; height: 100%; object-fit: contain; }
/* Playlist Navigation Controls */
.playlist-controls {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
background: rgba(0, 0, 0, 0.8);
padding: 8px 16px;
border-radius: 24px;
z-index: 10;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-container:hover .playlist-controls,
.playlist-controls.visible { opacity: 1; }
.playlist-nav-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.playlist-nav-btn:hover { background: rgba(255, 255, 255, 0.2); }
.playlist-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.playlist-nav-btn:disabled:hover { background: transparent; }
.playlist-nav-btn i { font-size: 20px; }
.playlist-nav-label {
font-size: 12px;
color: #aaa;
max-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Autoplay Toggle */
.autoplay-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
.autoplay-toggle label {
font-size: 12px;
color: #aaa;
cursor: pointer;
white-space: nowrap;
}
.autoplay-switch { position: relative; width: 36px; height: 20px; }
.autoplay-switch input { opacity: 0; width: 0; height: 0; }
.autoplay-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #666;
transition: 0.3s;
border-radius: 20px;
}
.autoplay-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.autoplay-switch input:checked + .autoplay-slider { background-color: var(--brand-red); }
.autoplay-switch input:checked + .autoplay-slider:before { transform: translateX(16px); }
/* Keyboard hint */
.keyboard-hint {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
background: rgba(0, 0, 0, 0.6);
padding: 4px 8px;
border-radius: 4px;
}
/* Video Info */
.video-title {
font-size: 20px;
@@ -41,7 +143,7 @@
margin: 16px 0 8px;
line-height: 1.3;
}
.video-stats-row {
display: flex;
align-items: center;
@@ -51,15 +153,15 @@
flex-wrap: wrap;
gap: 12px;
}
.video-stats-left { display: flex; align-items: center; gap: 16px; color: var(--text-secondary); }
.video-actions {
display: flex;
align-items: center;
gap: 8px;
}
.yt-action-btn {
display: flex;
align-items: center;
@@ -73,11 +175,11 @@
font-size: 14px;
font-weight: 500;
}
.yt-action-btn:hover { background: var(--border-color); }
.yt-action-btn.liked { color: var(--brand-red); }
/* Channel Row */
.channel-row {
display: flex;
@@ -85,27 +187,27 @@
justify-content: space-between;
padding: 16px 0;
}
.channel-info {
display: flex;
align-items: center;
gap: 12px;
}
.channel-avatar {
width: 48px; height: 48px; border-radius: 50%; background: #555;
}
.channel-name {
font-size: 16px;
font-weight: 500;
}
.channel-subs {
font-size: 14px;
color: var(--text-secondary);
}
.subscribe-btn {
background: white;
color: black;
@@ -117,7 +219,7 @@
cursor: pointer;
white-space: nowrap;
}
/* Description */
.video-description {
background: var(--bg-secondary);
@@ -125,26 +227,26 @@
padding: 16px;
margin-top: 16px;
}
.description-text {
white-space: pre-wrap;
font-size: 14px;
line-height: 1.5;
}
/* Sidebar */
.yt-sidebar-container {
width: 400px;
flex-shrink: 0;
}
.sidebar-video-card {
display: flex;
gap: 8px;
margin-bottom: 8px;
cursor: pointer;
}
.sidebar-thumb {
width: 168px;
aspect-ratio: 16/9;
@@ -153,11 +255,11 @@
background: #1a1a1a;
flex-shrink: 0;
}
.sidebar-thumb img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-info { flex: 1; min-width: 0; }
.sidebar-title {
font-size: 14px;
font-weight: 500;
@@ -167,21 +269,20 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
.sidebar-meta { font-size: 12px; color: var(--text-secondary); }
/* Responsive */
@media (max-width: 1300px) {
.yt-sidebar-container { width: 300px; }
}
@media (max-width: 991px) {
.yt-main { margin-left: 0; flex-direction: column; }
.yt-sidebar-container { width: 100%; }
.yt-header-center { display: none; }
.sidebar-video-card { flex-direction: column; }
.sidebar-thumb { width: 100%; }
.video-layout-container {
flex-direction: column !important;
}
@@ -194,12 +295,12 @@
margin-top: 16px;
}
}
@media (max-width: 576px) {
.video-stats-row { flex-direction: column; align-items: flex-start; }
.video-actions { width: 100%; overflow-x: auto; justify-content: flex-start; }
.yt-main { padding: 12px !important; }
.video-container {
max-height: 50vh !important;
border-radius: 0 !important;
@@ -241,14 +342,55 @@
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
</video>
<!-- Playlist Navigation Controls (only show when in playlist) -->
@if($nextVideo || $previousVideo)
<div class="playlist-controls" id="playlistControls">
<!-- Previous Video -->
@if($previousVideo)
<button class="playlist-nav-btn" onclick="goToPreviousVideo()" title="Previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
<span class="playlist-nav-label">{{ Str::limit($previousVideo->title, 20) }}</span>
@else
<button class="playlist-nav-btn" disabled title="No previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
@endif
<!-- Autoplay Toggle -->
<div class="autoplay-toggle">
<label for="autoplayToggle">Autoplay</label>
<label class="autoplay-switch">
<input type="checkbox" id="autoplayToggle" checked>
<span class="autoplay-slider"></span>
</label>
</div>
<!-- Next Video -->
@if($nextVideo)
<span class="playlist-nav-label">{{ Str::limit($nextVideo->title, 20) }}</span>
<button class="playlist-nav-btn" onclick="goToNextVideo()" title="Next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@else
<button class="playlist-nav-btn" disabled title="No next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@endif
</div>
<div class="keyboard-hint">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-right"></i>
</div>
@endif
</div>
<!-- Video Title with Music Note Icon (Music Type) -->
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px; margin: 8px 0 6px;">
<i class="bi bi-music-note" style="color: #ef4444;"></i>
<span>{{ $video->title }}</span>
</h1>
<!-- Stats Row - Hidden, shown in description box -->
<div class="video-stats-row" style="display: none;">
<div class="video-stats-left">
@@ -257,7 +399,7 @@
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
</div>
<!-- Channel Row - All in one line -->
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 0; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
@@ -273,7 +415,7 @@
</div>
</a>
</div>
<div class="video-actions" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
@auth
@if(Auth::id() !== $video->user_id)
@@ -286,7 +428,7 @@
@else
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
@endauth
@auth
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
@csrf
@@ -300,7 +442,7 @@
<i class="bi bi-hand-thumbs-up"></i> Like
</a>
@endauth
@if($video->isShareable())
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
<i class="bi bi-share"></i> Share
@@ -308,7 +450,7 @@
@endif
</div>
</div>
<!-- Description Box -->
@if($video->description)
@php
@@ -316,7 +458,7 @@
$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-bottom: 16px;">
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
<span>{{ number_format($video->view_count) }} views</span>
@@ -341,13 +483,13 @@
@endif
</div>
</div>
<script>
function toggleDescription() {
const descShort = document.getElementById('descShort');
const descFull = document.getElementById('descFull');
const toggleBtn = document.getElementById('descToggleBtn');
if (descShort.style.display !== 'none') {
descShort.style.display = 'none';
descFull.style.display = 'block';
@@ -359,27 +501,27 @@
}
}
</script>
<style>
.video-description-box .description-text { font-size: 14px; line-height: 1.5; color: var(--text-primary); }
.video-description-box .description-text p { margin-bottom: 8px; }
.video-description-box .description-text a { color: #3ea6ff; }
</style>
@endif
<!-- Comment Section -->
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
</h3>
@auth
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
<div style="flex: 1;">
<textarea
id="commentBody"
class="form-control"
<textarea
id="commentBody"
class="form-control"
placeholder="Add a comment... Use @ to mention someone"
rows="3"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"
@@ -395,7 +537,7 @@
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
</div>
@endauth
<div id="commentsList">
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
@include('videos.partials.comment', ['comment' => $comment])
@@ -404,12 +546,12 @@
@endforelse
</div>
</div>
<script>
function submitComment(videoId) {
const body = document.getElementById('commentBody').value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -426,10 +568,10 @@
}
});
}
function deleteComment(commentId) {
if (!confirm('Are you sure you want to delete this comment?')) return;
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
@@ -443,7 +585,7 @@
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const commentTexts = document.querySelectorAll('.comment-body');
commentTexts.forEach(text => {
@@ -451,12 +593,12 @@
text.innerHTML = html;
});
});
function submitReply(videoId, parentId) {
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
const body = textarea.value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -474,18 +616,104 @@
}
</script>
</div>
<!-- Sidebar -->
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
<div class="text-secondary">More videos coming soon...</div>
@if($playlist && $playlistVideos && $playlistVideos->count() > 0)
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<i class="bi bi-collection-play" style="margin-right: 8px;"></i>
{{ $playlist->name }}
<span style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }} videos)</span>
</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 }}'">
<div class="sidebar-thumb" style="position: relative;">
@if ($playlistVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $playlistVideo->thumbnail) }}" alt="{{ $playlistVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $playlistVideo->id }}/320/180" alt="{{ $playlistVideo->title }}">
@endif
@if ($playlistVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $playlistVideo->duration) }}</span>
@endif
@if ($playlistVideo->is_shorts)
<span class="yt-shorts-badge" style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;">
<i class="bi bi-collection-play-fill"></i> SHORTS
</span>
@endif
<!-- Playlist position indicator -->
<span style="position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: 500;">
{{ $index + 1 }}
</span>
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match ($playlistVideo->type) {'music' => 'bi-music-note','match' => 'bi-trophy',default => 'bi-film'} }}"
style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ 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>
</div>
</div>
</div>
@endif
@endforeach
</div>
@if($playlist->canEdit(Auth::user()))
<a href="{{ route('playlists.show', $playlist->id) }}" class="yt-action-btn" style="margin-top: 12px; display: inline-block;">
<i class="bi bi-pencil"></i> Edit Playlist
</a>
@endif
@else
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
@if($recommendedVideos && $recommendedVideos->count() > 0)
<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">
@if($recVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $recVideo->thumbnail) }}" alt="{{ $recVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $recVideo->id }}/320/180" alt="{{ $recVideo->title }}">
@endif
@if($recVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $recVideo->duration) }}</span>
@endif
@if($recVideo->is_shorts)
<span class="yt-shorts-badge" style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;">
<i class="bi bi-collection-play-fill"></i> SHORTS
</span>
@endif
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match($recVideo->type) { 'music' => 'bi-music-note', 'match' => 'bi-trophy', default => 'bi-film' } }}" style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ Str::limit($recVideo->title, 60) }}
</div>
<div class="sidebar-meta">
<div>{{ $recVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($recVideo->view_count) }} views • {{ $recVideo->created_at->diffForHumans() }}</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-secondary">No recommendations available yet. Check back later!</div>
@endif
@endif
</div>
</div>
@include('layouts.partials.share-modal')
@include('layouts.partials.edit-video-modal')
@if(Session::has('openEditModal') && Session::get('openEditModal'))
@auth
<script>
@@ -495,17 +723,82 @@
</script>
@endauth
@endif
<script>
// Playlist navigation URLs
const previousVideoUrl = @json($previousVideo ? route('videos.show', $previousVideo->id) . '?playlist=' . $playlist->id : null);
const nextVideoUrl = @json($nextVideo ? route('videos.show', $nextVideo->id) . '?playlist=' . $playlist->id : null);
// Autoplay management
let autoplayEnabled = true;
document.addEventListener('DOMContentLoaded', function() {
var videoPlayer = document.getElementById('videoPlayer');
var playlistControls = document.getElementById('playlistControls');
var autoplayToggle = document.getElementById('autoplayToggle');
if (videoPlayer) {
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'); });
}
// Handle video ended - autoplay next
if (nextVideoUrl) {
videoPlayer.addEventListener('ended', function() {
if (autoplayEnabled) {
console.log('Video ended, going to next...');
window.location.href = nextVideoUrl;
}
});
}
}
// Show playlist controls on hover
if (playlistControls) {
const videoContainer = document.getElementById('videoContainer');
videoContainer.addEventListener('mouseenter', function() {
playlistControls.classList.add('visible');
});
videoContainer.addEventListener('mouseleave', function() {
playlistControls.classList.remove('visible');
});
// Initially show for a few seconds
setTimeout(function() { playlistControls.classList.add('visible'); }, 2000);
setTimeout(function() { playlistControls.classList.remove('visible'); }, 6000);
}
// Autoplay toggle handler
if (autoplayToggle) {
autoplayToggle.addEventListener('change', function() {
autoplayEnabled = this.checked;
localStorage.setItem('playlistAutoplay', autoplayEnabled ? '1' : '0');
});
// Load saved preference
const savedAutoplay = localStorage.getItem('playlistAutoplay');
if (savedAutoplay !== null) {
autoplayEnabled = savedAutoplay === '1';
autoplayToggle.checked = autoplayEnabled;
}
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; }
if (e.key === 'ArrowLeft') { e.preventDefault(); goToPreviousVideo(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); goToNextVideo(); }
});
});
function goToPreviousVideo() {
if (previousVideoUrl) { window.location.href = previousVideoUrl; }
}
function goToNextVideo() {
if (nextVideoUrl) { window.location.href = nextVideoUrl; }
}
</script>
@endsection