latest update
This commit is contained in:
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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user