275 lines
12 KiB
PHP
275 lines
12 KiB
PHP
@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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
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
|