1087 lines
44 KiB
PHP
1087 lines
44 KiB
PHP
@extends('layouts.app')
|
|
|
|
@push('head')
|
|
<!-- Open Graph / WhatsApp / Facebook / Twitter / LinkedIn / Telegram Preview -->
|
|
<meta property="og:title" content="{{ $video->title }}">
|
|
<meta property="og:description"
|
|
content="{{ $video->description ? Str::limit($video->description, 200) : 'Watch ' . $video->title . ' on ' . config('app.name') }}">
|
|
<meta property="og:image" content="{{ $video->open_graph_image }}">
|
|
<meta property="og:image:width" content="{{ $video->thumbnail_width }}">
|
|
<meta property="og:image:height" content="{{ $video->thumbnail_height }}">
|
|
<meta property="og:image:alt" content="{{ $video->title }} - Video Thumbnail">
|
|
<meta property="og:url" content="{{ $video->share_url }}">
|
|
<meta property="og:type" content="video.other">
|
|
<meta property="og:site_name" content="{{ config('app.name') }}">
|
|
<meta property="og:locale" content="en_US">
|
|
<meta property="og:author" content="{{ $video->author_name }}">
|
|
<meta property="og:published_time" content="{{ $video->created_at->toIso8601String() }}">
|
|
|
|
<!-- Video-specific Open Graph tags -->
|
|
<meta property="video:duration" content="{{ $video->duration }}">
|
|
<meta property="video:release_date" content="{{ $video->created_at->toIso8601String() }}">
|
|
|
|
<!-- Alternative video tag for some platforms -->
|
|
<meta property="og:video" content="{{ $video->stream_url }}">
|
|
<meta property="og:video:url" content="{{ $video->stream_url }}">
|
|
<meta property="og:video:secure_url" content="{{ $video->stream_url }}">
|
|
<meta property="og:video:type" content="video/mp4">
|
|
<meta property="og:video:width" content="{{ $video->width ?? 1920 }}">
|
|
<meta property="og:video:height" content="{{ $video->height ?? 1080 }}">
|
|
|
|
<!-- Twitter Card - Enhanced for video sharing -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:site" content="{{ config('app.name') }}">
|
|
<meta name="twitter:creator" content="{{ $video->author_name }}">
|
|
<meta name="twitter:title" content="{{ $video->title }}">
|
|
<meta name="twitter:description"
|
|
content="{{ $video->description ? Str::limit($video->description, 200) : 'Watch ' . $video->title . ' on ' . config('app.name') }}">
|
|
<meta name="twitter:image" content="{{ $video->open_graph_image }}">
|
|
<meta name="twitter:image:alt" content="{{ $video->title }} - Video Thumbnail">
|
|
|
|
<!-- Twitter Player card for video -->
|
|
<meta name="twitter:player" content="{{ $video->share_url }}">
|
|
<meta name="twitter:player:width" content="{{ $video->width ?? 1920 }}">
|
|
<meta name="twitter:player:height" content="{{ $video->height ?? 1080 }}">
|
|
<meta name="twitter:player:stream" content="{{ $video->stream_url }}">
|
|
|
|
<!-- LinkedIn specific -->
|
|
<meta property="linkedin:owner" content="{{ $video->author_name }}">
|
|
|
|
<!-- Pinterest -->
|
|
<meta name="pinterest-rich-pin" content="true">
|
|
|
|
<!-- WhatsApp specific (uses Open Graph) -->
|
|
<!-- No additional meta needed - uses og: tags above -->
|
|
|
|
<!-- Schema.org VideoObject for search engines -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "VideoObject",
|
|
"name": "{{ $video->title }}",
|
|
"description": "{{ $video->description ? addslashes($video->description) : 'Watch ' . addslashes($video->title) . ' on ' . config('app.name') }}",
|
|
"thumbnailUrl": "{{ $video->open_graph_image }}",
|
|
"uploadDate": "{{ $video->created_at->toIso8601String() }}",
|
|
"duration": "{{ $video->iso_duration }}",
|
|
"contentUrl": "{{ $video->stream_url }}",
|
|
"embedUrl": "{{ $video->share_url }}",
|
|
"author": {
|
|
"@type": "Person",
|
|
"name": "{{ $video->author_name }}"
|
|
},
|
|
"publisher": {
|
|
"@type": "Organization",
|
|
"name": "{{ config('app.name') }}",
|
|
"logo": {
|
|
"@type": "ImageObject",
|
|
"url": "{{ asset('storage/images/fullLogo.png') }}"
|
|
}
|
|
},
|
|
"interactionStatistic": [
|
|
{
|
|
"@type": "InteractionCounter",
|
|
"interactionType": "https://schema.org/WatchAction",
|
|
"userInteractionCount": {{ $video->view_count }}
|
|
},
|
|
{
|
|
"@type": "InteractionCounter",
|
|
"interactionType": "https://schema.org/LikeAction",
|
|
"userInteractionCount": {{ $video->like_count }}
|
|
}
|
|
]
|
|
}
|
|
</script>
|
|
@endpush
|
|
|
|
@section('title', $video->title . ' | ' . config('app.name'))
|
|
|
|
@section('extra_styles')
|
|
<style>
|
|
/* Video Section */
|
|
.yt-video-section {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* Video Player */
|
|
.video-container {
|
|
position: relative;
|
|
aspect-ratio: 16/9;
|
|
background: #000;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
max-height: 70vh;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* Video Info */
|
|
.video-title {
|
|
font-size: 20px;
|
|
font-weight: 500;
|
|
margin: 16px 0 8px;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.video-stats-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
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;
|
|
gap: 8px;
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
border: none;
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
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;
|
|
align-items: center;
|
|
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;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 18px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Description */
|
|
.video-description {
|
|
background: var(--bg-secondary);
|
|
border-radius: 12px;
|
|
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;
|
|
border-radius: 8px;
|
|
padding: 6px;
|
|
transition: background .15s;
|
|
}
|
|
.sidebar-video-card:hover { background: var(--bg-secondary); }
|
|
.sidebar-video-card.current-video {
|
|
background: rgba(230,30,30,.10);
|
|
border-left: 3px solid var(--brand-red);
|
|
padding-left: 9px;
|
|
}
|
|
|
|
.sidebar-thumb {
|
|
width: 168px;
|
|
aspect-ratio: 16/9;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
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;
|
|
margin-bottom: 4px;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-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%;
|
|
}
|
|
|
|
|
|
.sidebar-video-card {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.sidebar-thumb {
|
|
width: 100%;
|
|
}
|
|
|
|
/* Video Layout Container - Stack vertically on tablet/mobile */
|
|
.video-layout-container {
|
|
flex-direction: column !important;
|
|
}
|
|
|
|
.yt-video-section {
|
|
width: 100% !important;
|
|
flex: none !important;
|
|
}
|
|
|
|
.yt-sidebar-container {
|
|
width: 100% !important;
|
|
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: 0 !important;
|
|
max-width: 100% !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
/* Mobile video player fixes - always full width */
|
|
.video-layout-container {
|
|
max-width: 100% !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
width: 100% !important;
|
|
}
|
|
|
|
.yt-video-section {
|
|
width: 100% !important;
|
|
max-width: 100% !important;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
.video-container {
|
|
width: 100% !important;
|
|
max-width: 100% !important;
|
|
max-height: 50vh !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
.video-container video {
|
|
width: 100% !important;
|
|
max-width: 100% !important;
|
|
object-fit: contain !important;
|
|
}
|
|
|
|
.video-title {
|
|
font-size: 16px !important;
|
|
margin: 12px 0 6px !important;
|
|
}
|
|
|
|
.channel-row {
|
|
flex-direction: column;
|
|
align-items: flex-start !important;
|
|
gap: 12px;
|
|
}
|
|
|
|
.channel-info {
|
|
width: 100%;
|
|
}
|
|
|
|
.subscribe-btn {
|
|
width: 100%;
|
|
}
|
|
|
|
.video-description {
|
|
padding: 12px !important;
|
|
}
|
|
}
|
|
</style>
|
|
@endsection
|
|
|
|
@section('content')
|
|
|
|
<!-- Video Layout Container -->
|
|
<div class="video-layout-container" style="display: flex; gap: 24px; max-width: 1800px; margin: 0 auto;">
|
|
|
|
<!-- Video Section -->
|
|
<div class="yt-video-section">
|
|
<!-- Video Player -->
|
|
<div class="video-container @if ($video->orientation === 'portrait') portrait @elseif($video->orientation === 'square') square @elseif($video->orientation === 'ultrawide') ultrawide @endif"
|
|
id="videoContainer">
|
|
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
|
|
<source src="{{ route('videos.stream', $video) }}" type="video/mp4">
|
|
</video>
|
|
</div>
|
|
|
|
@php
|
|
$typeIcon = match ($video->type) {
|
|
'music' => 'bi-music-note',
|
|
'match' => 'bi-trophy',
|
|
default => 'bi-film',
|
|
};
|
|
@endphp
|
|
|
|
<!-- Video Title with Type Icon -->
|
|
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px;">
|
|
<i class="bi {{ $typeIcon }}" style="color: #ef4444;"></i>
|
|
<span>{{ $video->title }}</span>
|
|
</h1>
|
|
|
|
<!-- Stats Row -->
|
|
<div class="video-stats-row">
|
|
<div class="video-stats-left">
|
|
<span>{{ number_format($video->view_count) }} views</span>
|
|
<span>•</span>
|
|
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
|
</div>
|
|
<div class="video-actions">
|
|
@auth
|
|
<!-- Like Button -->
|
|
<form method="POST"
|
|
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video) : route('videos.like', $video) }}"
|
|
class="d-inline">
|
|
@csrf
|
|
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
|
|
<i
|
|
class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
|
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Edit / Delete Buttons - Only for video owner -->
|
|
@if (Auth::id() === $video->user_id)
|
|
<button class="yt-action-btn" onclick="openEditVideoModal('{{ $video->getRouteKey() }}')">
|
|
<i class="bi bi-pencil"></i> Edit
|
|
</button>
|
|
<button class="yt-action-btn" onclick="openVideoDeleteDialog()"
|
|
style="color:#ef4444;">
|
|
<i class="bi bi-trash"></i> Delete
|
|
</button>
|
|
@endif
|
|
@else
|
|
<a href="{{ route('login') }}" class="yt-action-btn">
|
|
<i class="bi bi-hand-thumbs-up"></i> Like
|
|
</a>
|
|
@endauth
|
|
@if ($video->isShareable())
|
|
<button class="yt-action-btn"
|
|
onclick="videoShare('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')"><i
|
|
class="bi bi-share"></i> Share</button>
|
|
@endif
|
|
|
|
<!-- Save to Playlist Button -->
|
|
<button class="yt-action-btn" onclick="openAddToPlaylistModal({{ $video->id }})">
|
|
<i class="bi bi-collection-plus"></i> Save
|
|
</button>
|
|
@auth
|
|
<!-- Quick Watch Later Button -->
|
|
<form method="POST" action="{{ route('videos.watchLater', $video) }}" class="d-inline"
|
|
style="display: inline;">
|
|
@csrf
|
|
<button type="submit" class="yt-action-btn" title="Watch Later">
|
|
<i class="bi bi-clock"></i>
|
|
</button>
|
|
</form>
|
|
@endauth
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Channel Row -->
|
|
<div class="channel-row"
|
|
style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
|
|
<div style="display: flex; align-items: center; gap: 12px;">
|
|
<a href="{{ $video->user ? route('channel', $video->user->channel) : '#' }}" class="channel-info text-decoration-none"
|
|
style="color: inherit; display: flex; align-items: center; gap: 12px;">
|
|
@if ($video->user)
|
|
<img src="{{ $video->user->avatar_url }}" class="channel-avatar"
|
|
style="width: 36px; height: 36px;" alt="{{ $video->user->name }}">
|
|
@else
|
|
<div class="channel-avatar" style="width: 36px; height: 36px;"></div>
|
|
@endif
|
|
<div>
|
|
<div class="channel-name">{{ $video->user->name ?? 'Unknown' }}</div>
|
|
<div class="channel-subs">{{ number_format($video->user->subscriber_count ?? 0) }} subscribers
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
{{-- Subscribe Button --}}
|
|
@auth
|
|
@if (Auth::id() !== $video->user_id)
|
|
<button class="subscribe-btn">Subscribe</button>
|
|
@endif
|
|
@else
|
|
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
|
|
@endauth
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ── Description / Insights box ── --}}
|
|
@php
|
|
$isVideoOwner = Auth::check() && Auth::id() === $video->user_id;
|
|
$hasDesc = !empty($video->description);
|
|
$showBox = $hasDesc || $isVideoOwner;
|
|
@endphp
|
|
@if ($showBox)
|
|
@php
|
|
$fullDescription = $video->description ?? '';
|
|
$shortDescription = Str::limit($fullDescription, 200);
|
|
$needsExpand = strlen($fullDescription) > 200;
|
|
@endphp
|
|
|
|
<style>
|
|
/* ── Description box ── */
|
|
.vdb-wrap {
|
|
background: var(--bg-secondary);
|
|
border-radius: 12px;
|
|
margin-top: 12px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
.vdb-tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid var(--border-color);
|
|
padding: 0 4px;
|
|
}
|
|
.vdb-tab {
|
|
background: none; border: none;
|
|
color: var(--text-secondary);
|
|
font-size: 13px; font-weight: 600;
|
|
padding: 0 16px; height: 44px;
|
|
cursor: pointer; position: relative;
|
|
transition: color .15s; white-space: nowrap;
|
|
display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.vdb-tab:hover { color: var(--text-primary); }
|
|
.vdb-tab.active { color: var(--text-primary); }
|
|
.vdb-tab.active::after {
|
|
content: ''; position: absolute; bottom: 0; left: 0; right: 0;
|
|
height: 2px; background: #ef4444; border-radius: 2px 2px 0 0;
|
|
}
|
|
.vdb-panel { display: none; padding: 14px 16px 16px; }
|
|
.vdb-panel.active { display: block; }
|
|
|
|
/* About panel */
|
|
.vdb-meta { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 10px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
.vdb-desc-text { font-size: 14px; line-height: 1.6; color: var(--text-primary); white-space: pre-wrap; }
|
|
.vdb-desc-text p { margin-bottom: 8px; }
|
|
.vdb-desc-text a { color: #3ea6ff; }
|
|
.vdb-show-more { background: none; border: none; color: var(--text-primary); font-weight: 700; font-size: 13px; cursor: pointer; padding: 6px 0 0; }
|
|
|
|
</style>
|
|
|
|
<div class="vdb-wrap" id="vdbWrap">
|
|
{{-- Tab bar --}}
|
|
<div class="vdb-tabs">
|
|
<button class="vdb-tab active" data-panel="vdb-about" onclick="switchVdbTab('vdb-about',this)">
|
|
<i class="bi bi-card-text"></i> About
|
|
</button>
|
|
@if($isVideoOwner)
|
|
<button class="vdb-tab" data-panel="vdb-insights" onclick="switchVdbTab('vdb-insights',this)" id="insightsTabBtn">
|
|
<i class="bi bi-bar-chart-line-fill"></i> Insights
|
|
</button>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- About panel --}}
|
|
<div class="vdb-panel active" id="vdb-about">
|
|
<div class="vdb-meta">
|
|
<span><i class="bi bi-eye" style="margin-right:4px;"></i>{{ number_format($video->view_count) }} views</span>
|
|
<span>•</span>
|
|
<span>{{ $video->created_at->format('M d, Y') }}</span>
|
|
@if($video->duration)
|
|
<span>•</span>
|
|
<span><i class="bi bi-clock" style="margin-right:4px;"></i>{{ $video->formatted_duration }}</span>
|
|
@endif
|
|
</div>
|
|
@if($hasDesc)
|
|
<div id="vdbDescShort" class="vdb-desc-text">{{ $needsExpand ? $shortDescription : $fullDescription }}</div>
|
|
@if($needsExpand)
|
|
<div id="vdbDescFull" class="vdb-desc-text" style="display:none;">{{ $fullDescription }}</div>
|
|
<button class="vdb-show-more" onclick="toggleVdbDesc(this)">Show more</button>
|
|
@endif
|
|
@else
|
|
<p style="font-size:13px;color:var(--text-secondary);margin:0;">No description added.</p>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Insights panel (owner only) --}}
|
|
@if($isVideoOwner)
|
|
<x-video-insights :video="$video" />
|
|
@endif
|
|
</div>
|
|
|
|
<script>
|
|
// ── Tab switching ──────────────────────────────────────
|
|
function switchVdbTab(panelId, btn) {
|
|
document.querySelectorAll('.vdb-tab').forEach(b => b.classList.remove('active'));
|
|
document.querySelectorAll('.vdb-panel').forEach(p => p.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
document.getElementById(panelId).classList.add('active');
|
|
if (panelId === 'vdb-insights' && !window._insLoaded) loadInsights();
|
|
}
|
|
|
|
// ── Description expand/collapse ───────────────────────
|
|
function toggleVdbDesc(btn) {
|
|
const short = document.getElementById('vdbDescShort');
|
|
const full = document.getElementById('vdbDescFull');
|
|
if (full.style.display === 'none') {
|
|
short.style.display = 'none';
|
|
full.style.display = 'block';
|
|
btn.textContent = 'Show less';
|
|
} else {
|
|
short.style.display = 'block';
|
|
full.style.display = 'none';
|
|
btn.textContent = 'Show more';
|
|
}
|
|
}
|
|
|
|
// ── Share + track ──────────────────────────────────────
|
|
function videoShare(url, title, trackUrl) {
|
|
openShareModal(url, title);
|
|
fetch(trackUrl, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '', 'Accept': 'application/json' }
|
|
}).catch(() => {});
|
|
}
|
|
|
|
</script>
|
|
@endif
|
|
|
|
@include('components.video-comments', ['video' => $video])
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar - Up Next / Recommendations -->
|
|
<div class="yt-sidebar-container">
|
|
@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)
|
|
@php $isCurrent = $playlistVideo->id === $video->id; @endphp
|
|
<div class="sidebar-video-card{{ $isCurrent ? ' current-video' : '' }}"
|
|
{{ $isCurrent ? '' : "onclick=\"window.location.href='".route('videos.show', $playlistVideo)."?playlist={$playlist->share_token}'\"" }}
|
|
style="{{ $isCurrent ? 'cursor:default;' : 'cursor:pointer;' }}">
|
|
<div class="sidebar-thumb" style="position: relative;">
|
|
@if ($playlistVideo->thumbnail)
|
|
<img src="{{ route('media.thumbnail', $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">
|
|
@if($isCurrent)
|
|
<div style="color:var(--brand-red);font-weight:600;font-size:11px;">▶ Now Playing</div>
|
|
@else
|
|
<div>{{ $playlistVideo->user->name ?? 'Unknown' }}</div>
|
|
<div>{{ number_format($playlistVideo->view_count) }} views •
|
|
{{ $playlistVideo->created_at->diffForHumans() }}</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@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) }}'">
|
|
<div class="sidebar-thumb" style="position: relative;">
|
|
@if ($recVideo->thumbnail)
|
|
<img src="{{ route('media.thumbnail', $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>
|
|
|
|
|
|
<!-- Mobile Bottom Action Bar -->
|
|
<div class="mobile-bottom-bar">
|
|
@auth
|
|
<form method="POST"
|
|
action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video) : route('videos.like', $video) }}"
|
|
class="d-inline" style="flex:1;">
|
|
@csrf
|
|
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}"
|
|
style="width:100%;">
|
|
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
|
|
<span>{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}</span>
|
|
</button>
|
|
</form>
|
|
@else
|
|
<a href="{{ route('login') }}" class="yt-action-btn" style="flex:1;text-align:center;">
|
|
<i class="bi bi-hand-thumbs-up"></i><span>Like</span>
|
|
</a>
|
|
@endauth
|
|
|
|
@if ($video->isShareable())
|
|
<button class="yt-action-btn"
|
|
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')" style="flex:1;">
|
|
<i class="bi bi-share"></i><span>Share</span>
|
|
</button>
|
|
@endif
|
|
|
|
<button class="yt-action-btn" onclick="openAddToPlaylistModal({{ $video->id }})" style="flex:1;">
|
|
<i class="bi bi-collection-plus"></i><span>Save</span>
|
|
</button>
|
|
|
|
@auth
|
|
@if (Auth::id() === $video->user_id)
|
|
<button class="yt-action-btn" onclick="openVideoDeleteDialog()"
|
|
style="flex:1;color:#ef4444;">
|
|
<i class="bi bi-trash"></i><span>Delete</span>
|
|
</button>
|
|
@else
|
|
<button class="yt-action-btn" style="flex:1;background:var(--brand-red);color:white;">
|
|
<i class="bi bi-bell"></i><span>Subscribe</span>
|
|
</button>
|
|
@endif
|
|
@else
|
|
<a href="{{ route('login') }}" class="yt-action-btn"
|
|
style="flex:1;background:var(--brand-red);color:white;text-align:center;">
|
|
<i class="bi bi-bell"></i><span>Subscribe</span>
|
|
</a>
|
|
@endauth
|
|
</div>
|
|
|
|
@include('layouts.partials.share-modal')
|
|
@include('layouts.partials.edit-video-modal')
|
|
@include('layouts.partials.add-to-playlist-modal')
|
|
|
|
@if (Session::has('openEditModal') && Session::get('openEditModal'))
|
|
@auth
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
openEditVideoModal('{{ $video->getRouteKey() }}');
|
|
});
|
|
</script>
|
|
@endauth
|
|
@endif
|
|
|
|
@if(session('_auto_dl_video') && $video->isAudioOnly())
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
if (typeof startSlideshowDownload === 'function') {
|
|
startSlideshowDownload('{{ $video->getRouteKey() }}');
|
|
}
|
|
});
|
|
</script>
|
|
@endif
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var videoPlayer = document.getElementById('videoPlayer');
|
|
if (videoPlayer) {
|
|
videoPlayer.volume = 0.5;
|
|
var playPromise = videoPlayer.play();
|
|
if (playPromise !== undefined) {
|
|
playPromise.then(function() {}).catch(function() {});
|
|
}
|
|
|
|
@if($nextVideo && $playlist)
|
|
// Auto-advance to next playlist video when this one ends
|
|
videoPlayer.addEventListener('ended', function() {
|
|
window.location.href = '{{ route('videos.show', $nextVideo) }}?playlist={{ $playlist->share_token }}';
|
|
});
|
|
@endif
|
|
}
|
|
|
|
// Scroll the currently playing video into view in the playlist sidebar
|
|
var currentCard = document.querySelector('.sidebar-video-card.current-video');
|
|
if (currentCard) {
|
|
setTimeout(function() {
|
|
currentCard.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
}, 400);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
@auth
|
|
@if(Auth::id() === $video->user_id)
|
|
@php $owner2fa = Auth::user()->two_factor_enabled && Auth::user()->two_factor_secret; @endphp
|
|
|
|
{{-- Delete confirmation dialog --}}
|
|
<div id="videoDeleteDialog" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.7);backdrop-filter:blur(4px);align-items:center;justify-content:center;padding:16px;" onclick="if(event.target===this)closeVideoDeleteDialog()">
|
|
<div style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px;width:100%;max-width:420px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,.6);">
|
|
<div style="padding:20px 24px 16px;border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;">
|
|
<div style="display:flex;align-items:center;gap:10px;font-size:16px;font-weight:600;color:#ef4444;">
|
|
<i class="bi bi-trash"></i> Delete Video
|
|
</div>
|
|
<button onclick="closeVideoDeleteDialog()" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:18px;line-height:1;padding:4px;"><i class="bi bi-x-lg"></i></button>
|
|
</div>
|
|
<div style="padding:20px 24px;">
|
|
<p style="margin:0 0 12px;font-size:14px;color:var(--text-primary);">
|
|
You are about to permanently delete <strong>{{ $video->title }}</strong>.
|
|
</p>
|
|
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.25);border-radius:8px;padding:10px 14px;font-size:13px;color:#fca5a5;display:flex;gap:8px;align-items:flex-start;">
|
|
<i class="bi bi-exclamation-triangle-fill" style="flex-shrink:0;margin-top:1px;"></i>
|
|
<span>All views, likes, comments, and HLS files will also be deleted. This cannot be undone.</span>
|
|
</div>
|
|
@if($owner2fa)
|
|
<div style="margin-top:16px;">
|
|
<label style="font-size:13px;color:var(--text-secondary);display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
|
<i class="bi bi-shield-lock-fill" style="color:#a78bfa;"></i>
|
|
Enter your <strong style="color:var(--text-primary);">2FA code</strong> to confirm
|
|
</label>
|
|
<input type="text" id="delVideoOtp" inputmode="numeric" pattern="[0-9]*"
|
|
maxlength="6" autocomplete="one-time-code" placeholder="000000"
|
|
style="width:100%;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:8px;padding:10px 14px;color:#fff;font-size:22px;letter-spacing:.3em;text-align:center;box-sizing:border-box;">
|
|
</div>
|
|
@endif
|
|
<div id="delVideoError" style="display:none;margin-top:12px;padding:8px 12px;background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.3);border-radius:6px;color:#f87171;font-size:13px;"></div>
|
|
</div>
|
|
<div style="padding:12px 24px 20px;display:flex;gap:10px;justify-content:flex-end;">
|
|
<button onclick="closeVideoDeleteDialog()" class="yt-action-btn">Cancel</button>
|
|
<button id="delVideoConfirmBtn" onclick="confirmVideoDelete()" class="yt-action-btn" style="background:#ef4444;color:#fff;border-color:#ef4444;">
|
|
<i class="bi bi-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const _owner2fa = {{ $owner2fa ? 'true' : 'false' }};
|
|
const _videoDeleteUrl = '{{ route('videos.destroy', $video) }}';
|
|
const _videoCsrf = '{{ csrf_token() }}';
|
|
|
|
function openVideoDeleteDialog() {
|
|
const dlg = document.getElementById('videoDeleteDialog');
|
|
dlg.style.display = 'flex';
|
|
dlg.style.alignItems = 'center';
|
|
dlg.style.justifyContent = 'center';
|
|
document.getElementById('delVideoError').style.display = 'none';
|
|
if (_owner2fa) { document.getElementById('delVideoOtp').value = ''; setTimeout(() => document.getElementById('delVideoOtp').focus(), 100); }
|
|
}
|
|
function closeVideoDeleteDialog() {
|
|
document.getElementById('videoDeleteDialog').style.display = 'none';
|
|
}
|
|
function confirmVideoDelete() {
|
|
const btn = document.getElementById('delVideoConfirmBtn');
|
|
const errEl = document.getElementById('delVideoError');
|
|
const otpCode = _owner2fa ? document.getElementById('delVideoOtp').value.replace(/\s/g,'') : '';
|
|
|
|
if (_owner2fa && otpCode.length !== 6) {
|
|
errEl.textContent = 'Please enter your 6-digit 2FA code.';
|
|
errEl.style.display = 'block';
|
|
document.getElementById('delVideoOtp').focus();
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting…';
|
|
errEl.style.display = 'none';
|
|
|
|
const body = new URLSearchParams({ '_token': _videoCsrf, '_method': 'DELETE' });
|
|
if (_owner2fa) body.append('otp_code', otpCode);
|
|
|
|
fetch(_videoDeleteUrl, {
|
|
method: 'POST',
|
|
headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString()
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
window.location.href = '/';
|
|
} else {
|
|
errEl.textContent = data.message || 'Delete failed.';
|
|
errEl.style.display = 'block';
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-trash"></i> Delete';
|
|
if (_owner2fa) { document.getElementById('delVideoOtp').value = ''; document.getElementById('delVideoOtp').focus(); }
|
|
}
|
|
})
|
|
.catch(() => {
|
|
errEl.textContent = 'An error occurred. Please try again.';
|
|
errEl.style.display = 'block';
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-trash"></i> Delete';
|
|
});
|
|
}
|
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeVideoDeleteDialog(); });
|
|
</script>
|
|
@endif
|
|
@endauth
|
|
@endsection
|
|
|
|
|
|
/* Mobile Bottom Action Bar */
|
|
@media (max-width: 576px) {
|
|
.mobile-bottom-bar {
|
|
display: flex !important;
|
|
}
|
|
.desktop-actions {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.mobile-bottom-bar {
|
|
display: none;
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--bg-primary);
|
|
border-top: 1px solid var(--border-color);
|
|
padding: 12px 16px;
|
|
justify-content: space-around;
|
|
z-index: 1000;
|
|
gap: 8px;
|
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.mobile-bottom-bar .yt-action-btn {
|
|
flex: 1;
|
|
justify-content: center;
|
|
padding: 12px 8px;
|
|
font-size: 12px;
|
|
min-height: 44px;
|
|
}
|
|
|
|
.mobile-bottom-bar .yt-action-btn i {
|
|
font-size: 18px;
|
|
}
|
|
|
|
@media (max-width: 576px) {
|
|
.yt-video-section {
|
|
padding-bottom: 70px;
|
|
}
|
|
}
|
|
|
|
|
|
<!-- Extra Mobile Styles -->
|
|
<style>
|
|
@media (max-width: 400px) {
|
|
.video-layout-container {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.video-title {
|
|
font-size: 15px !important;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.video-stats-left {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.video-description-box {
|
|
margin: 12px 4px;
|
|
padding: 10px !important;
|
|
}
|
|
|
|
.comments-section {
|
|
margin-top: 16px;
|
|
padding: 12px 4px;
|
|
}
|
|
|
|
.comment-form {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.comment-form>img {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|