Files
2026-07-14 00:01:48 +03:00

275 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('frontend.layouts.app')
@section('title', 'Watch Party — ' . $party->episode->anime->title . ' | Animexe')
@push('styles')
<style>
body{background:#05050e}
#pg{padding-top:0}
.wp-wrap{display:flex;max-width:1600px;margin:0 auto;padding-top:var(--nav-h);min-height:calc(100vh - var(--nav-h));gap:0}
.wp-main{flex:1;min-width:0;background:#020208;display:flex;flex-direction:column}
.wp-side{width:320px;flex-shrink:0;background:rgba(10,10,22,.98);border-left:1px solid rgba(255,45,125,.15);display:flex;flex-direction:column;height:calc(100vh - var(--nav-h));position:sticky;top:var(--nav-h)}
/* Video */
.wp-vwrap{position:relative;width:100%;aspect-ratio:16/9;background:#000}
.wp-vwrap iframe,.wp-vwrap video{position:absolute;inset:0;width:100%;height:100%;border:none}
/* Host bar */
.wp-hostbar{padding:12px 16px;background:rgba(255,45,125,.06);border-bottom:1px solid rgba(255,45,125,.15);display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.wp-roomcode{font-size:1.4rem;font-weight:900;letter-spacing:.18em;color:#ff2d7d;cursor:pointer;user-select:all}
.wp-hostbtn{padding:6px 16px;border-radius:20px;border:1px solid rgba(255,45,125,.4);background:rgba(255,45,125,.1);color:#ff2d7d;font-size:.8rem;font-weight:700;cursor:pointer;font-family:inherit;transition:all .15s}
.wp-hostbtn:hover{background:rgba(255,45,125,.25)}
.wp-sync-status{font-size:.72rem;color:rgba(255,255,255,.3);margin-left:auto}
/* Members */
.wp-members{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06)}
.wp-members-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.3);margin-bottom:8px}
.wp-member-list{display:flex;flex-wrap:wrap;gap:6px}
.wp-member{display:flex;align-items:center;gap:6px;padding:4px 10px;background:rgba(255,255,255,.06);border-radius:20px;font-size:.76rem;color:#fff}
.wp-member.host{border:1px solid rgba(255,215,0,.3);background:rgba(255,215,0,.06)}
/* Chat */
.wp-chat{flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:8px;scroll-behavior:smooth}
.wc-msg{max-width:100%}
.wc-meta{font-size:.68rem;color:rgba(255,255,255,.3);margin-bottom:3px}
.wc-bubble{display:inline-block;padding:7px 12px;border-radius:0 10px 10px 10px;background:rgba(255,255,255,.07);font-size:.84rem;color:#fff;word-break:break-word;max-width:100%;line-height:1.4}
.wc-mine .wc-bubble{background:rgba(255,45,125,.15);border-radius:10px 0 10px 10px}
.wc-mine{align-self:flex-end;text-align:right}
.wc-mine .wc-meta{text-align:right}
/* Chat input */
.wp-chat-input{padding:12px 16px;border-top:1px solid rgba(255,255,255,.07);display:flex;gap:8px;flex-shrink:0}
.wp-chat-input input{flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:9px 14px;color:#fff;font-size:.84rem;font-family:inherit;outline:none}
.wp-chat-input button{padding:9px 16px;border-radius:10px;border:none;background:rgba(255,45,125,.2);color:#ff2d7d;font-size:.9rem;cursor:pointer;flex-shrink:0;transition:background .15s}
.wp-chat-input button:hover{background:rgba(255,45,125,.35)}
/* Episode info */
.wp-ep-info{padding:14px 16px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;gap:12px;align-items:center}
.wp-ep-info img{width:48px;height:72px;border-radius:8px;object-fit:cover;flex-shrink:0}
.wp-ep-title{font-size:.84rem;font-weight:700;color:#fff;margin-bottom:2px}
.wp-ep-sub{font-size:.72rem;color:rgba(255,255,255,.35)}
</style>
@endpush
@section('content')
<div class="wp-wrap">
{{-- ── Ana video alanı ─────────────────────────────────────────── --}}
<div class="wp-main">
{{-- Video --}}
<div class="wp-vwrap" id="wp-vwrap">
@if($party->episode->m3u8_url || $party->episode->video_url)
<video id="wp-vd" controls playsinline preload="metadata"
@if($party->episode->m3u8_url) data-src="{{ $party->episode->m3u8_url }}" @endif
@if($party->episode->video_url) src="{{ $party->episode->video_url }}" @endif>
</video>
@else
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.3);font-size:.9rem">
<div style="text-align:center"><i class="bi bi-play-circle" style="font-size:3rem;display:block;margin-bottom:8px"></i>Video yüklenemedi</div>
</div>
@endif
</div>
{{-- Host kontrol barı --}}
<div class="wp-hostbar">
<div>
<div style="font-size:.68rem;color:rgba(255,255,255,.3);margin-bottom:2px">Oda Kodu</div>
<div class="wp-roomcode" onclick="copyCode()" title="Kopyalamak için tıkla">{{ $party->room_code }}</div>
</div>
<div>
<div style="font-size:.68rem;color:rgba(255,255,255,.3);margin-bottom:4px">
Ev Sahibi: <span id="wp-host-name" style="color:#fff">{{ $party->host->name ?? '—' }}</span>
@auth @if(auth()->id() === $party->host_user_id)
<span style="background:rgba(255,215,0,.15);color:#ffd700;padding:1px 7px;border-radius:10px;font-size:.65rem;margin-left:5px">Sensin</span>
@endif @endauth
</div>
<div id="wp-member-count" style="font-size:.72rem;color:rgba(255,255,255,.35)">0 üye aktif</div>
</div>
@auth @if(auth()->id() === $party->host_user_id)
<button class="wp-hostbtn" onclick="hostPlay()"><i class="bi bi-play-fill"></i> Oynat</button>
<button class="wp-hostbtn" onclick="hostPause()"><i class="bi bi-pause-fill"></i> Durdur</button>
@endif @endauth
<span class="wp-sync-status" id="wp-sync-status"> bekleniyor...</span>
<a href="{{ route('watch', [$party->episode->anime->slug, $party->episode->season->season_number ?? 1, $party->episode->episode_number]) }}"
style="margin-left:auto;font-size:.76rem;color:rgba(255,255,255,.3);text-decoration:none">
<i class="bi bi-box-arrow-up-right"></i> Tam oynatıcı
</a>
</div>
</div>
{{-- ── Sağ panel: Üyeler + Chat ─────────────────────────────────── --}}
<div class="wp-side">
{{-- Bölüm bilgisi --}}
<div class="wp-ep-info">
@if($party->episode->anime->coverUrl)
<img src="{{ $party->episode->anime->coverUrl }}" alt="">
@endif
<div>
<div class="wp-ep-title">{{ $party->episode->anime->title }}</div>
<div class="wp-ep-sub">S{{ $party->episode->season->season_number ?? 1 }}E{{ $party->episode->episode_number }}
{{ $party->episode->title ? '— ' . $party->episode->title : '' }}</div>
</div>
</div>
{{-- Aktif üyeler --}}
<div class="wp-members">
<div class="wp-members-title"><i class="bi bi-people"></i> Aktif Üyeler</div>
<div class="wp-member-list" id="wp-members"></div>
</div>
{{-- Chat --}}
<div class="wp-chat" id="wp-chat"></div>
{{-- Chat input --}}
@auth
<div class="wp-chat-input">
<input type="text" id="wp-chat-inp" placeholder="Mesaj yaz..." maxlength="200"
onkeydown="if(event.key==='Enter')sendChatMsg()">
<button onclick="sendChatMsg()"><i class="bi bi-send-fill"></i></button>
</div>
@else
<div style="padding:12px;text-align:center;font-size:.78rem;color:rgba(255,255,255,.3)">
<a href="{{ route('frontend.login') }}" style="color:#ff2d7d">Giriş yap</a> sohbete katıl
</div>
@endauth
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
const WP_CSRF = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
const WP_ROOM = '{{ $party->room_code }}';
const WP_IS_HOST = {{ auth()->check() && auth()->id() === $party->host_user_id ? 'true' : 'false' }};
const WP_ME_NAME = {{ auth()->check() ? json_encode(auth()->user()->name) : 'null' }};
const WP_IS_AUTH = {{ auth()->check() ? 'true' : 'false' }};
// HLS init
const vd = document.getElementById('wp-vd');
if (vd) {
const src = vd.dataset.src;
if (src && Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(src);
hls.attachMedia(vd);
} else if (src && vd.canPlayType('application/vnd.apple.mpegurl')) {
vd.src = src;
}
}
// ── Chat (basit localStorage tabanlı, polling) ────────────────
// Gerçek WebSocket olmadığı için sync sırasında chat mesajlarını da taşı
let chatMessages = [];
let lastChatTs = 0;
function addChatMsg(msg, isMine) {
chatMessages.push({ ...msg, mine: isMine });
renderChat();
}
function renderChat() {
const el = document.getElementById('wp-chat');
if (!el) return;
el.innerHTML = chatMessages.map(m => `
<div class="wc-msg ${m.mine ? 'wc-mine' : ''}">
<div class="wc-meta">${escHtml(m.name || '?')} · ${m.time || ''}</div>
<div class="wc-bubble">${escHtml(m.body)}</div>
</div>`).join('');
el.scrollTop = el.scrollHeight;
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function sendChatMsg() {
const inp = document.getElementById('wp-chat-inp');
const body = inp?.value.trim();
if (!body) return;
inp.value = '';
const now = new Date().toLocaleTimeString('tr-TR', { hour:'2-digit', minute:'2-digit' });
addChatMsg({ name: WP_ME_NAME, body, time: now }, true);
// Broadcast via sync payload (next poll)
pendingChatMsg = { name: WP_ME_NAME, body, time: now };
}
let pendingChatMsg = null;
// ── Party Sync ─────────────────────────────────────────────────
function renderMembers(members) {
const el = document.getElementById('wp-members');
const cnt = document.getElementById('wp-member-count');
if (el) el.innerHTML = members.map(m => `
<div class="wp-member ${m.is_host ? 'host' : ''}">
${m.is_host ? '<i class="bi bi-star-fill" style="font-size:.6rem;color:#ffd700"></i>' : ''}
${escHtml(m.name || '?')}
</div>`).join('');
if (cnt) cnt.textContent = members.length + ' üye aktif';
}
async function doSync() {
const body = WP_IS_HOST
? { current_sec: Math.floor(vd?.currentTime ?? 0), is_playing: !(vd?.paused ?? true) }
: {};
try {
const r = await fetch(`/party/${WP_ROOM}/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': WP_CSRF, 'Accept': 'application/json' },
body: JSON.stringify(body),
});
const d = await r.json();
renderMembers(d.members || []);
// Non-host senkron
if (!WP_IS_HOST && vd) {
const diff = Math.abs(vd.currentTime - d.current_sec);
if (diff > 2) vd.currentTime = d.current_sec;
if (d.is_playing && vd.paused) vd.play().catch(()=>{});
if (!d.is_playing && !vd.paused) vd.pause();
}
document.getElementById('wp-sync-status').textContent = '✅ senkron ' + new Date().toLocaleTimeString('tr-TR',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
pendingChatMsg = null;
} catch(e) {
document.getElementById('wp-sync-status').textContent = '❌ bağlantı kesildi';
}
}
function hostPlay() { if (vd) { vd.play().catch(()=>{}); } }
function hostPause() { if (vd) vd.pause(); }
function copyCode() {
navigator.clipboard?.writeText(WP_ROOM).then(() => {
if (window.showToast) showToast('Oda kodu kopyalandı: ' + WP_ROOM, 'success');
else alert('Oda kodu kopyalandı: ' + WP_ROOM);
});
}
// İlk join
if (WP_IS_AUTH) {
fetch(`/party/${WP_ROOM}/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': WP_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({}),
}).then(r => r.json()).then(d => {
if (d.ok) {
renderMembers(d.members || []);
if (!WP_IS_HOST && vd && d.current_sec > 0) {
vd.currentTime = d.current_sec;
}
}
}).catch(()=>{});
}
setInterval(doSync, 3000);
// Ayrılma temizliği
window.addEventListener('beforeunload', () => {
navigator.sendBeacon(`/party/${WP_ROOM}/leave`, new Blob([JSON.stringify({})], { type: 'application/json' }));
});
</script>
@endpush