Files
animexe/resources/views/frontend/player.blade.php
T
AyrisAIandClaude Sonnet 5 80eff2ca8c fix: stop unwrapping the CORS proxy for aniziumserver hosts client-side
proxyUrl() in player.blade.php listed aniziumserver.sbs/site/com in
DIRECT_CORS_HOSTS, so it unwrapped the server's /stream/proxy URLs and
tried loading them directly from the browser. That CORS assumption is
no longer true (confirmed via browser console), which silently undid
the PlayerController fix from the previous commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:58:45 +03:00

5582 lines
271 KiB
PHP
Raw 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')
@php
$__epTitle = $anime->title . ' ' . $seasonModel->season_number . '. Sezon ' . $ep->episode_number . '. Bölüm İzle';
$__epDesc = $anime->title . ' ' . $seasonModel->season_number . '. sezon ' . $ep->episode_number . '. bölümünü Türkçe altyazılı ve dublajlı ücretsiz izle — Animexe.';
$__epImgUrl = $anime->coverUrl ?? url('/logo.jpg');
$__noindexWp = \App\Models\Setting::get('seo_noindex_watch', '0') === '1';
$__domain = rtrim(\App\Models\Setting::get('seo_canonical_domain', config('app.url')), '/');
@endphp
@section('title', $__epTitle . ' - Türkçe | Animexe')
@section('meta_description', $__epDesc)
@section('meta_keywords', $anime->title . ' izle, ' . $anime->title . ' ' . $ep->episode_number . '. bölüm izle, türkçe anime izle, animexe')
@section('robots', $__noindexWp ? 'noindex, nofollow' : 'index, follow')
@section('og_title', $__epTitle . ' - Türkçe | Animexe')
@section('og_description', $__epDesc)
@section('og_image', $__epImgUrl)
@section('schema')
@if(\App\Models\Setting::get('seo_enable_schema','1') === '1')
<script type="application/ld+json">
{
"@@context":"https://schema.org",
"@@type":"VideoObject",
"name":"{{ addslashes($__epTitle) }}",
"description":"{{ addslashes($__epDesc) }}",
"thumbnailUrl":"{{ $__epImgUrl }}",
"uploadDate":"{{ ($ep->created_at ?? now())->toIso8601String() }}",
"contentUrl":"{{ $__domain }}{{ route('watch', [$anime->slug, $seasonModel->season_number, $ep->episode_number], false) }}",
"embedUrl":"{{ $__domain }}{{ route('watch', [$anime->slug, $seasonModel->season_number, $ep->episode_number], false) }}",
"partOfSeries":{"@@type":"TVSeries","name":"{{ addslashes($anime->title) }}","url":"{{ $__domain }}{{ route('anime.show', $anime->slug, false) }}"},
"partOfSeason":{"@@type":"TVSeason","seasonNumber":{{ $seasonModel->season_number }}},
"episodeNumber":{{ $ep->episode_number }},
"inLanguage":"tr",
"publisher":{"@@type":"Organization","name":"Animexe","logo":{"@@type":"ImageObject","url":"{{ url('/logo.jpg') }}"}}
}
</script>
@endif
@endsection
@push('styles')
{{-- CDN preconnect: HLS segment bağlantıları için önceden bağlan --}}
<link rel="preconnect" href="https://u.aniziumserver.sbs" crossorigin>
<link rel="dns-prefetch" href="https://u.aniziumserver.sbs">
<style>
body{background:var(--bg)}
#pg{padding-top:0}
#topnav{background:rgba(6,6,13,.88) !important;backdrop-filter:blur(16px) !important}
.pw{display:flex;max-width:1400px;margin:0 auto;padding-top:var(--nav-h);min-height:calc(100vh - var(--nav-h))}
.pw-main{flex:1;min-width:0;background:#020208;position:relative}
.pw-side{width:340px;flex-shrink:0;background:linear-gradient(180deg,rgba(10,10,22,.98),rgba(6,6,14,.99));border-left:1px solid rgba(0,245,255,.12);display:flex;flex-direction:column;height:calc(100vh - var(--nav-h));position:sticky;top:var(--nav-h);overflow:hidden;box-shadow:-8px 0 40px rgba(0,0,0,.35)}
/* ── Video wrap ───────────────────────────────────────────────── */
.vw{position:relative;width:100%;aspect-ratio:16/9;background:#000;cursor:pointer;user-select:none;touch-action:manipulation;-webkit-tap-highlight-color:transparent;overflow:hidden}
/* vd-clip: video'yu saran overflow:hidden katmanı — zoom taşmasını keser, menüleri etkilemez */
#vd-clip{position:absolute;inset:0;overflow:hidden}
#vd{width:100%;height:100%;display:block;background:#000;object-fit:contain;transition:transform .25s ease,object-fit 0s}
/* ── Ambiyans Işığı ──────────────────────────────────── */
/* Canvas .vw dışında, .pw-main içinde — videonun arkasında hale efekti verir */
#ambi-canvas{
position:absolute;top:0;left:0;
width:100%;aspect-ratio:16/9;
pointer-events:none;opacity:0;
transition:opacity .7s ease;
filter:blur(72px) saturate(3) brightness(.7);
transform:scale(1.08);transform-origin:top center;
z-index:0;
}
.pw-main.ambi-on #ambi-canvas{opacity:.9}
/* .vw z-index:15 ile canvas'ın (0) ve eb-tabs'ın (10) üstünde durur; vmenu/backdrop bu stacking context içinde kalsa bile eb-tabs'ı geçer */
.vw{position:relative;z-index:15}
/* Ayarlar (gear) — her zaman görünür; portrait'te label gizlenir */
@media(orientation:portrait){.set-btn-wrap #qlbl{display:none!important}}
/* ── NicoNico yorum girişi (video altında) ───────────────────── */
#nico-input-row{display:flex;gap:8px;align-items:center;padding:6px 12px;background:rgba(0,0,0,.6);border-top:1px solid rgba(255,255,255,.06);flex-shrink:0;transition:opacity .3s,max-height .3s,padding .3s;max-height:52px;overflow:hidden}
#nico-input-row.ctrls-hidden{opacity:0;max-height:0;padding-top:0;padding-bottom:0;border-top:none;pointer-events:none}
#nico-input{flex:1;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:6px 12px;color:#fff;font-size:.82rem;font-family:inherit;outline:none}
#nico-input::placeholder{color:rgba(255,255,255,.3)}
#nico-color-pick{width:26px;height:26px;border-radius:6px;border:1px solid rgba(255,255,255,.2);cursor:pointer;flex-shrink:0;padding:0}
#nico-send-btn{padding:6px 14px;border-radius:8px;border:none;background:rgba(0,245,255,.15);color:var(--accent2);font-size:.82rem;font-weight:700;font-family:inherit;cursor:pointer;flex-shrink:0;transition:background .15s}
#nico-send-btn:hover{background:rgba(0,245,255,.28)}
/* NicoNico toggle & filter */
#nico-toggle-btn{padding:5px 11px;border-radius:8px;border:1px solid rgba(0,245,255,.2);background:rgba(0,245,255,.05);color:rgba(0,245,255,.45);font-size:.76rem;font-weight:700;cursor:pointer;flex-shrink:0;font-family:inherit;transition:all .15s;white-space:nowrap}
#nico-toggle-btn.on{background:rgba(0,245,255,.14);color:var(--accent2);border-color:rgba(0,245,255,.45)}
.nico-filter-chip{display:flex;align-items:center;gap:4px;padding:5px 10px;border-radius:15px;border:1px solid rgba(255,255,255,.1);background:none;color:rgba(255,255,255,.35);font-size:.72rem;font-weight:700;cursor:pointer;flex-shrink:0;font-family:inherit;transition:all .15s;white-space:nowrap}
.nico-filter-chip.on{border-color:rgba(0,245,255,.4);background:rgba(0,245,255,.1);color:var(--accent2)}
/* Nico kapalıyken input alanlarını gizle — sadece toggle butonu görünür */
#nico-input-row.nico-off #nico-input,#nico-input-row.nico-off #nico-color-pick,#nico-input-row.nico-off #nico-send-btn,#nico-input-row.nico-off .nico-filter-chip{display:none}
/* Overlay rozetleri kontroller ile birlikte gizle */
#firstwatch-badge,#party-fab{transition:opacity .35s,visibility .35s}
.vw.ctrls-off #firstwatch-badge,.vw.ctrls-off #party-fab{opacity:0;visibility:hidden}
/* ── Ruh Hali Motoru ─────────────────────────────────────── */
#mood-overlay{position:absolute;inset:0;z-index:80;background:rgba(5,5,14,.94);backdrop-filter:blur(20px);display:none;align-items:center;justify-content:center;flex-direction:column;padding:24px;text-align:center}
#mood-overlay.on{display:flex}
.mood-title{font-size:1.1rem;font-weight:900;margin-bottom:6px}
.mood-sub{font-size:.8rem;color:rgba(255,255,255,.4);margin-bottom:20px}
.mood-emojis{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-bottom:24px}
.mood-btn{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:14px;padding:12px 16px;cursor:pointer;font-size:.88rem;font-family:inherit;color:#fff;transition:all .18s;min-width:100px;display:flex;flex-direction:column;align-items:center;gap:4px}
.mood-btn:hover{background:rgba(255,255,255,.13);border-color:rgba(255,255,255,.3);transform:scale(1.04)}
.mood-btn .me{font-size:1.8rem}
.mood-btn .ml{font-size:.72rem;color:rgba(255,255,255,.5)}
.mood-results{display:none;width:100%;max-width:600px}
.mood-results-title{font-size:.8rem;color:rgba(255,255,255,.4);margin-bottom:12px}
.mood-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.mood-anime-card{background:rgba(255,255,255,.05);border-radius:12px;overflow:hidden;text-decoration:none;transition:transform .15s}
.mood-anime-card:hover{transform:scale(1.03)}
.mood-anime-card img{width:100%;aspect-ratio:2/3;object-fit:cover;display:block}
.mood-anime-card .mac-t{padding:6px 8px;font-size:.72rem;color:#fff;font-weight:700;line-height:1.2}
.mood-close{background:none;border:1px solid rgba(255,255,255,.15);border-radius:10px;padding:8px 20px;color:rgba(255,255,255,.45);font-size:.8rem;cursor:pointer;font-family:inherit;margin-top:16px;transition:all .15s}
.mood-close:hover{color:#fff;border-color:rgba(255,255,255,.4)}
/* ── Spoiler Kutu Paneli (sidebar) ───────────────────────── */
.ps-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.07);flex-shrink:0}
.ps-tab{flex:1;padding:10px;border:none;background:none;color:rgba(255,255,255,.35);font-size:.75rem;font-weight:700;cursor:pointer;font-family:inherit;border-bottom:2px solid transparent;transition:all .15s}
.ps-tab.on{color:#fff;border-bottom-color:var(--accent2)}
.ps-tab-content{display:none;flex:1;overflow-y:auto;flex-direction:column}
.ps-tab-content.on{display:flex}
#sb-panel{padding:12px;gap:8px}
.sb-item{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:10px;padding:12px;position:relative}
.sb-item.is-spoiler{border-color:rgba(255,159,10,.2);background:rgba(255,159,10,.03)}
.sb-spoiler-mask{position:absolute;inset:0;background:rgba(10,8,20,.9);border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.78rem;color:rgba(255,159,10,.8);font-weight:700;z-index:2;backdrop-filter:blur(4px)}
.sb-body{font-size:.83rem;line-height:1.5;color:rgba(255,255,255,.82);margin-bottom:8px;word-break:break-word}
.sb-meta{display:flex;align-items:center;gap:8px;font-size:.7rem;color:rgba(255,255,255,.3)}
.sb-like-btn{background:none;border:1px solid rgba(255,255,255,.1);border-radius:20px;padding:2px 10px;color:rgba(255,255,255,.35);cursor:pointer;font-size:.7rem;font-family:inherit;display:inline-flex;align-items:center;gap:4px;transition:all .15s}
.sb-like-btn:hover,.sb-like-btn.on{background:rgba(255,45,125,.1);border-color:rgba(255,45,125,.3);color:#ff2d7d}
.sb-form{padding:10px 12px;border-top:1px solid rgba(255,255,255,.06);flex-shrink:0}
.sb-inp{width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:8px 12px;color:#fff;font-size:.82rem;font-family:inherit;resize:none;outline:none;box-sizing:border-box}
.sb-send{width:100%;margin-top:7px;padding:8px;border:none;border-radius:8px;background:rgba(255,45,125,.15);color:#ff2d7d;font-weight:700;font-size:.8rem;cursor:pointer;font-family:inherit;transition:background .15s}
.sb-send:hover{background:rgba(255,45,125,.28)}
/* ── Watch Party Modal ───────────────────────────────────────── */
#party-modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9000;align-items:center;justify-content:center}
#party-modal-bg.open{display:flex}
#party-modal{background:#0e0e1a;border:1px solid rgba(255,45,125,.25);border-radius:20px;width:100%;max-width:420px;overflow:hidden}
/* ── Prediction item ─────────────────────────────────────────── */
.pred-item{background:rgba(255,255,255,.04);border:1px solid rgba(255,159,10,.12);border-radius:10px;padding:11px 13px}
.pred-item.correct{border-color:rgba(50,220,100,.4);background:rgba(50,220,100,.06)}
.pred-item.wrong{border-color:rgba(255,80,80,.3);background:rgba(255,80,80,.04);opacity:.6}
.pred-vote-btn{background:none;border:1px solid rgba(255,159,10,.3);border-radius:20px;padding:3px 12px;font-size:.74rem;color:#ff9f0a;cursor:pointer;transition:all .15s}
.pred-vote-btn.voted{background:rgba(255,159,10,.15)}
/* ── Intro overlay ────────────────────────────────────────────── */
#intro-overlay{position:absolute;inset:0;z-index:60;background:#000;display:flex;align-items:center;justify-content:center}
#intro-overlay.hidden{display:none}
#intro-vd{width:100%;height:100%;object-fit:contain;display:block}
#intro-skip{
position:absolute;bottom:20px;right:16px;
padding:8px 18px;border-radius:8px;border:1px solid rgba(255,255,255,.3);
background:rgba(0,0,0,.65);backdrop-filter:blur(10px);
color:#fff;font-size:.82rem;font-weight:700;cursor:pointer;
font-family:inherit;transition:background .15s,border-color .15s;
display:none
}
#intro-skip:hover{background:rgba(255,255,255,.15);border-color:rgba(255,255,255,.6)}
#intro-skip.visible{display:block}
#intro-timer{color:rgba(255,255,255,.5);font-weight:400}
/* Zoom slider */
.zoom-row{padding:8px 12px 4px;display:none}
.zoom-row.on{display:block}
.zoom-slider{-webkit-appearance:none;appearance:none;width:100%;height:3px;border-radius:2px;background:rgba(255,255,255,.18);outline:none;cursor:pointer}
.zoom-slider::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent2);cursor:pointer;box-shadow:0 0 8px rgba(0,245,255,.5)}
.zoom-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--accent2);border:none;cursor:pointer}
.zoom-meta{display:flex;justify-content:space-between;font-size:.66rem;color:#444;margin-top:5px}
/* Buffer spinner (mid-play only) */
.vs{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;z-index:6;opacity:0;pointer-events:none;transition:opacity .2s}
.vs.on{opacity:1}
.vsp{width:48px;height:48px;border-radius:50%;border:3px solid rgba(255,255,255,.08);border-top-color:var(--accent2);animation:sp .75s linear infinite;box-shadow:0 0 20px rgba(0,245,255,.12)}
@keyframes sp{to{transform:rotate(360deg)}}
#vs-msg{font-size:.7rem;font-weight:700;color:rgba(255,255,255,.38);letter-spacing:.04em;transition:opacity .3s;display:none}
#vs-msg.on{display:block}
/* ── Loading screen (ilk yükleme + kaynak değişimi) ───────────────────── */
.vload{position:absolute;inset:0;z-index:22;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(4,4,14,.97);transition:opacity .38s ease;opacity:0;pointer-events:none}
.vload.on{opacity:1;pointer-events:auto}
.vload-bg{position:absolute;inset:-24px;background-size:cover;background-position:center;filter:blur(32px) brightness(.16) saturate(1.4);z-index:0;transition:opacity .6s}
.vload-inner{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:20px;padding:0 24px}
/* Orbiting rig */
.vload-rig{position:relative;width:112px;height:112px;flex-shrink:0}
.vload-ring{position:absolute;inset:0;border-radius:50%;border:1.5px solid rgba(0,245,255,.6);animation:vring 2.8s cubic-bezier(.15,.8,.15,1) infinite}
.vload-ring:nth-child(2){inset:20%;border-color:rgba(255,45,125,.45);animation-delay:.7s}
.vload-ring:nth-child(3){inset:38%;border-color:rgba(184,77,255,.4);animation-delay:1.4s}
@keyframes vring{0%{transform:scale(.45);opacity:1}100%{transform:scale(1.9);opacity:0}}
.vload-orb{position:absolute;top:50%;left:50%;border-radius:50%;margin:-4px;animation:vorbit linear infinite}
.vload-orb:nth-child(4){width:8px;height:8px;background:#00f5ff;box-shadow:0 0 10px #00f5ff;--r:46px;animation-duration:2.6s}
.vload-orb:nth-child(5){width:7px;height:7px;background:#ff2d7d;box-shadow:0 0 9px #ff2d7d;--r:37px;animation-duration:2.0s;animation-delay:-.8s}
.vload-orb:nth-child(6){width:6px;height:6px;background:#b84dff;box-shadow:0 0 8px #b84dff;--r:52px;animation-duration:3.3s;animation-delay:-1.5s}
@keyframes vorbit{from{transform:rotate(0deg) translateX(var(--r,44px)) rotate(0deg)}to{transform:rotate(360deg) translateX(var(--r,44px)) rotate(-360deg)}}
.vload-dot{position:absolute;top:50%;left:50%;width:12px;height:12px;border-radius:50%;background:#00f5ff;box-shadow:0 0 18px #00f5ff,0 0 36px rgba(0,245,255,.4);margin:-6px;animation:vdpulse 1.3s ease-in-out infinite}
@keyframes vdpulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.7);opacity:.65}}
/* Text */
.vload-lbl{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:rgba(255,255,255,.65)}
.vload-ldots::after{content:'';animation:vdots 1.6s steps(4) infinite}
@keyframes vdots{0%{content:''}25%{content:'.'}50%{content:'..'}75%{content:'...'}100%{content:''}}
.vload-tip{font-size:.72rem;color:rgba(255,255,255,.38);text-align:center;max-width:270px;min-height:34px;line-height:1.55;transition:opacity .35s ease}
.vload-tip.fade{opacity:0}
.vload-playbtn{display:none;align-items:center;gap:10px;margin-top:4px;padding:13px 34px;border-radius:50px;border:none;background:linear-gradient(135deg,#00c8d4,#e0195c);color:#fff;font-size:1rem;font-weight:800;cursor:pointer;font-family:inherit;letter-spacing:.03em;box-shadow:0 0 28px rgba(0,245,255,.3),0 6px 24px rgba(0,0,0,.55);animation:vdpulse 1.6s ease-in-out infinite}
.vload-playbtn.show{display:flex}
.vload-playbtn i{font-size:1.5rem}
.vload-retrybtn{display:none;align-items:center;gap:8px;margin-top:8px;padding:9px 22px;border-radius:50px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.07);color:rgba(255,255,255,.65);font-size:.82rem;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s,color .15s}
.vload-retrybtn:hover{background:rgba(255,255,255,.14);color:#fff}
.vload-retrybtn.show{display:flex}
/* Subtitle */
.vsub{
position:absolute;bottom:24px;left:50%;transform:translateX(-50%);
text-align:center;pointer-events:none;z-index:25;
max-width:88%;display:none;flex-direction:column;align-items:center;gap:3px;
transition:bottom .25s ease;
}
.vsub.on{display:flex}
/* Kontroller açıkken altyazıyı yukarı kaydır — kontrol barının üstünde kalsın */
.vw:not(.ctrls-off) .vsub{bottom:90px}
.vsub-line{
color:var(--sub-color,#fff);font-size:var(--sub-size,clamp(.88rem,2.2vw,1.2rem));font-weight:600;line-height:1.5;
white-space:pre-wrap;word-break:break-word;
background:var(--sub-bg,transparent);padding:var(--sub-pad,0 4px);border-radius:3px;
text-shadow:var(--sub-shadow,0 0 4px #000,0 1px 6px #000,1px 1px 0 #000,-1px 1px 0 #000,1px -1px 0 #000,-1px -1px 0 #000);
}
.vsub-line.vsub-b{
color:var(--sub-color-b,rgba(255,255,200,0.92));
}
/* Seek flash — YouTube-style */
.vrf{position:absolute;top:0;bottom:0;width:40%;pointer-events:none;z-index:26;opacity:0;display:flex;align-items:center;justify-content:center;overflow:hidden}
.vrf.L{left:0}.vrf.R{right:0}
.vrf-bg{position:absolute;inset:0;background:rgba(255,255,255,.13);border-radius:0 80% 80% 0/0 50% 50% 0}
.vrf.R .vrf-bg{border-radius:80% 0 0 80%/50% 0 0 50%}
.vrf-inner{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:6px}
.vrf-chevs{display:flex;gap:0;font-size:2rem;color:#fff}
.vrf-chevs i{opacity:0}
.vrf-sec{font-size:.78rem;font-weight:700;color:#fff;text-shadow:0 1px 5px rgba(0,0,0,.7);white-space:nowrap}
@keyframes vrfFade{0%{opacity:0}15%{opacity:1}70%{opacity:1}100%{opacity:0}}
@keyframes vrfBg{0%{transform:scale(.8)}30%{transform:scale(1)}100%{transform:scale(1.06)}}
@keyframes vrfChev{0%,100%{opacity:0}30%,75%{opacity:1}}
.vrf.go{animation:vrfFade .75s ease forwards}
.vrf.go .vrf-bg{animation:vrfBg .75s ease forwards}
.vrf.L.go .vrf-chevs i:nth-child(3){animation:vrfChev .45s .04s ease forwards}
.vrf.L.go .vrf-chevs i:nth-child(2){animation:vrfChev .45s .14s ease forwards}
.vrf.L.go .vrf-chevs i:nth-child(1){animation:vrfChev .45s .24s ease forwards}
.vrf.R.go .vrf-chevs i:nth-child(1){animation:vrfChev .45s .04s ease forwards}
.vrf.R.go .vrf-chevs i:nth-child(2){animation:vrfChev .45s .14s ease forwards}
.vrf.R.go .vrf-chevs i:nth-child(3){animation:vrfChev .45s .24s ease forwards}
/* Next ep card (controls altında kalmasın diye vc altında) */
.vnx{position:absolute;bottom:76px;right:14px;z-index:28;background:rgba(12,12,12,.96);backdrop-filter:blur(14px);border:1px solid #333;border-radius:12px;padding:14px;width:248px;max-width:calc(100% - 28px);display:none;box-shadow:0 12px 40px rgba(0,0,0,.7)}
.vnx.on{display:block}
.vnx-lbl{font-size:.68rem;text-transform:uppercase;letter-spacing:1px;color:#666;margin-bottom:5px}
.vnx-ttl{font-size:.86rem;font-weight:700;color:#f1f1f1;margin-bottom:10px}
.vnx-bar{height:3px;background:rgba(255,255,255,.1);border-radius:2px;overflow:hidden;margin-bottom:10px}
.vnx-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .5s linear;box-shadow:0 0 12px rgba(0,245,255,.3)}
.vnx-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
.vnx-hdr .vnx-lbl{margin-bottom:0}
.vnx-cancel{background:none;border:none;color:#555;cursor:pointer;padding:2px 4px;border-radius:4px;font-size:1rem;line-height:1;transition:color .15s}
.vnx-cancel:hover{color:#ccc}
.vnx-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:9px;border-radius:7px;border:none;cursor:pointer;background:linear-gradient(135deg,var(--accent),var(--accent3));color:#fff;font-size:.83rem;font-weight:700;text-decoration:none;box-shadow:0 0 24px rgba(255,45,125,.35)}
/* ── Subtitle style chips (settings menu) ───────────────────── */
.sub-sz-btn,.sub-clr-btn,.sub-bg-btn{display:inline-flex;align-items:center;justify-content:center;border:1.5px solid #444;border-radius:6px;background:#1a1a1a;color:#ccc;cursor:pointer;transition:border-color .15s,background .15s,color .15s;line-height:1}
.sub-sz-btn{padding:3px 7px;font-weight:700;font-family:inherit}
.sub-sz-btn:nth-child(1){font-size:.72rem}.sub-sz-btn:nth-child(2){font-size:.88rem}.sub-sz-btn:nth-child(3){font-size:1rem}.sub-sz-btn:nth-child(4){font-size:1.15rem}
.sub-clr-btn{width:22px;height:22px;border-radius:50%;border:2px solid #555}
.sub-bg-btn{padding:2px 8px;font-size:.7rem;font-family:inherit}
.sub-sz-btn.on,.sub-bg-btn.on{border-color:var(--accent2);background:rgba(0,200,212,.15);color:#fff}
.sub-clr-btn.on{box-shadow:0 0 0 2px var(--accent2)}
.sub-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap}
.sub-row-lbl{font-size:.7rem;color:#888;min-width:48px}
/* ── Controls ─────────────────────────────────────────────────── */
/* Mobilde tüm .vc pointer-events:none iken alt bar tıklanamaz → sadece üst boşluk none */
/* Sadece .vc.on ile görünür — .vw:hover ASLA kullanma (fare üstteyken hideCtrls işe yaramaz; mobil emülatör aynı bug) */
.vc{position:absolute;inset:0;z-index:40;display:flex;flex-direction:column;justify-content:flex-end;opacity:0;transition:opacity .35s cubic-bezier(0.25,0.46,0.45,0.94);pointer-events:none}
.vc.on{opacity:1}
.vc .vp-wrap,.vc .vcb{pointer-events:auto;touch-action:manipulation}
.vc:not(.on) .vp-wrap,.vc:not(.on) .vcb{pointer-events:none;opacity:0;transition:opacity .32s ease}
.vc.on .vp-wrap,.vc.on .vcb{pointer-events:auto;opacity:1}
/* Dar ekran: alt panel animasyonu transform YOK (transform fixed menüyü bozar / kırpar) */
@media(max-width:768px){
.vc:not(.on) .vp-wrap,.vc:not(.on) .vcb{opacity:0;pointer-events:none}
.vc.on .vp-wrap,.vc.on .vcb{opacity:1;pointer-events:auto}
}
/* gradient */
.vc::before{content:'';position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.85) 0%,rgba(0,0,0,.25) 30%,transparent 65%);pointer-events:none}
/* Progress — backdrop üstünde */
.vp-wrap{position:relative;padding:0 12px 6px;z-index:36}
.vp-bar{position:relative;height:3px;background:rgba(255,255,255,.2);border-radius:2px;cursor:pointer;transition:height .12s;margin:10px 0;touch-action:none}
.vp-bar::before{content:'';position:absolute;inset:-10px 0;cursor:pointer;touch-action:none}
.vp-bar:hover,.vp-bar:active{height:5px}
.vp-bar:hover .vp-th,.vp-bar:active .vp-th{opacity:1}
.vp-buf{position:absolute;top:0;left:0;height:100%;background:rgba(255,255,255,.25);border-radius:2px;pointer-events:none}
.vp-fill{position:absolute;top:0;left:0;height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:2px;pointer-events:none;box-shadow:0 0 10px rgba(0,245,255,.25)}
.vp-th{position:absolute;top:50%;right:-6px;width:13px;height:13px;border-radius:50%;background:#fff;transform:translateY(-50%);pointer-events:none;opacity:0;transition:opacity .12s;box-shadow:0 2px 6px rgba(0,0,0,.5)}
.vp-tip{position:absolute;bottom:calc(100% + 14px);background:rgba(6,6,13,.92);border:1px solid rgba(255,255,255,.12);color:#fff;font-size:.7rem;font-weight:700;padding:0;border-radius:8px;transform:translateX(-50%);display:none;white-space:nowrap;pointer-events:none;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.6)}
.vp-bar:hover .vp-tip,.vp-tip.force-show{display:block}
#vptip-thumb{display:block;width:160px;height:90px;background:#111}
#vptip-time{display:block;text-align:center;padding:4px 6px;font-size:.72rem;font-weight:700;color:#fff}
/* Skip segment marker on progress bar */
.vp-skip-mark{position:absolute;top:0;height:100%;width:3px;background:var(--accent2);border-radius:2px;pointer-events:none;opacity:.85}
/* Skip banner button */
/* Shared skip button style */
#skip-seg-btn,#aniskip-btn{
position:absolute;right:20px;bottom:80px;
display:none;align-items:center;gap:7px;
padding:10px 18px;border-radius:12px;
background:rgba(8,8,18,.88);backdrop-filter:blur(12px);
color:#fff;font-size:.88rem;font-weight:700;cursor:pointer;font-family:inherit;
box-shadow:0 4px 20px rgba(0,0,0,.5);
transition:opacity .2s,background .15s;z-index:25;
border:1px solid rgba(0,245,255,.45);
letter-spacing:.01em;
}
#skip-seg-btn.visible,#aniskip-btn.visible{display:flex}
#skip-seg-btn:hover,#aniskip-btn:hover{background:rgba(0,245,255,.15);border-color:rgba(0,245,255,.8)}
/* Control bar — sol | orta(süre) | sağ(fs dahil) */
.vcb{
display:flex;align-items:center;justify-content:space-between;
padding:4px 10px calc(12px + env(safe-area-inset-bottom, 0px));
gap:4px;
position:relative;z-index:36;width:100%;box-sizing:border-box;flex-wrap:nowrap;
}
.vcb-l{display:flex;align-items:center;gap:2px;flex:0 0 auto;min-width:0}
.vcb-time{
flex:1 1 auto;min-width:0;display:flex;align-items:center;justify-content:center;
padding:0 4px;pointer-events:none;
}
.vcb-r{display:flex;align-items:center;gap:2px;flex:0 0 auto;min-width:0}
/* Masaüstünde dublaj/kaynak ayrı buton; mobilde menü içinde (.dub-in-menu) */
.dub-in-menu{display:none}
@media(max-width:768px){
.dub-bar,.src-bar{display:none}
.dub-in-menu{display:block}
.vvr,.cb.cb-kbhelp{display:none}
}
.vm-sep{height:1px;background:#222;margin:8px 0}
.cb{background:none;border:none;color:rgba(255,255,255,.85);font-size:1.1rem;cursor:pointer;padding:7px 7px;border-radius:6px;line-height:1;display:flex;align-items:center;gap:4px;transition:color .12s,background .12s;white-space:nowrap;flex-shrink:0;font-family:inherit}
.cb:hover{color:#fff;background:rgba(255,255,255,.1)}
.cb.on{color:var(--accent2);text-shadow:0 0 12px rgba(0,245,255,.4)}
.cb .lbl{font-size:.72rem;font-weight:700}
.cb.skbtn{flex-direction:column;align-items:center;padding:5px 6px;font-size:.9rem}
.cb.skbtn small{font-size:.6rem;font-weight:700;line-height:1;margin-top:-1px}
/* Volume */
.vcv{display:flex;align-items:center;gap:4px}
.vvr{-webkit-appearance:none;appearance:none;width:68px;height:3px;border-radius:2px;cursor:pointer;outline:none;background:linear-gradient(90deg,rgba(255,255,255,.82) var(--v,100%),rgba(255,255,255,.18) var(--v,100%))}
.vvr::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#fff;cursor:pointer}
/* Time */
.vtime{font-size:.75rem;font-weight:600;color:rgba(255,255,255,.82);user-select:none;white-space:nowrap;letter-spacing:.02em}
/* Dropdown — açıkken vdrop yükselir (diğer butonların üstüne) */
.vdrop{position:relative;z-index:1}
.vdrop.drop-open{z-index:200}
@supports selector(:has(*)){
.vdrop:has(.vmenu.on){z-index:200}
}
.vmenu{
position:absolute;right:0;bottom:calc(100% + 10px);left:auto;
background:rgba(10,10,22,.97);backdrop-filter:blur(18px);border:1px solid rgba(0,245,255,.2);border-radius:12px;padding:8px;
min-width:188px;max-width:min(300px,calc(100vw - 20px));max-height:min(52vh,420px);
overflow-y:auto;-webkit-overflow-scrolling:touch;display:none;z-index:210;
box-shadow:0 12px 48px rgba(0,0,0,.75),0 0 32px rgba(0,245,255,.08);
}
.vmenu.on{display:block}
/* Telefon: menü viewporta sabit alt panel */
@media(max-width:768px){
.vmenu{
position:fixed !important;
left:8px !important;right:8px !important;top:auto !important;
bottom:max(8px, env(safe-area-inset-bottom, 0px)) !important;
width:auto !important;min-width:0 !important;max-width:none !important;
max-height:min(72vh, 520px) !important;
z-index:10002 !important;
border-radius:18px 18px 14px 14px;
}
/* Mobile backdrop — menünün arkasını karartır, tıklayınca kapatır */
.vm-mobile-backdrop{
display:none;
position:fixed;inset:0;
z-index:10001;
background:rgba(0,0,0,.55);
backdrop-filter:blur(2px);
-webkit-backdrop-filter:blur(2px);
}
.vm-mobile-backdrop.on{display:block}
}
/* vc içinde: menülerden düşük, video alanını karartır */
.vm-backdrop{display:none}
.vmt{font-size:.67rem;text-transform:uppercase;letter-spacing:1px;color:#555;font-weight:700;padding:4px 10px 8px}
.vmi{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:6px;cursor:pointer;color:rgba(255,255,255,.7);font-size:.83rem;transition:background .12s}
.vmi:hover{background:rgba(255,255,255,.07);color:#fff}
.vmi.on{color:var(--accent2)}
.vmi .dot{width:5px;height:5px;border-radius:50%;flex-shrink:0}
.vmi.on .dot{background:var(--accent2);box-shadow:0 0 8px rgba(0,245,255,.6)}
.vmi:not(.on) .dot{border:1px solid rgba(255,255,255,.2)}
/* ── Player info bar ──────────────────────────────────────────── */
.pinfo{background:linear-gradient(180deg,#0a0a16,#06060d);padding:14px 20px 16px;border-bottom:1px solid rgba(255,255,255,.06)}
.pinfo-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:6px}
.pinfo-ep-meta{font-size:.72rem;color:var(--accent2);font-weight:700;margin-bottom:2px;letter-spacing:.3px;text-shadow:0 0 14px rgba(0,245,255,.2)}
.pinfo-ep-title{font-size:1.02rem;font-weight:800;color:#eee;line-height:1.3;margin-bottom:4px}
.pinfo-stats{display:flex;gap:10px;color:#555;font-size:.75rem;flex-wrap:wrap}
.pinfo-stats span{display:flex;align-items:center;gap:3px}
.pnav{display:flex;gap:6px;flex-shrink:0}
.pnav-btn{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;border-radius:7px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#666;font-size:.78rem;font-weight:600;cursor:pointer;text-decoration:none;transition:all .13s;font-family:inherit}
.pnav-btn:hover{background:rgba(0,245,255,.07);color:var(--text);border-color:rgba(0,245,255,.3)}
.pnav-btn.pnav-next{background:rgba(0,245,255,.08);border-color:rgba(0,245,255,.25);color:var(--accent2)}
.pnav-btn.pnav-next:hover{background:rgba(0,245,255,.15)}
/* Sub chips */
.sub-row{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:6px}
.sub-row-lbl{font-size:.66rem;text-transform:uppercase;letter-spacing:1px;color:#444;font-weight:700;flex-shrink:0}
.schip{padding:3px 10px;border-radius:10px;font-size:.73rem;font-weight:700;cursor:pointer;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);color:#555;transition:all .12s;user-select:none}
.schip:hover{border-color:rgba(0,245,255,.5);color:var(--accent2)}
.schip.on{background:rgba(255,45,125,.12);border-color:var(--accent);color:var(--accent)}
/* ── Sidebar ──────────────────────────────────────────────────── */
.ps-hdr{padding:13px 13px 0;flex-shrink:0;border-bottom:1px solid #1e1e1e}
.ps-hdr h3{font-size:.72rem;text-transform:uppercase;letter-spacing:1px;color:#444;font-weight:700;margin-bottom:10px}
.ps-stabs{display:flex;gap:5px;flex-wrap:wrap;padding-bottom:11px}
.pst{padding:3px 10px;border-radius:10px;font-size:.72rem;font-weight:700;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);color:#444;cursor:pointer;transition:all .13s;font-family:inherit}
.pst:hover{color:#aaa}
.pst.on{background:rgba(0,245,255,.1);border-color:rgba(0,245,255,.35);color:var(--accent2)}
.ps-list{flex:1;overflow-y:auto;padding:6px}
.ps-list::-webkit-scrollbar{width:3px}
.ps-list::-webkit-scrollbar-thumb{background:#222;border-radius:2px}
.ps-item{display:flex;gap:8px;align-items:center;padding:7px 7px;border-radius:8px;cursor:pointer;text-decoration:none;color:inherit;border:1px solid transparent;margin-bottom:3px;transition:background .12s}
.ps-item:hover{background:rgba(255,255,255,.04)}
.ps-item.now{background:rgba(0,245,255,.08);border-color:rgba(0,245,255,.22)}
.ps-item.ps-watched .ps-name{color:rgba(255,255,255,.35)}
.ps-thumb{width:84px;flex-shrink:0;border-radius:5px;overflow:hidden;background:#1a1a1a;position:relative}
.ps-thumb img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}
.ps-thumb .nth{aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;color:#2a2a2a}
.ps-thumb .nowmark{position:absolute;inset:0;background:rgba(0,245,255,.15);display:flex;align-items:center;justify-content:center;color:var(--accent2);font-size:1rem}
/* Progress bar on thumbnail */
.ps-prog{position:absolute;bottom:0;left:0;height:3px;width:var(--p,0%);background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:0 2px 2px 0}
.ps-info{flex:1;min-width:0}
.ps-num{font-size:.66rem;color:#444;font-weight:700;margin-bottom:2px}
.ps-name{font-size:.78rem;font-weight:600;color:rgba(255,255,255,.65);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.35}
.ps-item.now .ps-name{color:var(--accent2)}
.ps-pct{font-size:.6rem;color:var(--accent2);opacity:.7;margin-top:2px;font-weight:700}
@media(max-width:1100px){
.pw{flex-direction:column}
.pw-side{width:100%;height:auto;max-height:400px;position:static;border-left:none;border-top:1px solid #1e1e1e;order:2}
.pw-main{order:1}
.vvr{display:none}
.ep-below{padding:0 14px 36px}
}
@media(max-width:600px){
.vsub{bottom:16px}
.vw:not(.ctrls-off) .vsub{bottom:76px}
.vsub-line{font-size:.88rem}
.pinfo{padding:10px 14px 14px}
.pinfo-row{flex-wrap:nowrap;align-items:center;gap:8px}
.pinfo-row>div:first-child{min-width:0;flex:1;overflow:hidden}
.pinfo-ep-title{font-size:.85rem;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;margin-bottom:2px}
.pinfo-ep-meta{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:.68rem}
.pinfo-stats{flex-wrap:nowrap;overflow:hidden;gap:8px}
.pinfo-stats span:nth-child(n+3){display:none}
.pnav{gap:4px;flex-shrink:0}
.pnav-btn{padding:5px 8px;font-size:.72rem;gap:3px}
.eb-info,.eb-cmt{padding-left:14px;padding-right:14px}
.vcb{padding:4px 6px 10px;gap:4px}
.vcb-l{gap:0}
.vcb-time{padding:0 4px}
.vcb-r{gap:0}
/* 44px minimum touch target (iOS/Android HIG) */
.cb{min-width:44px;min-height:44px;padding:6px 6px;font-size:.95rem;justify-content:center}
.cb:active{background:rgba(255,255,255,.15)}
.cb.skbtn{padding:4px 4px;min-width:44px}
.cb .lbl{max-width:64px;overflow:hidden;text-overflow:ellipsis;font-size:.65rem}
.vtime{font-size:.68rem}
/* Bigger progress bar touch area on mobile */
.vp-bar::before{inset:-14px 0}
/* Skip buttons: lower position so they don't crowd the control bar */
#skip-seg-btn,#aniskip-btn{bottom:calc(62px + env(safe-area-inset-bottom, 0px));right:12px;padding:8px 14px;font-size:.82rem;gap:5px}
/* Sidebar: cap height on small phones */
.pw-side{max-height:260px}
}
@media(max-width:480px){
/* NicoNico row: hide filter chip (too cramped), allow wrap, ease padding */
#nico-input-row{max-height:none;flex-wrap:wrap;gap:5px;padding:5px 8px}
#nico-input-row .nico-filter-chip{display:none!important}
#nico-color-pick{width:22px;height:22px;border-radius:5px}
#nico-send-btn{padding:5px 10px;font-size:.78rem}
#nico-toggle-btn{font-size:.72rem;padding:4px 9px}
/* Mood buttons: smaller on narrow screens */
.mood-btn{min-width:88px;padding:10px 12px}
.mood-title{font-size:1rem}
}
@media(max-width:520px){
/* Seek butonlarını gizle — double-tap zaten var, fullscreen görünsün */
.skbtn{display:none !important}
}
@media(max-width:380px){
/* Very narrow: mood results 2-col grid */
.mood-grid{grid-template-columns:repeat(2,1fr)}
.mood-emojis{gap:7px}
.mood-btn{min-width:0;flex:1 1 calc(50% - 7px);max-width:none}
}
/* ── Episode below ──────────────────────────────────────────────── */
.ep-below{padding:0;max-width:100%}
/* Tabs */
.eb-tabs{display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,.07);background:#06060d;padding:0 12px;position:sticky;top:var(--nav-h);z-index:10;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.eb-tabs::-webkit-scrollbar{display:none}
.eb-tab{padding:12px 14px;font-size:.82rem;font-weight:700;color:#555;cursor:pointer;border:none;background:none;font-family:inherit;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .15s,border-color .15s;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0}
.eb-tab:hover{color:#aaa}
.eb-tab.on{color:var(--accent2);border-bottom-color:var(--accent2)}
.eb-tab .tab-badge{background:rgba(0,245,255,.15);color:var(--accent2);font-size:.65rem;padding:2px 6px;border-radius:8px;font-weight:800}
/* Tab panels */
.eb-panel{display:none;animation:fadeIn .2s ease}
.eb-panel.on{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@keyframes unmute-pulse{0%,100%{box-shadow:0 4px 24px rgba(255,45,125,.3)}50%{box-shadow:0 4px 32px rgba(255,45,125,.7),0 0 0 4px rgba(255,45,125,.2)}}
/* Info panel */
.eb-info{padding:24px 20px 40px;max-width:900px;margin:0 auto}
@media(max-width:1100px){.eb-info{max-width:100%}}
/* Anime card */
.ep-card{display:flex;gap:20px;background:linear-gradient(135deg,rgba(0,245,255,.04) 0%,rgba(255,45,125,.025) 100%);border:1px solid rgba(255,255,255,.07);border-radius:14px;padding:20px;margin-bottom:24px;position:relative;overflow:hidden}
.ep-card::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at top left,rgba(0,245,255,.06),transparent 60%);pointer-events:none}
.ep-card-poster{width:100px;flex-shrink:0;border-radius:9px;overflow:hidden;box-shadow:0 8px 28px rgba(0,0,0,.5);align-self:flex-start}
.ep-card-poster img{width:100%;display:block;aspect-ratio:2/3;object-fit:cover}
.ep-card-body{flex:1;min-width:0}
.ep-card-anime-title{font-size:.72rem;text-transform:uppercase;letter-spacing:.12em;color:var(--accent2);font-weight:700;margin-bottom:5px;text-shadow:0 0 14px rgba(0,245,255,.2)}
.ep-card-anime-title a{color:inherit;text-decoration:none}
.ep-card-anime-title a:hover{text-decoration:underline}
.ep-card-ep-title{font-size:1.05rem;font-weight:800;color:#f0f0f0;line-height:1.3;margin-bottom:10px}
.ep-card-stats{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:12px}
.ep-stat{display:flex;align-items:center;gap:5px;font-size:.75rem;color:#666;font-weight:600}
.ep-stat i{font-size:.82rem}
.ep-stat.stat-views{color:#888}
.ep-stat.stat-dur{color:#666}
.ep-stat.stat-dub{color:rgba(0,245,255,.6);background:rgba(0,245,255,.08);border:1px solid rgba(0,245,255,.15);border-radius:6px;padding:2px 8px}
.ep-stat.stat-rating{color:#ffd60a}
/* Description */
.ep-desc-wrap{margin-bottom:16px}
.ep-desc-lbl{font-size:.68rem;text-transform:uppercase;letter-spacing:.1em;color:#444;font-weight:700;margin-bottom:8px}
.ep-desc-text{color:#888;font-size:.88rem;line-height:1.75;margin:0;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4}
.ep-desc-text.expanded{-webkit-line-clamp:unset;display:block}
.ep-expand-btn{background:none;border:none;color:var(--accent2);font-size:.78rem;font-weight:700;cursor:pointer;padding:5px 0;font-family:inherit;margin-top:4px}
.ep-expand-btn:hover{text-decoration:underline}
/* Genre / tag chips */
.ep-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:4px}
.ep-tag{padding:3px 10px;border-radius:10px;font-size:.72rem;font-weight:700;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#666;text-decoration:none;transition:all .12s}
.ep-tag:hover{border-color:rgba(255,45,125,.5);color:var(--accent)}
/* Anime meta row */
.ep-meta-row{display:flex;gap:20px;flex-wrap:wrap;padding-top:16px;border-top:1px solid rgba(255,255,255,.05);margin-top:4px}
.ep-meta-item{display:flex;flex-direction:column;gap:2px}
.ep-meta-key{font-size:.62rem;text-transform:uppercase;letter-spacing:.1em;color:#3a3a5a;font-weight:700}
.ep-meta-val{font-size:.8rem;font-weight:600;color:#777}
@media(max-width:600px){
.ep-card{gap:14px;padding:14px}
.ep-card-poster{width:72px}
.ep-card-ep-title{font-size:.92rem}
}
@media(max-width:400px){
.ep-card-poster{display:none}
}
/* ── Comments panel ──────────────────────────────────────── */
.eb-cmt{padding:20px 20px 48px;max-width:900px;margin:0 auto}
@media(max-width:1100px){.eb-cmt{max-width:100%}}
/* ── Watermark cover ─────────────────────────────────── */
#wm-cover{
position:absolute;
top:0;left:50%;transform:translateX(-50%);
width:clamp(160px,28%,280px);
height:clamp(32px,8%,54px);
background:rgba(0,0,0,.92);
backdrop-filter:blur(10px);
border-radius:0 0 12px 12px;
z-index:30;
pointer-events:none;
transition:opacity .6s ease;
display:flex;align-items:center;justify-content:center;
}
#wm-cover.hide{opacity:0}
#wm-cover span{
font-family:var(--font-display,'Orbitron',sans-serif);
font-size:clamp(.6rem,1.6vw,.8rem);
font-weight:800;letter-spacing:.18em;
background:linear-gradient(105deg,#fff,var(--accent2,#00f5ff),var(--accent,#ff2d7d));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
text-shadow:none;opacity:.95;
}
/* Dikey (portrait) tam ekran — vw çok küçük olur, vmin kullan */
@media(orientation:portrait){
#wm-cover{
width:clamp(180px,52%,280px);
height:clamp(34px,12%,52px);
}
#wm-cover span{font-size:clamp(.68rem,3vmin,.82rem)}
}
/* Fullscreen safe-area (notch) desteği */
#vw:fullscreen #wm-cover,
#vw:-webkit-full-screen #wm-cover{
top:env(safe-area-inset-top,0px);
width:clamp(180px,38%,300px);
height:clamp(34px,7%,56px);
}
#vw:fullscreen #wm-cover span,
#vw:-webkit-full-screen #wm-cover span{
font-size:clamp(.7rem,2.2vmin,.85rem);
}
@media(orientation:portrait){
#vw:fullscreen #wm-cover,
#vw:-webkit-full-screen #wm-cover{
width:clamp(180px,55%,300px);
height:clamp(36px,14%,56px);
}
#vw:fullscreen #wm-cover span,
#vw:-webkit-full-screen #wm-cover span{
font-size:clamp(.7rem,3.5vmin,.85rem);
}
}
/* ── iOS fake fullscreen ────────────────────────────── */
#vw.ios-fullscreen{
position:fixed!important;top:0;left:0;
width:100vw!important;height:100dvh!important;
z-index:99999;background:#000;
}
body.ios-fs-active{overflow:hidden!important;touch-action:none}
/* ── Comments ────────────────────────────────────────── */
.cmt-section{margin-top:8px}
.cmt-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.cmt-title{font-size:.95rem;font-weight:800;color:var(--text);display:flex;align-items:center;gap:8px}
.cmt-count{font-size:.78rem;color:var(--text3)}
/* Form */
.cmt-form-wrap{display:flex;gap:12px;margin-bottom:20px}
.cmt-form-avatar{
width:36px;height:36px;border-radius:50%;flex-shrink:0;
background:linear-gradient(135deg,var(--accent2),var(--accent));
display:flex;align-items:center;justify-content:center;
font-size:.82rem;font-weight:800;color:#06060d;overflow:hidden;
}
.cmt-form-body{flex:1;min-width:0}
.cmt-input{
width:100%;background:rgba(17,17,31,.9);border:1px solid var(--border);
border-radius:var(--r);padding:11px 14px;color:var(--text);font-size:.88rem;
resize:none;outline:none;font-family:inherit;min-height:44px;max-height:180px;
transition:border-color .18s,box-shadow .18s;line-height:1.5;
}
.cmt-input:focus{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(0,245,255,.15)}
.cmt-form-actions{display:flex;align-items:center;justify-content:space-between;margin-top:8px}
.cmt-gif-btn{
background:rgba(17,17,31,.8);border:1px solid var(--border);color:var(--text2);
padding:7px 14px;border-radius:var(--r);font-size:.8rem;font-weight:700;cursor:pointer;
font-family:inherit;transition:all .15s;display:flex;align-items:center;gap:5px;
}
.cmt-gif-btn:hover{border-color:var(--accent2);color:var(--accent2)}
.cmt-submit-btn{
background:linear-gradient(135deg,var(--accent),var(--accent3));
border:none;color:#fff;padding:8px 20px;border-radius:var(--r);
font-size:.82rem;font-weight:700;cursor:pointer;font-family:inherit;
display:flex;align-items:center;gap:6px;transition:transform .15s,box-shadow .15s;
box-shadow:0 0 20px rgba(255,45,125,.25);
}
.cmt-submit-btn:hover{transform:translateY(-1px);box-shadow:0 0 28px rgba(255,45,125,.4)}
.cmt-submit-btn:disabled{opacity:.5;pointer-events:none}
/* GIF Picker */
.gif-picker{
background:rgba(10,10,22,.98);border:1px solid var(--border);
border-radius:var(--r2);padding:14px;margin-bottom:18px;
box-shadow:0 12px 48px rgba(0,0,0,.55);
}
.gif-search-wrap{position:relative;margin-bottom:12px}
.gif-search-input{
width:100%;background:rgba(17,17,31,.9);border:1px solid var(--border);
border-radius:var(--r);padding:10px 14px 10px 36px;color:var(--text);font-size:.86rem;outline:none;font-family:inherit;
}
.gif-search-input:focus{border-color:var(--accent2)}
.gif-results{
display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:6px;
max-height:260px;overflow-y:auto;
}
.gif-results img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px;cursor:pointer;transition:opacity .15s}
.gif-results img:hover{opacity:.8}
.gif-loading{grid-column:1/-1;text-align:center;color:var(--text3);font-size:.84rem;padding:20px}
/* Comment items */
.cmt-item{
display:flex;gap:13px;align-items:flex-start;
margin-bottom:10px;padding:11px 14px;
border-radius:13px;
background:rgba(255,255,255,.018);
border:1px solid rgba(255,255,255,.05);
transition:background .15s,border-color .15s;
}
.cmt-item:hover{background:rgba(255,255,255,.03);border-color:rgba(255,255,255,.09)}
/* Avatar — no-frame variant (direct flex child) */
.cmt-ava{
width:40px;height:40px;border-radius:50%;flex-shrink:0;
background:linear-gradient(135deg,rgba(0,245,255,.55),rgba(255,45,125,.55));
display:flex;align-items:center;justify-content:center;
font-size:.84rem;font-weight:800;color:#fff;overflow:hidden;
}
.cmt-ava.admin{background:linear-gradient(135deg,var(--accent),var(--accent3))}
/* Avatar frame wrapper — frame variant */
.cmt-ava-wrap{
position:relative;flex-shrink:0;
width:46px;height:46px;align-self:flex-start;
}
.cmt-ava-wrap::before{
content:'';position:absolute;inset:0;
border-radius:50%;
animation:frame-spin 4s linear infinite;
pointer-events:none;
}
/* Avatar inside wrapper sits at inset:3px, covering ring center */
.cmt-ava-wrap .cmt-ava{
position:absolute;inset:3px;
width:auto;height:auto;flex-shrink:initial;
}
/* Per-frame ring colors */
.cmt-ava-wrap.fr-neon::before {background:conic-gradient(#00f5ff,#b84dff,#00f5ff);box-shadow:0 0 8px rgba(0,245,255,.45)}
.cmt-ava-wrap.fr-fire::before {background:conic-gradient(#ff6b35,#ff2d7d,#ff8c00,#ff6b35);box-shadow:0 0 8px rgba(255,107,53,.45)}
.cmt-ava-wrap.fr-galaxy::before {background:conic-gradient(#7c3aed,#b84dff,#4a1d96,#7c3aed);box-shadow:0 0 8px rgba(124,58,237,.45)}
.cmt-ava-wrap.fr-gold::before {background:conic-gradient(#ffd700,#ff8c00,#ffd700);box-shadow:0 0 10px rgba(255,215,0,.4)}
.cmt-ava-wrap.fr-sakura::before {background:conic-gradient(#ff9ec4,#ff2d7d,#ffd6e7,#ff9ec4);box-shadow:0 0 8px rgba(255,158,196,.4)}
.cmt-ava-wrap.fr-rainbow::before{background:conic-gradient(#ff0000,#ff8c00,#ffd700,#00c800,#0088ff,#8b00ff,#ff0080,#ff0000)}
.cmt-ava-wrap.fr-ice::before {background:conic-gradient(#a8edff,#60cfff,#a8edff);box-shadow:0 0 8px rgba(168,237,255,.4)}
.cmt-ava-wrap.fr-blood::before {background:conic-gradient(#8b0000,#dc143c,#8b0000);box-shadow:0 0 8px rgba(220,20,60,.4)}
.cmt-ava-wrap.fr-mint::before {background:conic-gradient(#00ff7f,#00d4a4,#00ff7f);box-shadow:0 0 8px rgba(0,255,127,.35)}
.cmt-ava-wrap.fr-ocean::before {background:conic-gradient(#006fbf,#00d2ff,#006fbf);box-shadow:0 0 8px rgba(0,210,255,.4)}
.cmt-ava-wrap.fr-poison::before {background:conic-gradient(#6b0ac9,#c300ff,#6b0ac9);box-shadow:0 0 8px rgba(195,0,255,.4)}
.cmt-body{flex:1;min-width:0}
.cmt-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:5px}
.cmt-name{font-size:.84rem;font-weight:700;color:var(--text);text-decoration:none}
.cmt-name:not([class*="username-color-"]):hover{color:#fff}
.cmt-badge{font-size:.6rem;padding:2px 7px;border-radius:10px;font-weight:800;letter-spacing:.06em}
.cmt-badge-admin{background:rgba(255,45,125,.2);color:var(--accent);border:1px solid rgba(255,45,125,.3)}
.cmt-badge-pinned{background:rgba(0,245,255,.1);color:var(--accent2);border:1px solid rgba(0,245,255,.25)}
.cmt-badge-staff{background:rgba(255,191,0,.15);color:#ffbf00;border:1px solid rgba(255,191,0,.3)}
.cmt-time{font-size:.72rem;color:var(--text3)}
.cmt-text{font-size:.88rem;color:var(--text2);line-height:1.6;word-break:break-word;white-space:pre-wrap}
.cmt-gif{max-width:240px;border-radius:8px;margin-top:8px;display:block}
.cmt-actions{display:flex;align-items:center;gap:14px;margin-top:8px}
.cmt-action-btn{
background:none;border:none;cursor:pointer;color:var(--text3);font-size:.76rem;font-weight:700;
font-family:inherit;display:flex;align-items:center;gap:4px;padding:4px 0;transition:color .14s;
}
.cmt-action-btn:hover{color:var(--text)}
.cmt-action-btn.liked{color:var(--accent);text-shadow:0 0 12px rgba(255,45,125,.4)}
.cmt-follow-btn{
background:none;border:1px solid rgba(0,245,255,.25);cursor:pointer;
color:rgba(0,245,255,.6);font-size:.68rem;font-weight:700;font-family:inherit;
padding:2px 7px;border-radius:20px;transition:all .15s;
display:inline-flex;align-items:center;gap:3px;
}
.cmt-follow-btn:hover{background:rgba(0,245,255,.12);color:var(--accent2);border-color:var(--accent2)}
.cmt-follow-btn.following{border-color:rgba(255,255,255,.15);color:rgba(255,255,255,.35)}
.cmt-follow-btn.following:hover{border-color:rgba(255,80,80,.4);color:#ff6b6b;background:rgba(255,80,80,.08)}
.cmt-mention{color:var(--accent2);font-weight:700;text-decoration:none}
.cmt-mention:hover{text-decoration:underline}
.cmt-action-btn.liked i{filter:drop-shadow(0 0 6px rgba(255,45,125,.5))}
/* Replies */
.cmt-replies{margin-top:10px;margin-left:4px;padding-left:16px;border-left:2px solid rgba(0,245,255,.12)}
.cmt-replies .cmt-item{padding:8px 12px;margin-bottom:6px;border-radius:10px}
.cmt-replies .cmt-ava{width:30px;height:30px;font-size:.72rem}
.cmt-replies .cmt-ava-wrap{width:36px;height:36px}
.cmt-replies .cmt-name{font-size:.8rem}
.reply-form-wrap{display:flex;gap:10px;margin-top:12px}
.reply-input{
flex:1;background:rgba(17,17,31,.9);border:1px solid var(--border);
border-radius:var(--r);padding:9px 13px;color:var(--text);font-size:.84rem;
outline:none;font-family:inherit;min-height:38px;resize:none;
}
.reply-input:focus{border-color:var(--accent2)}
.reply-submit-btn{
background:var(--accent);border:none;color:#fff;
padding:9px 16px;border-radius:var(--r);font-size:.8rem;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap;
}
.cmt-login-prompt{
text-align:center;padding:28px 20px;color:var(--text3);font-size:.88rem;
background:rgba(17,17,31,.5);border:1px solid var(--border);border-radius:var(--r2);margin-bottom:20px;
}
.cmt-empty{text-align:center;padding:32px 20px;color:var(--text3);font-size:.88rem}
.cmt-load-btn{
background:rgba(17,17,31,.8);border:1px solid var(--border);color:var(--text2);
padding:10px 24px;border-radius:var(--r);font-size:.84rem;font-weight:700;cursor:pointer;font-family:inherit;transition:all .15s;
}
.cmt-load-btn:hover{border-color:var(--accent2);color:var(--accent2)}
/* ═══════════════════════════════════════════════════
PLAYER PAGE PREMIUM ENHANCEMENTS
═══════════════════════════════════════════════════ */
/* Video progress bar glow */
.vp-fill{
box-shadow:0 0 14px rgba(0,245,255,.45),0 0 28px rgba(255,45,125,.2);
transition:width .1s linear;
}
/* Video thumb enhanced */
.vp-th{
width:16px;height:16px;
background:linear-gradient(135deg,#fff,rgba(0,245,255,.9));
box-shadow:0 0 12px rgba(0,245,255,.7),0 2px 8px rgba(0,0,0,.6);
}
/* Control buttons enhanced */
.cb:hover{
color:#fff;background:rgba(0,245,255,.1);
box-shadow:0 0 16px rgba(0,245,255,.1);
}
/* Video controls gradient stronger */
.vc::before{
background:linear-gradient(to top,rgba(0,0,0,.9) 0%,rgba(0,0,0,.3) 40%,transparent 70%);
}
/* Current episode sidebar highlight */
.ps-item.now{
background:linear-gradient(135deg,rgba(0,245,255,.1),rgba(255,45,125,.05));
border-color:rgba(0,245,255,.3);
box-shadow:inset 0 0 0 1px rgba(0,245,255,.15);
}
.ps-item.now .ps-name{
color:var(--accent2);
text-shadow:0 0 12px rgba(0,245,255,.3);
}
/* Episode sidebar item hover */
.ps-item:hover{
background:rgba(0,245,255,.06);
border-color:rgba(0,245,255,.12);
}
/* Pinfo bar enhanced */
.pinfo{
background:linear-gradient(180deg,rgba(10,10,22,.98),rgba(6,6,13,.99));
border-bottom:1px solid rgba(0,245,255,.08);
}
.pinfo-ep-meta{
background:linear-gradient(90deg,var(--accent2),var(--accent));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
font-size:.75rem;font-weight:800;
}
.pinfo-ep-title{
font-size:1.05rem;font-weight:800;
color:#f0f4ff;
}
/* Next episode card enhanced */
.vnx{
background:rgba(8,8,20,.97);
border:1px solid rgba(0,245,255,.2);
box-shadow:0 12px 48px rgba(0,0,0,.75),0 0 30px rgba(0,245,255,.06);
}
.vnx-btn{
box-shadow:0 0 28px rgba(255,45,125,.4),0 4px 16px rgba(0,0,0,.4);
transition:transform .15s,box-shadow .18s;
}
.vnx-btn:hover{transform:translateY(-2px);box-shadow:0 0 40px rgba(255,45,125,.55)}
.vnx-fill{background:linear-gradient(90deg,var(--accent2),var(--accent));box-shadow:0 0 14px rgba(0,245,255,.35)}
/* Skip button enhanced */
#skip-seg-btn,#aniskip-btn{
border:1px solid rgba(0,245,255,.5);
box-shadow:0 0 20px rgba(0,245,255,.15),0 4px 20px rgba(0,0,0,.4);
backdrop-filter:blur(14px);
}
/* Comment section enhanced */
.cmt-input:focus{
border-color:var(--accent2);
box-shadow:0 0 0 2px rgba(0,245,255,.12),0 0 20px rgba(0,245,255,.06);
}
/* Episode tabs enhanced */
.eb-tab.on{
color:var(--accent2);
text-shadow:0 0 12px rgba(0,245,255,.3);
}
/* Below player ep card enhanced */
.ep-card{
transition:border-color .2s,box-shadow .2s;
}
.ep-card:hover{
border-color:rgba(0,245,255,.18);
box-shadow:0 8px 32px rgba(0,0,0,.4),0 0 30px rgba(0,245,255,.05);
}
/* Anime info card in player enhanced */
.ec-poster{box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 40px rgba(0,245,255,.06)}
/* Spinner enhanced */
.vsp{
border:3px solid rgba(255,255,255,.08);
border-top-color:var(--accent2);
box-shadow:0 0 24px rgba(0,245,255,.35),inset 0 0 12px rgba(0,245,255,.05);
}
/* Volume slider enhanced */
.vvr::-webkit-slider-thumb{
box-shadow:0 0 8px rgba(255,255,255,.4);
}
/* Watch party FAB enhanced */
#party-fab{
box-shadow:0 0 28px rgba(255,45,125,.4),0 4px 20px rgba(0,0,0,.4);
}
/* Mobile: video aspect ratio preserved */
@media(max-width:600px){
.pw-main{background:#000}
.pinfo-ep-title{font-size:.9rem}
}
/* NicoNico input enhanced */
#nico-input:focus{
border-color:rgba(0,245,255,.4);
box-shadow:0 0 0 2px rgba(0,245,255,.1);
background:rgba(255,255,255,.1);
}
/* ── Kaynak Seçici Tab Bar ──────────────────────────────────────────── */
.src-tab-bar{
background:linear-gradient(180deg,#09090f 0%,#0b0b18 100%);
border-bottom:1px solid rgba(255,255,255,.05);
padding:10px 16px 12px;
}
.src-tab-hdr{
display:flex;align-items:center;gap:5px;
color:rgba(255,255,255,.28);font-size:.62rem;font-weight:700;
text-transform:uppercase;letter-spacing:1.1px;margin-bottom:8px;
}
.src-tab-scroll{
display:flex;gap:7px;overflow-x:auto;
scrollbar-width:none;-webkit-overflow-scrolling:touch;
padding-bottom:2px;
}
.src-tab-scroll::-webkit-scrollbar{display:none}
.src-tab{
display:inline-flex;align-items:center;gap:6px;
padding:7px 15px;border-radius:30px;flex-shrink:0;
border:1px solid rgba(255,255,255,.1);
background:rgba(255,255,255,.04);
color:rgba(255,255,255,.5);
font-size:.8rem;font-weight:600;cursor:pointer;
white-space:nowrap;font-family:inherit;
transition:background .15s,border-color .15s,color .15s,box-shadow .15s;
}
.src-tab i{font-size:.85rem;opacity:.75;flex-shrink:0}
.src-tab:hover{
background:rgba(0,245,255,.07);
border-color:rgba(0,245,255,.22);
color:rgba(255,255,255,.85);
}
.src-tab.on{
background:rgba(0,245,255,.12);
border-color:rgba(0,245,255,.38);
color:var(--accent2,#00f5ff);
box-shadow:0 0 18px rgba(0,245,255,.14),inset 0 1px 0 rgba(0,245,255,.18);
}
.src-tab.on i{opacity:1}
/* 4K (Anizium) kaynak — altın sarısı badge */
.src-tab-4k{
background:linear-gradient(135deg,rgba(255,214,10,.13),rgba(255,180,0,.07));
border-color:rgba(255,214,10,.45);
color:rgba(255,214,10,.9);
}
.src-tab-4k i{color:#ffd60a;opacity:1}
.src-tab-4k:hover{
background:linear-gradient(135deg,rgba(255,214,10,.22),rgba(255,180,0,.13));
border-color:rgba(255,214,10,.65);
color:#ffd60a;
}
.src-tab-4k.on{
background:linear-gradient(135deg,rgba(255,214,10,.28),rgba(255,180,0,.18));
border-color:#ffd60a;
color:#ffd60a;
box-shadow:0 0 18px rgba(255,214,10,.22),inset 0 1px 0 rgba(255,214,10,.25);
}
/* Dropdown menüdeki featured satır */
.vmi-featured{
background:linear-gradient(90deg,rgba(255,214,10,.08),transparent);
border-left:2px solid rgba(255,214,10,.6);
padding-left:10px;
color:rgba(255,214,10,.9);
}
.vmi-featured .dot{background:#ffd60a !important}
</style>
@endpush
@section('content')
<div class="pw">
{{-- ── Main ───────────────────────────────────────────────────── --}}
<div class="pw-main">
{{-- Ambiyans ışığı canvas .vw dışında, .pw-main içinde --}}
<canvas id="ambi-canvas" width="32" height="18" aria-hidden="true"></canvas>
<div class="vw" id="vw">
<div id="vd-clip"><video id="vd" playsinline></video></div>
{{-- IMA Ad Container --}}
@if(!empty($adsConfig['enabled']) && !empty($adsConfig['vast_url']))
<div id="ad-container" style="position:absolute;inset:0;z-index:50;display:none;background:#000"></div>
@endif
{{-- Intro overlay --}}
@if($introUrl)
<div id="intro-overlay">
<video id="intro-vd" src="{{ $introUrl }}" playsinline webkit-playsinline
style="width:100%;height:100%;object-fit:contain;display:block"></video>
<button id="intro-skip" onclick="skipIntro()">
Geç <span id="intro-timer"></span>
</button>
</div>
@endif
{{-- NicoNico yorum katmanı --}}
<div id="nico-layer" style="position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:18"></div>
{{-- İlk kez izleyenler badge --}}
<div id="firstwatch-badge" style="display:none;position:absolute;top:12px;left:12px;z-index:20;background:rgba(0,0,0,.65);border:1px solid rgba(0,245,255,.35);border-radius:20px;padding:5px 13px;font-size:.75rem;color:#fff;backdrop-filter:blur(8px);pointer-events:none">
<span style="color:var(--accent2)">🔴</span> <span id="firstwatch-count">0</span> kişi ilk kez izliyor
</div>
{{-- Watch Party butonu --}}
@auth
<button id="party-fab" onclick="openPartyModal()"
style="display:none;position:absolute;top:12px;right:12px;z-index:20;background:rgba(255,45,125,.15);border:1px solid rgba(255,45,125,.4);border-radius:20px;padding:5px 13px;font-size:.75rem;color:#fff;cursor:pointer;backdrop-filter:blur(8px);transition:all .2s"
onmouseover="this.style.background='rgba(255,45,125,.3)'" onmouseout="this.style.background='rgba(255,45,125,.15)'">
<i class="bi bi-people-fill" style="color:#ff2d7d"></i> Birlikte İzle
</button>
@endauth
{{-- Ruh Hali Motoru overlay --}}
<div id="mood-overlay">
<div id="mood-picker">
<div class="mood-title">🎬 Bölüm bitti! Sırada ne izleyelim?</div>
<div class="mood-sub">Ruh halini seç, sana özel öneri getirelim.</div>
<div class="mood-emojis">
<button class="mood-btn" onclick="fetchMood('sad')"><span class="me">😭</span><span class="ml">Ağlamak</span></button>
<button class="mood-btn" onclick="fetchMood('funny')"><span class="me">🤣</span><span class="ml">Gülmek</span></button>
<button class="mood-btn" onclick="fetchMood('hype')"><span class="me">🔥</span><span class="ml">Heyecan</span></button>
<button class="mood-btn" onclick="fetchMood('think')"><span class="me">🧠</span><span class="ml">Düşünmek</span></button>
<button class="mood-btn" onclick="fetchMood('romance')"><span class="me">😍</span><span class="ml">Romantizm</span></button>
<button class="mood-btn" onclick="fetchMood('scary')"><span class="me">👻</span><span class="ml">Korkmak</span></button>
</div>
<button class="mood-close" onclick="closeMoodOverlay()">Şimdi değil</button>
</div>
<div class="mood-results" id="mood-results">
<div class="mood-results-title" id="mood-results-title">Önerilen animeler</div>
<div class="mood-grid" id="mood-grid"></div>
<button class="mood-close" onclick="closeMoodOverlay()">Kapat</button>
</div>
</div>
{{-- Watermark cover --}}
@if($wmCoverSeconds > 0)
<div id="wm-cover"><span>ANIMEXE</span></div>
@endif
{{-- Buffer spinner (mid-play buffering) --}}
<div class="vs" id="vs"><div class="vsp"></div><div id="vs-msg"></div></div>
{{-- Loading screen (ilk yükleme + kaynak değişimi) --}}
<div id="vload" class="vload on">
<div class="vload-bg" id="vload-bg"></div>
<div class="vload-inner">
<div class="vload-rig">
<div class="vload-ring"></div>
<div class="vload-ring"></div>
<div class="vload-ring"></div>
<div class="vload-orb"></div>
<div class="vload-orb"></div>
<div class="vload-orb"></div>
<div class="vload-dot"></div>
</div>
<div class="vload-lbl">Yükleniyor<span class="vload-ldots"></span></div>
<div class="vload-tip" id="vload-tip"></div>
<button class="vload-playbtn" id="vload-playbtn" onclick="_vloadPlay()">
<i class="bi bi-play-fill"></i> Oynatmak için tıkla
</button>
<button class="vload-retrybtn" id="vload-retrybtn" onclick="window.location.reload()">
<i class="bi bi-arrow-clockwise"></i> Yüklenemiyor Yenile
</button>
</div>
</div>
{{-- İntro/Outro atla butonu --}}
<button id="aniskip-btn" onclick="skipAniSkip()">
<i class="bi bi-skip-end-fill"></i> <span id="aniskip-label">İntroyu Atla</span>
</button>
{{-- Most-skipped segment button --}}
<button id="skip-seg-btn" style="display:none!important"></button>
{{-- Muted chip (autoplay policy küçük, video'yu bloklamaz) --}}
<div id="vunmute" style="display:none;position:absolute;bottom:72px;left:50%;transform:translateX(-50%);z-index:36;pointer-events:auto">
<button onclick="doUnmute()" style="display:flex;align-items:center;gap:8px;padding:10px 20px;border-radius:50px;background:rgba(6,6,20,.92);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,45,125,.5);color:#fff;font-size:.84rem;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap;box-shadow:0 4px 24px rgba(255,45,125,.3);animation:unmute-pulse 1.8s ease-in-out infinite">
<i class="bi bi-volume-mute-fill" style="color:#ff2d7d;font-size:1.1rem"></i>
<span>Sessiz — sesi açmak için tıkla</span>
</button>
</div>
{{-- Subtitle overlay --}}
<div class="vsub" id="vsub"></div>
{{-- Seek flash --}}
<div class="vrf L" id="rf-l"><div class="vrf-bg"></div><div class="vrf-inner"><div class="vrf-chevs"><i class="bi bi-chevron-left"></i><i class="bi bi-chevron-left"></i><i class="bi bi-chevron-left"></i></div><span class="vrf-sec">10 saniye</span></div></div>
<div class="vrf R" id="rf-r"><div class="vrf-bg"></div><div class="vrf-inner"><div class="vrf-chevs"><i class="bi bi-chevron-right"></i><i class="bi bi-chevron-right"></i><i class="bi bi-chevron-right"></i></div><span class="vrf-sec">10 saniye</span></div></div>
<div id="hold-ind" style="display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.72);border:1px solid rgba(255,255,255,.18);border-radius:28px;padding:10px 22px;color:#fff;font-size:1rem;font-weight:800;pointer-events:none;z-index:30;backdrop-filter:blur(8px);white-space:nowrap">⚡ 2× Hız</div>
{{-- Controls --}}
<div class="vc" id="vc">
<div class="vm-backdrop" id="vm-backdrop" onclick="event.stopPropagation();closeMenus();" aria-hidden="true"></div>
<div class="vm-mobile-backdrop" id="vm-mobile-backdrop" onclick="closeMenus();" aria-hidden="true"></div>
{{-- Progress --}}
<div class="vp-wrap">
<div class="vp-bar" id="vpbar">
<div class="vp-tip" id="vptip"><canvas id="vptip-thumb" width="160" height="90"></canvas><span id="vptip-time">0:00</span></div>
<div class="vp-buf" id="vpbuf" style="width:0%"></div>
<div class="vp-fill" id="vpfill" style="width:0%">
<div class="vp-th"></div>
</div>
</div>
</div>
{{-- Control bar — süre ortada, sağda ayar / tam ekran aynı hizada --}}
<div class="vcb">
<div class="vcb-l">
@if($prev)
<a href="{{ route('watch',[$anime->slug,$prev->season?->season_number??$seasonModel->season_number,$prev->episode_number]) }}" class="cb" title="Önceki Bölüm (P)">
<i class="bi bi-skip-start-fill"></i>
</a>
@endif
<button class="cb skbtn" onclick="seek(-10)" title="-10s (J)">
<i class="bi bi-arrow-counterclockwise"></i><small>10s</small>
</button>
<button class="cb" id="cb-play" onclick="doPlay()" title="Oynat/Durdur (K)">
<i class="bi bi-play-fill" id="cb-play-i"></i>
</button>
<button class="cb skbtn" onclick="seek(10)" title="+10s (L)">
<i class="bi bi-arrow-clockwise"></i><small>10s</small>
</button>
@if($next)
<a href="{{ $nextUrl }}" class="cb" title="Sonraki Bölüm (N)">
<i class="bi bi-skip-end-fill"></i>
</a>
@endif
</div>
<div class="vcb-time"><span class="vtime" id="vtime">0:00 / 0:00</span></div>
<div class="vcb-r">
{{-- Kalite badge: sadece landscape'te görünür --}}
<div class="vdrop set-btn-wrap">
<button class="cb" id="cb-set" onclick="openMenu('mnu-set')" title="Kalite ve Ayarlar" style="gap:4px">
<i class="bi bi-gear-fill" style="font-size:.95rem"></i>
<span class="lbl" id="qlbl" style="font-size:.72rem;font-weight:700;min-width:32px;text-align:left">Auto</span>
</button>
<div class="vmenu" id="mnu-set" style="min-width:190px">
@if(count($videoSourcesData ?? []) > 1)
<div class="dub-in-menu">
<div class="vmt">Çeviri / Kaynak</div>
@foreach($videoSourcesData as $idx => $vs)
<div class="vmi vmi-src {{ $idx === 0 ? 'on' : '' }}{{ !empty($vs['featured']) ? ' vmi-featured' : '' }}"
data-url="{{ $vs['url'] }}" data-lbl="{{ $vs['label'] }}" data-key="{{ $vs['key'] }}" data-type="{{ $vs['type'] }}" onclick="doSource(this)">
<span class="dot"></span>
@if(!empty($vs['featured']))
<span> {{ $vs['label'] }}</span>
@else
<span>{{ $vs['label'] }}</span>
@endif
</div>
@endforeach
<div class="vm-sep"></div>
</div>
@endif
@if(count($dubSources) > 1)
<div class="dub-in-menu">
<div class="vmt">Dublaj / Ses</div>
@foreach($dubSources as $d)
<div class="vmi vmi-dub {{ $d['active'] ? 'on' : '' }}" data-url="{{ $d['url'] }}" data-lbl="{{ $d['label'] }}" data-key="{{ $d['key'] }}" onclick="doDub(this)">
<span class="dot"></span><span>{{ $d['label'] }}</span>
</div>
@endforeach
<div class="vm-sep"></div>
</div>
@endif
@if(count($subtitlesData) > 0)
<div class="vmt">Altyazı</div>
<div class="vmi {{ !collect($subtitlesData)->contains('is_default',true)?'on':'' }}" data-i="-1" onclick="doSub(-1,this)">
<span class="dot"></span><span>Kapalı</span>
</div>
@foreach($subtitlesData as $i => $s)
<div class="vmi {{ $s['is_default']?'on':'' }}" data-i="{{ $i }}" onclick="doSub({{ $i }},this)">
<span class="dot"></span><span>{{ $s['label'] }}</span>
</div>
@endforeach
<div class="vmt" style="margin-top:2px">Altyazı Görünümü</div>
<div style="padding:2px 0 6px">
<div class="sub-row">
<span class="sub-row-lbl">Boyut</span>
<div style="display:flex;gap:4px">
<button class="sub-sz-btn" data-sz="small" onclick="setSubSize('small',this)">A</button>
<button class="sub-sz-btn" data-sz="medium" onclick="setSubSize('medium',this)">A</button>
<button class="sub-sz-btn" data-sz="large" onclick="setSubSize('large',this)">A</button>
<button class="sub-sz-btn" data-sz="xlarge" onclick="setSubSize('xlarge',this)">A</button>
</div>
</div>
<div class="sub-row">
<span class="sub-row-lbl">Renk</span>
<div style="display:flex;gap:5px;align-items:center">
<button class="sub-clr-btn" data-clr="#ffffff" onclick="setSubColor('#ffffff',this)" title="Beyaz" style="background:#fff"></button>
<button class="sub-clr-btn" data-clr="#ffff00" onclick="setSubColor('#ffff00',this)" title="Sarı" style="background:#ffff00"></button>
<button class="sub-clr-btn" data-clr="#00ffcc" onclick="setSubColor('#00ffcc',this)" title="Nane" style="background:#00ffcc"></button>
<button class="sub-clr-btn" data-clr="#ff9acd" onclick="setSubColor('#ff9acd',this)" title="Pembe" style="background:#ff9acd"></button>
<button class="sub-clr-btn" data-clr="#a0d4ff" onclick="setSubColor('#a0d4ff',this)" title="Mavi" style="background:#a0d4ff"></button>
</div>
</div>
<div class="sub-row">
<span class="sub-row-lbl">Arka plan</span>
<div style="display:flex;gap:4px">
<button class="sub-bg-btn" data-bg="none" onclick="setSubBg('none',this)">Yok</button>
<button class="sub-bg-btn" data-bg="semi" onclick="setSubBg('semi',this)">Yarı</button>
<button class="sub-bg-btn" data-bg="full" onclick="setSubBg('full',this)">Tam</button>
</div>
</div>
</div>
<div style="height:1px;background:#222;margin:6px 0"></div>
@endif
<div class="vmt">Video Kalitesi</div>
<div class="vmi on" id="q-auto" data-q="-1" onclick="doQuality(-1,this)">
<span class="dot"></span><span>Otomatik</span>
</div>
{{-- HLS levels injected by JS --}}
<div id="q-levels"></div>
<div style="height:1px;background:#222;margin:6px 0"></div>
<div class="vmt">Oynatma Hızı</div>
@foreach([0.5,0.75,1,1.25,1.5,2] as $spd)
<div class="vmi {{ $spd==1?'on':'' }}" data-spd="{{ $spd }}" onclick="doSpeed({{ $spd }},this)">
<span class="dot"></span><span>{{ $spd }}x</span>
</div>
@endforeach
<div style="height:1px;background:#222;margin:6px 0"></div>
<div class="vmt">Video Zoom</div>
<div class="vmi" id="fill-toggle" onclick="toggleFill()">
<span class="dot"></span><span>Ekranı Doldur</span>
</div>
<div class="zoom-row" id="zoom-row">
<input type="range" class="zoom-slider" id="zoom-slider"
min="100" max="200" step="5" value="100"
oninput="applyZoom(this.value)">
<div class="zoom-meta">
<span>Normal</span>
<span id="zoom-lbl" style="color:var(--accent2);font-weight:700">100%</span>
<span>2×</span>
</div>
</div>
<div style="height:1px;background:#222;margin:6px 0"></div>
<div class="vmt">Görsel</div>
<div class="vmi" id="ambi-toggle" onclick="toggleAmbi()"><span class="dot"></span><span>Ambiyans Işığı</span></div>
</div>
</div>
{{-- Altyazı: her zaman görünür, ayrı CC butonu --}}
@if(count($subtitlesData) > 0)
<div class="vdrop sub-bar">
<button class="cb" id="cb-sub" onclick="openMenu('mnu-sub')" title="Altyazı (C)" style="flex-shrink:0">
<i class="bi bi-badge-cc-fill"></i>
</button>
<div class="vmenu" id="mnu-sub" style="min-width:170px">
<div class="vmt">Altyazı</div>
<div class="vmi {{ !collect($subtitlesData)->contains('is_default',true)?'on':'' }}" data-i="-1" onclick="doSub(-1,this);closeMenus()">
<span class="dot"></span><span>Kapalı</span>
</div>
@foreach($subtitlesData as $i => $s)
<div class="vmi {{ $s['is_default']?'on':'' }}" data-i="{{ $i }}" onclick="doSub({{ $i }},this);closeMenus()">
<span class="dot"></span><span>{{ $s['label'] }}</span>
</div>
@endforeach
</div>
</div>
@endif
{{-- Dub: sadece geniş ekranda ayrı buton --}}
@if(count($dubSources) > 1)
<div class="vdrop dub-bar">
<button class="cb" id="cb-dub" onclick="openMenu('mnu-dub')" title="Dublaj">
<i class="bi bi-translate"></i>
<span class="lbl" id="dub-lbl">{{ collect($dubSources)->firstWhere('active',true)['label'] ?? ($dubSources[0]['label']??'Dil') }}</span>
</button>
<div class="vmenu" id="mnu-dub">
<div class="vmt">Dublaj / Dil</div>
@foreach($dubSources as $d)
<div class="vmi vmi-dub {{ $d['active'] ? 'on' : '' }}" data-url="{{ $d['url'] }}" data-lbl="{{ $d['label'] }}" data-key="{{ $d['key'] }}" onclick="doDub(this)">
<span class="dot"></span><span>{{ $d['label'] }}</span>
</div>
@endforeach
</div>
</div>
@endif
<div class="vcv">
<input type="range" class="vvr" id="cb-vvr" min="0" max="1" step="0.02" value="1" style="--v:100%" oninput="doVol(this.value)">
<button class="cb" id="cb-vol" onclick="doMute()" title="Sessiz (M)" style="flex-shrink:0">
<i class="bi bi-volume-up-fill" id="cb-vol-i"></i>
</button>
</div>
<button class="cb cb-kbhelp" onclick="toggleKbHelp()" title="Klavye Kısayolları (?)" style="flex-shrink:0;font-size:.72rem;font-weight:800;padding:0 8px;opacity:.6">?</button>
<button class="cb" id="cb-fs" onclick="doFS()" title="Tam Ekran (F)" style="flex-shrink:0">
<i class="bi bi-fullscreen" id="cb-fs-i"></i>
</button>
</div>
</div>
</div>
{{-- Next ep --}}
@if($next)
<div class="vnx" id="vnx">
<div class="vnx-hdr">
<div class="vnx-lbl">Sıradaki Bölüm</div>
<button class="vnx-cancel" onclick="cancelNext()" title="İptal"><i class="bi bi-x"></i></button>
</div>
<div class="vnx-ttl">{{ $next->episode_number }}. {{ $next->title ?: 'Bölüm' }}</div>
<div class="vnx-bar"><div class="vnx-fill" id="vnx-fill" style="width:0%"></div></div>
<a href="{{ $nextUrl }}" class="vnx-btn"><i class="bi bi-play-fill"></i> Sonraki Bölüme Geç</a>
</div>
@endif
</div>
{{-- NicoNico yorum giriş satırı --}}
@auth
<div id="nico-input-row" class="nico-off ctrls-hidden">
<button id="nico-toggle-btn" onclick="toggleNico()" title="Canlı yorumları aç/kapat"><i class="bi bi-chat-right-text-fill"></i> Canlı</button>
<input type="color" id="nico-color-pick" value="#ffffff" title="Renk seç">
<input type="text" id="nico-input" placeholder="Yorum yaz, Enter ile gönder..." maxlength="100" onkeydown="if(event.key==='Enter')sendNicoComment()">
<button class="nico-filter-chip" id="nico-filter-btn" onclick="toggleNicoFilter()" title="Sadece takip ettiklerimin yorumları"><i class="bi bi-people-fill"></i> Takip</button>
<button id="nico-send-btn" onclick="sendNicoComment()"><i class="bi bi-send-fill"></i></button>
</div>
@endauth
{{-- Kaynak Seçici Tab Bar --}}
@if(count($videoSourcesData ?? []) > 1)
<div class="src-tab-bar">
<div class="src-tab-hdr"><i class="bi bi-collection-play-fill"></i> Çeviri / Kaynak</div>
<div class="src-tab-scroll">
@foreach($videoSourcesData as $idx => $vs)
<button class="src-tab {{ $idx === 0 ? 'on' : '' }}{{ !empty($vs['featured']) ? ' src-tab-4k' : '' }}"
data-url="{{ $vs['url'] }}"
data-lbl="{{ $vs['label'] }}"
data-key="{{ $vs['key'] }}"
data-type="{{ $vs['type'] }}"
onclick="doSource(this)">
@if(!empty($vs['featured']))
<i class="bi bi-star-fill" style="color:#ffd60a"></i>
@else
<i class="bi bi-play-circle-fill"></i>
@endif
<span>{{ $vs['label'] }}</span>
</button>
@endforeach
</div>
</div>
<div id="src-hint" style="display:none;align-items:flex-start;gap:10px;margin:0;padding:10px 14px;background:rgba(0,245,255,.05);border-top:1px solid rgba(0,245,255,.12);border-bottom:1px solid rgba(0,245,255,.06)">
<i class="bi bi-info-circle-fill" style="color:rgba(0,245,255,.6);font-size:.9rem;flex-shrink:0;margin-top:1px"></i>
<div style="flex:1;min-width:0">
<div style="font-size:.78rem;font-weight:700;color:rgba(255,255,255,.8);margin-bottom:3px">Kaynak & Çeviri Hakkında</div>
<div style="font-size:.74rem;color:rgba(255,255,255,.45);line-height:1.55">
Yukarıdaki sekmeler farklı çeviri gruplarının yayınlarıdır. Bir kaynak çalışmıyorsa veya altyazı bozuksa başka bir sekmeyi deneyin genellikle en az bir tanesi sorunsuz çalışır.
</div>
</div>
<button onclick="document.getElementById('src-hint').style.display='none';localStorage.setItem('axe_src_hint','1')" style="background:none;border:none;color:rgba(255,255,255,.25);font-size:1rem;cursor:pointer;padding:2px 4px;flex-shrink:0;line-height:1;transition:color .15s" onmouseover="this.style.color='rgba(255,255,255,.7)'" onmouseout="this.style.color='rgba(255,255,255,.25)'" title="Kapat">
<i class="bi bi-x-lg"></i>
</button>
</div>
<script>if(!localStorage.getItem('axe_src_hint')){document.getElementById('src-hint').style.display='flex'}</script>
@endif
{{-- Info bar --}}
<div class="pinfo">
<div class="pinfo-row">
<div style="flex:1;min-width:0">
<div class="pinfo-ep-meta">
<a href="{{ $anime->detailUrl }}" style="color:inherit">{{ $anime->title }}</a>
&nbsp;·&nbsp;{{ $seasonModel->season_number }}. Sezon
</div>
<h1 class="pinfo-ep-title">
{{ $ep->episode_number }}. Bölüm{{ $ep->title ? ' — '.$ep->title : '' }}
</h1>
<div class="pinfo-stats">
<span><i class="bi bi-eye"></i>{{ number_format($ep->view_count) }}</span>
@if($ep->duration)<span><i class="bi bi-clock"></i>{{ $ep->duration_formatted }}</span>@endif
@if($activeDub)<span style="color:rgba(0,245,255,.55)"><i class="bi bi-translate"></i>{{ ['trdub'=>'TR Dublaj','original'=>'Japonca','endub'=>'EN Dublaj'][$activeDub]??$activeDub }}</span>@endif
@if($anime->rating)<span style="color:#ffd60a"><i class="bi bi-star-fill"></i>{{ number_format($anime->rating,1) }}</span>@endif
</div>
</div>
<div class="pnav">
@auth
<button class="pnav-btn" onclick="openShareModal()" title="Arkadaşınla paylaş" style="color:var(--accent2)">
<i class="bi bi-send-fill"></i><span class="d-none d-sm-inline"> Paylaş</span>
</button>
@endauth
@if($prev)
<a href="{{ route('watch',[$anime->slug,$prev->season?->season_number??$seasonModel->season_number,$prev->episode_number]) }}" class="pnav-btn">
<i class="bi bi-chevron-left"></i><span class="d-none d-sm-inline"> Önceki</span>
</a>
@endif
@if($next)
<a href="{{ $nextUrl }}" class="pnav-btn pnav-next">
<span class="d-none d-sm-inline">Sonraki </span><i class="bi bi-chevron-right"></i>
</a>
@endif
</div>
</div>
</div>
{{-- ── Tabs + Content ─────────────────────────────────────────── --}}
<div class="ep-below">
{{-- Tab nav --}}
<div class="eb-tabs">
<button class="eb-tab on" onclick="switchTab('info',this)">
<i class="bi bi-info-circle"></i> Bölüm Bilgisi
</button>
<button class="eb-tab" onclick="switchTab('cmt',this)">
<i class="bi bi-chat-dots"></i> Yorumlar
<span class="tab-badge" id="tab-cmt-badge" style="display:none"></span>
</button>
@auth
<button class="eb-tab" onclick="switchTab('ai',this)" style="color:var(--accent3)">
<i class="bi bi-stars"></i> AI Analiz
</button>
<button class="eb-tab" onclick="switchTab('notes',this)" style="color:#ffd700">
<i class="bi bi-journal-text"></i> Notlarım
<span class="tab-badge" id="tab-notes-badge" style="display:none;background:rgba(255,215,0,.15);color:#ffd700"></span>
</button>
@else
<button class="eb-tab" onclick="showPlayerLoginPrompt()" style="color:var(--accent3);opacity:.6">
<i class="bi bi-stars"></i> AI Analiz <i class="bi bi-lock-fill" style="font-size:.65rem"></i>
</button>
@endauth
<button class="eb-tab" onclick="switchTab('predict',this)" style="color:#ff9f0a">
<i class="bi bi-lightning-charge"></i> Tahminler
<span class="tab-badge" id="tab-predict-badge" style="display:none;background:rgba(255,159,10,.15);color:#ff9f0a"></span>
</button>
</div>
{{-- ── Info Panel ──────────────────────────────────────────── --}}
<div class="eb-panel on" id="panel-info">
<div class="eb-info">
<div class="ep-card">
{{-- Anime posteri --}}
@if($anime->coverUrl)
<div class="ep-card-poster">
<img src="{{ $anime->coverUrl }}" alt="{{ $anime->title }}" loading="lazy">
</div>
@endif
<div class="ep-card-body">
<div class="ep-card-anime-title">
<a href="{{ $anime->detailUrl }}">{{ $anime->title }}</a>
&nbsp;·&nbsp; {{ $seasonModel->season_number }}. Sezon
</div>
<div class="ep-card-ep-title">
{{ $ep->episode_number }}. Bölüm{{ $ep->title ? ' — '.$ep->title : '' }}
</div>
<div class="ep-card-stats">
<div class="ep-stat stat-views"><i class="bi bi-eye"></i> {{ number_format($ep->view_count) }} izlenme</div>
@if($ep->duration)<div class="ep-stat stat-dur"><i class="bi bi-clock"></i> {{ $ep->duration_formatted }}</div>@endif
@if($activeDub)<div class="ep-stat stat-dub"><i class="bi bi-translate"></i> {{ ['trdub'=>'Türkçe Dublaj','original'=>'Japonca (Orijinal)','endub'=>'İngilizce Dublaj'][$activeDub] ?? $activeDub }}</div>@endif
@if($anime->rating)<div class="ep-stat stat-rating"><i class="bi bi-star-fill"></i> {{ number_format($anime->rating,1) }}</div>@endif
</div>
{{-- Like / Dislike --}}
@php
$epLikes = \App\Models\EpisodeVote::where('episode_id',$ep->id)->where('vote',1)->count();
$epDislikes = \App\Models\EpisodeVote::where('episode_id',$ep->id)->where('vote',-1)->count();
$myVote = auth()->check() ? \App\Models\EpisodeVote::where('user_id',auth()->id())->where('episode_id',$ep->id)->value('vote') : null;
@endphp
<div class="ep-vote-row" id="ep-vote-row" style="display:flex;gap:10px;margin-top:14px;align-items:center">
@auth
<button id="btn-like" onclick="voteEpisode(1)"
class="ep-vote-btn {{ $myVote==1?'voted-like':'' }}"
style="display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:20px;border:1px solid {{ $myVote==1?'#3fb950':'#30363d' }};background:{{ $myVote==1?'rgba(63,185,80,.15)':'transparent' }};color:{{ $myVote==1?'#3fb950':'var(--text2)' }};cursor:pointer;font-size:.82rem;transition:all .2s">
<i class="bi bi-hand-thumbs-up{{ $myVote==1?'-fill':'' }}" id="like-icon"></i>
<span id="like-cnt">{{ $epLikes }}</span>
</button>
<button id="btn-dislike" onclick="voteEpisode(-1)"
class="ep-vote-btn {{ $myVote==-1?'voted-dislike':'' }}"
style="display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:20px;border:1px solid {{ $myVote==-1?'#f85149':'#30363d' }};background:{{ $myVote==-1?'rgba(248,81,73,.15)':'transparent' }};color:{{ $myVote==-1?'#f85149':'var(--text2)' }};cursor:pointer;font-size:.82rem;transition:all .2s">
<i class="bi bi-hand-thumbs-down{{ $myVote==-1?'-fill':'' }}" id="dislike-icon"></i>
<span id="dislike-cnt">{{ $epDislikes }}</span>
</button>
@else
<a href="{{ route('frontend.login') }}" style="display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:20px;border:1px solid #30363d;color:var(--text3);font-size:.82rem;text-decoration:none">
<i class="bi bi-hand-thumbs-up"></i> {{ $epLikes }}
</a>
<a href="{{ route('frontend.login') }}" style="display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:20px;border:1px solid #30363d;color:var(--text3);font-size:.82rem;text-decoration:none">
<i class="bi bi-hand-thumbs-down"></i> {{ $epDislikes }}
</a>
@endauth
</div>
@if($ep->description)
<div class="ep-desc-wrap">
<div class="ep-desc-lbl">Bölüm Hakkında</div>
<p class="ep-desc-text" id="ep-desc">{{ $ep->description }}</p>
@if(mb_strlen($ep->description) > 220)
<button class="ep-expand-btn" id="ep-expand-btn" onclick="toggleDesc()">Devamını Oku <i class="bi bi-chevron-down"></i></button>
@endif
</div>
@elseif($anime->description)
<div class="ep-desc-wrap">
<div class="ep-desc-lbl">Anime Hakkında</div>
<p class="ep-desc-text" id="ep-desc">{{ $anime->description }}</p>
@if(mb_strlen($anime->description) > 220)
<button class="ep-expand-btn" id="ep-expand-btn" onclick="toggleDesc()">Devamını Oku <i class="bi bi-chevron-down"></i></button>
@endif
</div>
@endif
{{-- Türler --}}
@if($anime->genres && $anime->genres->count())
<div class="ep-tags">
@foreach($anime->genres->take(6) as $g)
<a href="{{ route('genre', $g->slug) }}" class="ep-tag">{{ $g->name }}</a>
@endforeach
</div>
@endif
</div>
</div>
{{-- Anime meta --}}
<div class="ep-meta-row">
@if($anime->studio)
<div class="ep-meta-item">
<span class="ep-meta-key">Stüdyo</span>
<span class="ep-meta-val">{{ $anime->studio }}</span>
</div>
@endif
@if($anime->release_year)
<div class="ep-meta-item">
<span class="ep-meta-key">Yıl</span>
<span class="ep-meta-val">{{ $anime->release_year }}</span>
</div>
@endif
@if($anime->status)
<div class="ep-meta-item">
<span class="ep-meta-key">Durum</span>
<span class="ep-meta-val">{{ ['ongoing'=>'Devam Ediyor','completed'=>'Tamamlandı','upcoming'=>'Yakında'][$anime->status] ?? $anime->status }}</span>
</div>
@endif
<div class="ep-meta-item">
<span class="ep-meta-key">Sezon</span>
<span class="ep-meta-val">{{ $anime->seasons->count() }} Sezon · {{ $anime->episode_count }} Bölüm</span>
</div>
</div>
</div>
</div>
{{-- ── Comments Panel ──────────────────────────────────────── --}}
<div class="eb-panel" id="panel-cmt">
<div class="eb-cmt">
<div class="cmt-section" id="cmt-section">
<div class="cmt-hdr">
<span class="cmt-title"><i class="bi bi-chat-dots"></i> Yorumlar</span>
<span class="cmt-count" id="cmt-count-lbl"></span>
</div>
@auth
<div class="cmt-form-wrap" id="cmt-form-wrap">
<div class="cmt-form-avatar">@if(auth()->user()->avatar)<img src="{{ \App\Support\MediaUrl::fromStoragePath(auth()->user()->avatar) }}" style="width:100%;height:100%;object-fit:cover">@else{{ mb_substr(auth()->user()->name,0,1) }}@endif</div>
<div class="cmt-form-body">
<div id="cmt-gif-preview" style="display:none;margin-bottom:8px">
<div style="position:relative;display:inline-block">
<img id="cmt-gif-img" src="" alt="GIF" style="max-height:140px;border-radius:8px;display:block">
<button onclick="clearGif()" style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,.7);border:none;color:#fff;border-radius:50%;width:22px;height:22px;cursor:pointer;font-size:.75rem;display:flex;align-items:center;justify-content:center"></button>
</div>
</div>
<textarea id="cmt-input" class="cmt-input" placeholder="Yorumunu yaz..." rows="2" oninput="autoResize(this);updateCharCount()"></textarea>
<div style="font-size:.68rem;color:rgba(255,255,255,.25);text-align:right;margin-top:2px" id="cmt-char-count"></div>
<div class="cmt-form-actions">
@if(auth()->check() && auth()->user()->hasPerk('comment_gif'))
<button class="cmt-gif-btn" onclick="toggleGifPicker()" title="GIF ekle">
<i class="bi bi-image"></i> GIF
</button>
@else
<button class="cmt-gif-btn" style="opacity:.4;cursor:default" title="GIF eklemek için premium gerekiyor" onclick="event.preventDefault()">
<i class="bi bi-image"></i> GIF <i class="bi bi-lock-fill" style="font-size:.65rem"></i>
</button>
@endif
<button class="cmt-submit-btn" id="cmt-submit-btn" onclick="submitComment()">
<i class="bi bi-send-fill"></i> Gönder
</button>
</div>
</div>
</div>
<div id="gif-picker" class="gif-picker" style="display:none">
<div class="gif-search-wrap">
<i class="bi bi-search" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--text3);font-size:.85rem"></i>
<input type="text" id="gif-search-input" class="gif-search-input" placeholder="GIF ara..." oninput="searchGif(this.value)">
</div>
<div id="gif-results" class="gif-results">
<div class="gif-loading">Arama yapın...</div>
</div>
</div>
@else
<div class="cmt-login-prompt">
<i class="bi bi-chat-square-heart" style="font-size:2rem;opacity:.3;display:block;margin-bottom:10px"></i>
Yorum yapmak için <a href="{{ route('frontend.login') }}" style="color:var(--accent2)">giriş yapın</a>.
</div>
@endauth
<div id="cmt-list"></div>
<div id="cmt-load-more" style="display:none;text-align:center;padding:16px">
<button class="cmt-load-btn" onclick="loadComments()">Daha fazla yorum yükle</button>
</div>
<div id="cmt-empty" style="display:none" class="cmt-empty">
<i class="bi bi-chat-dots" style="font-size:2rem;opacity:.2;display:block;margin-bottom:8px"></i>
İlk yorumu sen yap!
</div>
</div>
</div>
</div>
{{-- ── AI Analiz Paneli ──────────────────────────────────────── --}}
@auth
<div class="eb-panel" id="panel-ai">
<div style="padding:24px 20px">
{{-- Bölüm AI analizi --}}
<div style="margin-bottom:24px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--accent3),var(--accent));display:flex;align-items:center;justify-content:center;font-size:.95rem;color:#fff;box-shadow:0 0 16px rgba(184,77,255,.4);flex-shrink:0"><i class="bi bi-cpu"></i></div>
<div>
<div style="font-weight:700;color:var(--text);font-size:.9rem">Bölüm AI Analizi</div>
<div style="font-size:.75rem;color:var(--text3);margin-top:1px">Spoilersız bölüm analizi ve öneriler</div>
</div>
</div>
<button id="ai-ep-btn" onclick="loadEpAi()" style="padding:10px 20px;border-radius:10px;border:1px solid rgba(184,77,255,.35);background:rgba(184,77,255,.1);color:var(--accent3);font-size:.85rem;font-weight:700;cursor:pointer;transition:all .16s;font-family:inherit;display:flex;align-items:center;gap:8px">
<i class="bi bi-stars"></i> Bu bölümü AI ile analiz et
</button>
<div id="ai-ep-loader" style="display:none;padding:16px 0;display:none;align-items:center;gap:8px">
<div style="display:flex;gap:4px">
<div style="width:7px;height:7px;border-radius:50%;background:var(--accent3);animation:aip-b .9s ease-in-out infinite"></div>
<div style="width:7px;height:7px;border-radius:50%;background:var(--accent);animation:aip-b .9s .18s ease-in-out infinite"></div>
<div style="width:7px;height:7px;border-radius:50%;background:var(--accent2);animation:aip-b .9s .36s ease-in-out infinite"></div>
</div>
<span style="color:var(--text3);font-size:.82rem">AI analiz yapıyor...</span>
</div>
<div id="ai-ep-result" style="display:none;margin-top:16px;padding:16px;background:linear-gradient(135deg,rgba(184,77,255,.1),rgba(0,245,255,.06));border:1px solid rgba(184,77,255,.2);border-radius:12px;color:var(--text);font-size:.88rem;line-height:1.65"></div>
</div>
{{-- Sohbet bölümü (mini) --}}
<div style="border-top:1px solid rgba(255,255,255,.06);padding-top:20px">
<div style="font-size:.8rem;font-weight:700;color:var(--text2);margin-bottom:10px;text-transform:uppercase;letter-spacing:.08em"><i class="bi bi-chat-dots" style="margin-right:5px;color:var(--accent3)"></i>Bu anime hakkında AI'ya sor</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px">
@foreach(['Bu animeyi tamamlamalı mıyım?','Benzer anime önerir misin?','Bu bölümde neler oldu?','Anime ne zaman bitti?'] as $q)
<button onclick="aiPlayerAsk('{{ $q }}')" style="padding:5px 11px;border-radius:999px;font-size:.75rem;font-weight:600;cursor:pointer;background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.2);color:var(--accent2);transition:all .14s;font-family:inherit">{{ $q }}</button>
@endforeach
</div>
<div style="display:flex;gap:8px">
<input id="ai-player-inp" type="text" placeholder="Soru sor..." style="flex:1;background:rgba(255,255,255,.05);border:1px solid rgba(184,77,255,.25);border-radius:9px;padding:10px 12px;color:var(--text);font-size:.84rem;font-family:inherit;outline:none" onkeydown="if(event.key==='Enter') sendPlayerAiMsg()">
<button onclick="sendPlayerAiMsg()" style="padding:10px 16px;border-radius:9px;border:none;background:linear-gradient(135deg,var(--accent3),var(--accent));color:#fff;cursor:pointer;font-size:.85rem;transition:transform .14s" id="ai-player-send"><i class="bi bi-send-fill"></i></button>
</div>
<div id="ai-player-msgs" style="margin-top:12px;display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto"></div>
</div>
</div>
</div>
{{-- ── Not Paneli ─────────────────────────────────────────────── --}}
<div class="eb-panel" id="panel-notes" style="padding:16px">
<div style="margin-bottom:14px">
<div style="font-size:.8rem;font-weight:700;color:#ffd700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px"><i class="bi bi-journal-text" style="margin-right:5px"></i>Bölüm Notları</div>
<div style="display:flex;gap:8px;align-items:flex-start">
<textarea id="note-input" placeholder="Bu bölüm için not al..." style="flex:1;background:rgba(255,255,255,.05);border:1px solid rgba(255,215,0,.2);border-radius:9px;padding:10px 12px;color:var(--text);font-size:.84rem;font-family:inherit;outline:none;resize:none;height:72px" maxlength="500"></textarea>
</div>
<div style="display:flex;gap:6px;margin-top:8px;justify-content:flex-end">
<button onclick="captureTimestamp()" style="padding:6px 12px;border-radius:8px;border:1px solid rgba(255,215,0,.3);background:rgba(255,215,0,.08);color:#ffd700;font-size:.78rem;font-family:inherit;cursor:pointer">
<i class="bi bi-clock"></i> <span id="note-ts-label">Şu anki zaman</span>
</button>
<button onclick="saveNote()" style="padding:6px 14px;border-radius:8px;border:none;background:rgba(255,215,0,.85);color:#000;font-size:.82rem;font-weight:700;font-family:inherit;cursor:pointer">
<i class="bi bi-plus-lg"></i> Kaydet
</button>
</div>
</div>
<div id="notes-list" style="display:flex;flex-direction:column;gap:8px;max-height:320px;overflow-y:auto"></div>
<div id="notes-empty" style="text-align:center;padding:28px;color:var(--text3);font-size:.82rem">
<i class="bi bi-journal" style="font-size:1.8rem;opacity:.3;display:block;margin-bottom:8px"></i>
Henüz not eklemedin. Sahne kaydet, sonra bul!
</div>
</div>
@endauth
{{-- ── Tahmin Paneli (herkese görünür, giriş gerektirir yazma) ── --}}
<div class="eb-panel" id="panel-predict" style="padding:16px">
<div style="font-size:.8rem;font-weight:700;color:#ff9f0a;text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px">
<i class="bi bi-lightning-charge" style="margin-right:5px"></i>Bir Sonraki Bölüm Tahminleri
</div>
@auth
<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:14px" id="predict-form-wrap">
<textarea id="predict-input" placeholder="Ne olacağını tahmin et... (maks 280 karakter)"
style="flex:1;background:rgba(255,159,10,.05);border:1px solid rgba(255,159,10,.2);border-radius:9px;padding:10px 12px;color:var(--text);font-size:.84rem;font-family:inherit;outline:none;resize:none;height:68px" maxlength="280"></textarea>
<button onclick="submitPrediction()" style="padding:9px 14px;border-radius:9px;border:none;background:#ff9f0a;color:#000;font-size:.85rem;font-weight:800;font-family:inherit;cursor:pointer;flex-shrink:0;align-self:flex-end">
<i class="bi bi-send-fill"></i>
</button>
</div>
@else
<div style="text-align:center;padding:12px;margin-bottom:12px;background:rgba(255,159,10,.05);border:1px solid rgba(255,159,10,.15);border-radius:10px;font-size:.82rem;color:rgba(255,255,255,.4)">
<a href="{{ route('frontend.login') }}" style="color:#ff9f0a">Giriş yap</a> veya <a href="{{ route('frontend.register') }}" style="color:#ff9f0a">kayıt ol</a> — tahmin yaz
</div>
@endauth
<div id="predictions-list" style="display:flex;flex-direction:column;gap:8px;max-height:380px;overflow-y:auto"></div>
<div id="predictions-empty" style="text-align:center;padding:28px;color:var(--text3);font-size:.82rem;display:none">
<i class="bi bi-lightning" style="font-size:1.8rem;opacity:.3;display:block;margin-bottom:8px"></i>
Henüz tahmin yok. İlk tahmini sen yaz!
</div>
</div>
</div>
</div>
{{-- ── Sidebar ─────────────────────────────────────────────────── --}}
<div class="pw-side">
{{-- Sekmeler --}}
<div class="ps-tabs">
<button class="ps-tab on" id="tab-eps" onclick="switchSideTab('eps')"><i class="bi bi-collection-play"></i> Bölümler</button>
<button class="ps-tab" id="tab-spoiler" onclick="switchSideTab('spoiler')"><i class="bi bi-lock-fill"></i> Spoiler Kutusu</button>
</div>
{{-- Bölümler sekmesi --}}
<div class="ps-tab-content on" id="tc-eps" style="flex-direction:column">
<div class="ps-hdr" style="flex-shrink:0">
@if($anime->seasons->count() > 1)
<div class="ps-stabs">
@foreach($anime->seasons as $s)
<button class="pst {{ $s->id===$seasonModel->id?'on':'' }}" data-s="{{ $s->season_number }}" onclick="swS({{ $s->season_number }},this)">
{{ $s->season_number }}. Sezon
</button>
@endforeach
</div>
@endif
</div>
<div style="flex:1;overflow-y:auto">
@foreach($anime->seasons as $s)
<div class="ps-list" id="psl{{ $s->season_number }}" style="{{ $s->id!==$seasonModel->id?'display:none':'' }}">
@foreach($s->episodes as $pe)
@php
$isnow = $pe->id === $ep->id;
$pct = $watchProgress[$pe->id] ?? 0;
$watched = !$isnow && $pct >= 90;
@endphp
<a href="{{ route('watch',[$anime->slug,$s->season_number,$pe->episode_number]) }}"
class="ps-item {{ $isnow?'now':'' }} {{ $watched?'ps-watched':'' }}" id="{{ $isnow?'now-ep':'' }}">
<div class="ps-thumb">
@if($pe->thumbnailUrl)<img src="{{ $pe->thumbnailUrl }}" alt="" loading="lazy">
@elseif($anime->coverUrl)<img src="{{ $anime->coverUrl }}" alt="" loading="lazy">
@else<div class="nth"><i class="bi bi-film"></i></div>@endif
@if($isnow)<div class="nowmark"><i class="bi bi-play-circle-fill"></i></div>@endif
@if($watched && !$isnow)<div class="nowmark" style="background:rgba(0,245,255,.6)"><i class="bi bi-check-circle-fill" style="font-size:.9rem"></i></div>@endif
@if($pct > 0 && $pct < 90 && !$isnow)
<div class="ps-prog" style="--p:{{ $pct }}%"></div>
@endif
</div>
<div class="ps-info">
<div class="ps-num">{{ $pe->episode_number }}. Bölüm @if($watched && !$isnow)<span style="color:var(--accent2);font-size:.62rem;margin-left:4px"><i class="bi bi-check2"></i></span>@endif</div>
<div class="ps-name">{{ $pe->title ?: ($pe->episode_number.'. Bölüm') }}</div>
@if($pct > 0 && $pct < 90 && !$isnow)<div class="ps-pct">%{{ $pct }}</div>@endif
</div>
</a>
@endforeach
</div>
@endforeach
</div>
</div>
{{-- Spoiler Kutusu sekmesi --}}
<div class="ps-tab-content" id="tc-spoiler">
<div id="sb-panel" class="ps-tab-content on" style="flex:1;overflow-y:auto;padding:12px;gap:8px;flex-direction:column">
<div id="sb-loading" style="text-align:center;padding:24px;color:rgba(255,255,255,.3);font-size:.8rem">
<i class="bi bi-hourglass"></i> Yükleniyor...
</div>
</div>
@auth
<div class="sb-form">
<textarea class="sb-inp" id="sb-inp" rows="2" maxlength="600" placeholder="Spoiler veya gizli düşüncen (AI kontrol eder)..."></textarea>
<button class="sb-send" onclick="submitSpoilerBox()"><i class="bi bi-lock"></i> Gönder</button>
</div>
@else
<div style="padding:12px;text-align:center;font-size:.75rem;color:rgba(255,255,255,.3)">
<a href="{{ route('frontend.login') }}" style="color:#ff2d7d">Giriş yap</a> — yorum ekle
</div>
@endauth
</div>
</div>
</div>
{{-- ── Share Modal ─────────────────────────────────── --}}
@auth
<div id="share-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:8000;backdrop-filter:blur(8px);align-items:center;justify-content:center;padding:16px" onclick="if(event.target===this)closeShareModal()">
<div style="background:#0e0e1a;border:1px solid rgba(255,255,255,.1);border-radius:20px;width:100%;max-width:400px;overflow:hidden">
<div style="padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.07);display:flex;justify-content:space-between;align-items:center">
<h5 style="margin:0;font-size:.95rem;color:#fff"><i class="bi bi-send" style="color:var(--accent2)"></i> Bölümü Paylaş</h5>
<button onclick="closeShareModal()" style="background:none;border:none;color:rgba(255,255,255,.4);font-size:1.1rem;cursor:pointer"><i class="bi bi-x-lg"></i></button>
</div>
{{-- Episode preview --}}
<div style="padding:14px 20px;display:flex;gap:12px;align-items:center;border-bottom:1px solid rgba(255,255,255,.06)">
@if($anime->coverUrl)
<img src="{{ $anime->coverUrl }}" style="width:44px;height:66px;border-radius:8px;object-fit:cover;flex-shrink:0" alt="">
@endif
<div>
<div style="font-size:.82rem;font-weight:800;color:#fff">{{ $anime->title }}</div>
<div style="font-size:.74rem;color:rgba(255,255,255,.45)">{{ $seasonModel->season_number }}. Sezon {{ $ep->episode_number }}. Bölüm{{ $ep->title?' '.$ep->title:'' }}</div>
<label style="display:flex;align-items:center;gap:6px;margin-top:6px;font-size:.74rem;color:rgba(255,255,255,.5);cursor:pointer">
<input type="checkbox" id="share-ts-chk" style="accent-color:var(--accent2)"> Şu anki konumdan başlat
</label>
</div>
</div>
{{-- User search --}}
<div style="padding:14px 20px">
<input type="text" id="share-search" placeholder="Kullanıcı ara (username)..." autocomplete="off"
style="width:100%;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:9px 14px;color:#fff;font-size:.85rem;font-family:inherit;outline:none"
oninput="shareSearch(this.value)">
<div id="share-results" style="margin-top:8px;max-height:200px;overflow-y:auto"></div>
<div id="share-selected" style="display:none;margin-top:10px"></div>
</div>
<div style="padding:0 20px 16px">
<button id="share-send-btn" onclick="doShareSend()" disabled
style="width:100%;padding:11px;border-radius:12px;border:none;background:var(--accent2);color:#05050e;font-size:.88rem;font-weight:700;cursor:pointer;transition:all .15s;opacity:.5"
>Gönder</button>
</div>
</div>
</div>
@endauth
{{-- Watch Party Modal --}}
@auth
<div id="party-modal-bg" onclick="if(event.target===this)closePartyModal()">
<div id="party-modal">
<div style="padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.07);display:flex;justify-content:space-between;align-items:center">
<h5 style="margin:0;font-size:.95rem;color:#fff"><i class="bi bi-people-fill" style="color:#ff2d7d"></i> Birlikte İzle — Watch Party</h5>
<button onclick="closePartyModal()" style="background:none;border:none;color:rgba(255,255,255,.4);font-size:1.1rem;cursor:pointer"><i class="bi bi-x-lg"></i></button>
</div>
<div id="party-modal-body" style="padding:20px">
<div id="party-create-view">
<p style="font-size:.84rem;color:rgba(255,255,255,.55);margin-bottom:16px">Oda oluştur, kodunu arkadaşlarınla paylaş ve aynı anda izleyin.</p>
<label style="display:flex;align-items:center;gap:10px;font-size:.84rem;color:rgba(255,255,255,.6);margin-bottom:12px;cursor:pointer">
<input type="checkbox" id="party-private" style="accent-color:#ff2d7d"> Şifreli oda
</label>
<div id="party-pw-wrap" style="display:none;margin-bottom:12px">
<input type="text" id="party-pw" placeholder="Oda şifresi" maxlength="30"
style="width:100%;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:9px 14px;color:#fff;font-size:.85rem;font-family:inherit;outline:none;box-sizing:border-box">
</div>
<div style="margin-bottom:16px">
<div id="party-join-wrap" style="display:flex;gap:8px;align-items:center">
<input type="text" id="party-join-code" placeholder="Oda kodu gir (6 harf)" maxlength="10"
style="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:.85rem;font-family:inherit;outline:none;text-transform:uppercase"
oninput="this.value=this.value.toUpperCase()">
<button onclick="joinParty()" style="padding:9px 16px;border-radius:10px;border:none;background:rgba(0,245,255,.15);color:var(--accent2);font-size:.85rem;font-weight:700;font-family:inherit;cursor:pointer">
Katıl
</button>
</div>
<div style="text-align:center;font-size:.74rem;color:rgba(255,255,255,.3);margin:10px 0">veya</div>
</div>
<button onclick="createParty()" style="width:100%;padding:11px;border-radius:12px;border:none;background:linear-gradient(135deg,#ff2d7d,#ff6b6b);color:#fff;font-size:.88rem;font-weight:700;cursor:pointer">
<i class="bi bi-plus-lg"></i> Yeni Oda Oluştur
</button>
</div>
<div id="party-room-view" style="display:none">
<div style="text-align:center;margin-bottom:16px">
<div style="font-size:.78rem;color:rgba(255,255,255,.4);margin-bottom:6px">Oda Kodu</div>
<div id="party-room-code" style="font-size:2rem;font-weight:900;letter-spacing:.2em;color:#ff2d7d;cursor:pointer" onclick="copyRoomCode()" title="Kopyala"></div>
<div style="font-size:.72rem;color:rgba(255,255,255,.3);margin-top:4px">Kopyalamak için tıkla</div>
</div>
<div id="party-members-list" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px;min-height:40px"></div>
<button onclick="leaveParty()" style="width:100%;padding:9px;border-radius:10px;border:1px solid rgba(255,80,80,.3);background:rgba(255,80,80,.08);color:#ff5050;font-size:.84rem;font-weight:700;cursor:pointer">
<i class="bi bi-box-arrow-right"></i> Odadan Çık
</button>
</div>
</div>
</div>
</div>
@endauth
@endsection
{{-- Tracking meta tags --}}
<meta name="track-anime-id" content="{{ $anime->id }}">
<meta name="track-episode-id" content="{{ $ep->id }}">
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.13/dist/hls.min.js" onerror="window._hlsLoadFailed=true"></script>
@if(!empty($adsConfig['enabled']) && !empty($adsConfig['vast_url']))
<script src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"></script>
@endif
<script>
console.log('[Animexe Player] v3 loaded');
const M3U8 = @json($ep->m3u8_url);
const MP4 = @json($ep->video_url);
const IS_AUTH = {{ auth()->check() ? 'true' : 'false' }};
// ── Referer'sız doğrudan yükleme politikası ────────────────────────────────────
// Anizium CDN'i: Referer=animexe.com → 403, ama Referer YOK → 200 (+ CORS animexe.com'a açık).
// Bu yüzden tüm alt-istekler referer'sız gitmeli. Sunucu proxy'sine (IP bloklu → 502) gerek kalmaz.
(function(){
try {
var m = document.createElement('meta');
m.name = 'referrer'; m.content = 'no-referrer';
(document.head || document.documentElement).appendChild(m);
} catch(e) {}
})();
// CORS açık CDN'ler — doğrudan tarayıcıdan yükle (sunucu proxy YOK).
// NOT: aniziumserver.sbs/site/com buradan bilerek çıkarıldı — CORS izni
// vermiyorlar (browser console'da doğrulandı), proxy'den geçmeleri gerekiyor.
const DIRECT_CORS_HOSTS = [
'anizium.co',
'misakina.asia', 'misakina.cfd',
'irtau1.online',
'tsuriko-1.asia', 'tsuriko-2.asia', 'tsuriko-3.asia',
'rhyzoku-2.asia', 'rhyzoku-4.asia',
'kamadotanjiro.asia', 'uryuishida.asia',
'tohru.icu', 'tohru.cyou',
'zappy-net.store', 'pixel-quirk.shop', 'tau-video.xyz',
];
// Referer gereken (CORS kapalı) CDN'ler için son çare sunucu proxy'si
const KNOWN_REFS = {
'aniziumserver.sbs': 'https://anizium.co/',
'aniziumserver.site': 'https://anizium.co/',
'aniziumserver.com': 'https://anizium.co/',
};
// Sunucu-proxy'sine sarılmış URL'yi çöz (gerçek CDN URL'sini çıkar)
function _unwrapProxy(url) {
try {
var m = url.match(/\/(?:stream\/proxy|cf-proxy)\?u=([^&]+)/);
if (m) { return atob(decodeURIComponent(m[1])); }
m = url.match(/\/hls-proxy\.php\?(?:.*&)?url=([^&]+)/);
if (m) { return decodeURIComponent(m[1]); }
} catch(e) {}
return url;
}
function proxyUrl(url) {
if (!url) return url;
try {
// Sunucu proxy'sine sarılmışsa aç — doğrudan CDN'e gitmeyi dene
var wrapped = url.indexOf('/stream/proxy') !== -1
|| url.indexOf('/hls-proxy.php') !== -1
|| url.indexOf('/cf-proxy') !== -1;
var real = wrapped ? _unwrapProxy(url) : url;
var host;
try { host = new URL(real, location.origin).hostname; } catch(e) { return url; }
// Kendi origin / CDN → dokunma
if (host.endsWith('animexe.com') || host.endsWith('b-cdn.net') || host === location.hostname) return real;
// CORS açık CDN → DOĞRUDAN (no-referrer meta sayesinde 200)
if (DIRECT_CORS_HOSTS.some(function(d){ return host === d || host.endsWith('.' + d); })) return real;
// Zaten sarılmış ama bilinmeyen host → sunucu proxy'sini koru
if (wrapped) return url;
// Sarılmamış dış URL, referer gereken CDN → sunucu proxy'si (son çare)
var ref = (Object.entries(KNOWN_REFS).find(function(e){ return host.endsWith(e[0]); }) || [null,''])[1];
if (ref) return '/hls-proxy.php?ref=' + encodeURIComponent(ref) + '&url=' + encodeURIComponent(real);
return real; // referer gerekmiyorsa doğrudan dene
} catch(e) { return url; }
}
const SUBS = @json($subtitlesData);
const VIDEO_SOURCES = @json($videoSourcesData ?? []);
// ── Reklam (VAST / IMA) ───────────────────────────────────────────────────────
@if(!empty($adsConfig['enabled']) && !empty($adsConfig['vast_url']))
(function initAds() {
const VAST_URL = @json($adsConfig['vast_url']);
const FREQ_EPISODES = @json($adsConfig['freq_episodes']);
const FREQ_MS = @json($adsConfig['freq_minutes']) * 60 * 1000;
const LS_KEY = 'axe_ad';
function _adState() {
try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch { return {}; }
}
function _saveState(s) {
try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {}
}
// Önce mevcut durumu oku (tick'ten ÖNCE)
const _state = _adState();
const _now = Date.now();
// Bölüm sayacını artır ve kaydet
_state.epsSinceLast = (_state.epsSinceLast || 0) + 1;
_saveState(_state);
// Frekans kontrolü (tick sonrası değerle)
const _lastShown = _state.lastShown || 0;
if ((_now - _lastShown) < FREQ_MS) return; // dakika limiti dolmadı
if (_state.epsSinceLast < FREQ_EPISODES) return; // bölüm limiti dolmadı
// IMA SDK yüklü değilse (adblocker vb.) sessizce geç
if (typeof google === 'undefined' || !google.ima) return;
const adContainer = document.getElementById('ad-container');
if (!adContainer) return;
let adsManager, adDisplayContainer, adsLoader;
let _adStarted = false;
// Güvenlik: IMA 8 saniyede cevap vermezse videoyu başlat
const _fallbackTimer = setTimeout(() => {
if (!_adStarted) { _adError(); }
}, 8000);
function _startMainVideo() {
clearTimeout(_fallbackTimer);
window._imaAdPending = false;
if (typeof startPlayer === 'function') startPlayer();
}
function _adDone() {
adContainer.style.display = 'none';
try { adsManager && adsManager.destroy(); } catch {}
_saveState({ lastShown: Date.now(), epsSinceLast: 0 });
_startMainVideo();
}
function _adError() {
adContainer.style.display = 'none';
try { adsManager && adsManager.destroy(); } catch {}
_startMainVideo();
}
function _onAdsLoaded(e) {
_adStarted = true;
adsManager = e.getAdsManager(document.getElementById('vd'));
adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, _adDone);
adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPED, _adDone);
adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, _adDone);
adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, _adError);
// Reklam başlarken ana videoyu durdur (IMA standart CONTENT_PAUSE_REQUESTED)
adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, () => {
const v = document.getElementById('vd');
if (v && !v.paused) v.pause();
});
try {
adContainer.style.display = 'block';
const w = adContainer.offsetWidth || window.innerWidth || 640;
const h = adContainer.offsetHeight || window.innerHeight || 360;
adsManager.init(w, h, google.ima.ViewMode.NORMAL);
adsManager.start();
} catch { _adError(); }
}
adDisplayContainer = new google.ima.AdDisplayContainer(adContainer, document.getElementById('vd'));
adDisplayContainer.initialize();
adsLoader = new google.ima.AdsLoader(adDisplayContainer);
adsLoader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, _onAdsLoaded);
adsLoader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, _adError);
const req = new google.ima.AdsRequest();
req.adTagUrl = VAST_URL;
req.linearAdSlotWidth = window.innerWidth || 640;
req.linearAdSlotHeight = window.innerHeight || 360;
adsLoader.requestAds(req);
window._imaAdPending = true;
})();
@endif
// ── Kendi MP4 Pre-roll Reklam Sistemi ────────────────────────────────────────
@if(!empty($vadConfig['mode']))
(function initVideoAd() {
const CFG = @json($vadConfig);
const LS_KEY = 'axe_vad';
function _st() { try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch { return {}; } }
function _save(s) { try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {} }
// Frekans kontrolü: N bölümde 1 + M dakikada max 1
const state = _st();
state.epsSinceLast = (state.epsSinceLast || 0) + 1;
_save(state);
const now = Date.now();
if ((now - (state.lastShown || 0)) < CFG.freq_minutes * 60 * 1000) return;
if (state.epsSinceLast < CFG.freq_episodes) return;
// Reklam gösterilecek — ana videoyu ve introyu beklet
window._imaAdPending = true;
const vw = document.getElementById('vw');
if (!vw) { window._imaAdPending = false; return; }
function _markShown() { _save({ lastShown: Date.now(), epsSinceLast: 0 }); }
function _vadDone() {
_markShown();
document.getElementById('vad-overlay')?.remove();
window._imaAdPending = false;
// Intro varsa önce intro, yoksa direkt ana video
if (typeof INTRO_URL !== 'undefined' && INTRO_URL) {
const iv = document.getElementById('intro-vd');
if (iv) {
iv.play().catch(() => { iv.muted = true; iv.play().catch(() => window.skipIntro?.()); });
return;
}
}
if (typeof startPlayer === 'function') startPlayer();
}
function _beacon(path) {
try { fetch(path, { method: 'POST', keepalive: true }).catch(() => {}); } catch {}
}
// Reklam/upsell overlay stilleri — mobil uyumlu (bir kez enjekte)
function _injectVadStyle() {
if (document.getElementById('vad-style')) return;
document.head.insertAdjacentHTML('beforeend', `<style id="vad-style">
.vad-card{margin:auto;max-width:420px;width:100%;text-align:center;background:linear-gradient(160deg,#1a1035,#0d0a20);border:1px solid rgba(167,139,250,.35);border-radius:18px;padding:30px 26px;box-shadow:0 20px 60px rgba(0,0,0,.6)}
.vad-emoji{font-size:40px;margin-bottom:10px;line-height:1}
.vad-title{font-size:19px;font-weight:800;color:#fff;margin-bottom:8px}
.vad-desc{font-size:13px;color:#b8b3d9;line-height:1.55;margin-bottom:20px}
.vad-btn-premium{display:block;background:linear-gradient(90deg,#8b5cf6,#6d28d9);color:#fff;font-weight:700;font-size:14.5px;padding:13px;border-radius:10px;text-decoration:none;margin-bottom:10px}
.vad-btn-skip{width:100%;background:rgba(255,255,255,.07);color:#8f8ab0;border:1px solid rgba(255,255,255,.12);font-size:14px;font-weight:600;padding:12px;border-radius:10px;cursor:not-allowed}
.vad-btn-skip.ready{background:rgba(255,255,255,.16);color:#fff;cursor:pointer}
.vad-bar{position:absolute;left:0;right:0;bottom:0;min-height:56px;background:linear-gradient(transparent,rgba(0,0,0,.8));display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 14px;z-index:1}
.vad-cta{color:#c4b5fd;font-size:12.5px;font-weight:600;text-decoration:none;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.vad-skip{flex:0 0 auto;background:rgba(255,255,255,.14);color:#cfcbe8;border:1px solid rgba(255,255,255,.25);font-size:13px;font-weight:700;padding:9px 16px;border-radius:8px;cursor:not-allowed;white-space:nowrap}
.vad-skip.ready{background:rgba(255,255,255,.24);color:#fff;cursor:pointer}
@media (max-width:480px){ .vad-cta{font-size:11px} .vad-skip{padding:8px 12px;font-size:12px} }
@media (max-width:360px){ .vad-cta{display:none} }
@media (max-height:560px){
.vad-card{padding:16px 18px;border-radius:14px}
.vad-emoji{font-size:26px;margin-bottom:4px}
.vad-title{font-size:15px;margin-bottom:5px}
.vad-desc{font-size:11.5px;line-height:1.4;margin-bottom:12px}
.vad-btn-premium{padding:10px;font-size:13px;margin-bottom:7px}
.vad-btn-skip{padding:9px;font-size:13px}
}
@media (max-width:420px){ .vad-card{padding:20px 16px} }
</style>`);
}
// ── Upsell modu: reklam yerine premium teklifi ────────────────────────────
if (CFG.mode === 'upsell') {
_injectVadStyle();
vw.insertAdjacentHTML('beforeend', `
<div id="vad-overlay" class="vad-ov-upsell" style="position:absolute;inset:0;z-index:60;background:rgba(5,5,16,.96);display:flex;overflow-y:auto;padding:16px">
<div class="vad-card">
<div class="vad-emoji">👑</div>
<div class="vad-title">Reklamsız İzlemek İster misin?</div>
<div class="vad-desc">
Premium üyelikle hiç reklam görmeden, kesintisiz anime izlersin.
Üstelik özel rozetler ve daha fazlası seni bekliyor.
</div>
<a href="${CFG.premium_url}" class="vad-btn-premium">👑 Premium'a Geç</a>
<button id="vad-skip" class="vad-btn-skip" disabled>Devam Et (5)</button>
</div>
</div>`);
const skipBtn = document.getElementById('vad-skip');
let left = 5;
const t = setInterval(() => {
left--;
if (left <= 0) {
clearInterval(t);
skipBtn.textContent = 'Devam Et ▶';
skipBtn.disabled = false;
skipBtn.classList.add('ready');
skipBtn.onclick = _vadDone;
} else {
skipBtn.textContent = `Devam Et (${left})`;
}
}, 1000);
return;
}
// ── Reklam modu: MP4 pre-roll ─────────────────────────────────────────────
_injectVadStyle();
const AD = CFG.ad;
vw.insertAdjacentHTML('beforeend', `
<div id="vad-overlay" style="position:absolute;inset:0;z-index:60;background:#000">
<video id="vad-vd" src="${AD.url}" playsinline webkit-playsinline preload="auto"
style="width:100%;height:100%;object-fit:contain;display:block"></video>
${AD.click_url ? `<a id="vad-click" href="${AD.click_url}" target="_blank" rel="noopener sponsored" style="position:absolute;inset:0 0 64px 0;display:block"></a>` : ''}
<div style="position:absolute;top:10px;left:10px;background:rgba(0,0,0,.65);color:#ffd60a;font-size:11px;font-weight:700;letter-spacing:.08em;padding:4px 10px;border-radius:6px;pointer-events:none">REKLAM</div>
<button id="vad-unmute" style="display:none;position:absolute;top:10px;right:10px;background:rgba(0,0,0,.65);color:#fff;border:1px solid rgba(255,255,255,.25);font-size:12.5px;font-weight:600;padding:7px 12px;border-radius:8px;cursor:pointer">🔊 Sesi Aç</button>
<div class="vad-bar">
<a href="${CFG.premium_url}" class="vad-cta">👑 Premium'a geç, reklamsız izle</a>
<button id="vad-skip" class="vad-skip" disabled>Reklamı Geç (${AD.skip_after})</button>
</div>
<div id="vad-prog" style="position:absolute;left:0;bottom:0;height:3px;width:0%;background:#ffd60a;pointer-events:none;z-index:2"></div>
</div>`);
const adVd = document.getElementById('vad-vd');
const skipBtn = document.getElementById('vad-skip');
const unmute = document.getElementById('vad-unmute');
const prog = document.getElementById('vad-prog');
const clickEl = document.getElementById('vad-click');
let _impSent = false, _skipReady = false, _finished = false;
function _finish() {
if (_finished) return;
_finished = true;
try { adVd.pause(); } catch {}
_vadDone();
}
// Güvenlik: 10 sn içinde oynamaya başlamazsa reklamı atla
const _loadTimeout = setTimeout(() => { if (!_impSent) _finish(); }, 10000);
adVd.addEventListener('playing', () => {
clearTimeout(_loadTimeout);
if (!_impSent) { _impSent = true; _beacon(`/api/slot/${AD.id}/view`); }
});
adVd.addEventListener('timeupdate', () => {
if (adVd.duration > 0) prog.style.width = (adVd.currentTime / adVd.duration * 100) + '%';
if (!_skipReady && AD.skip_after > 0) {
const left = Math.ceil(AD.skip_after - adVd.currentTime);
if (left <= 0) {
_skipReady = true;
skipBtn.textContent = 'Reklamı Geç ▶';
skipBtn.disabled = false;
skipBtn.classList.add('ready');
skipBtn.onclick = _finish;
} else {
skipBtn.textContent = `Reklamı Geç (${left})`;
}
}
});
// skip_after = 0 → geç butonu hiç aktifleşmez, reklam sonuna kadar izlenir
if (AD.skip_after <= 0) skipBtn.style.display = 'none';
adVd.addEventListener('ended', _finish);
adVd.addEventListener('error', _finish);
if (clickEl) clickEl.addEventListener('click', () => _beacon(`/api/slot/${AD.id}/hit`));
// Otomatik oynat — ses engellenirse sessiz başlat + "Sesi Aç" göster
adVd.play().catch(() => {
adVd.muted = true;
unmute.style.display = 'block';
unmute.onclick = () => { adVd.muted = false; unmute.style.display = 'none'; };
adVd.play().catch(_finish);
});
})();
@endif
// ── H.265 uyumluluk (global) ──────────────────────────────────────────────────
const _hevcOk = typeof MediaSource !== 'undefined' && [
'video/mp4; codecs="hev1.1.6.L120.90"',
'video/mp4; codecs="hvc1.1.6.L120.90"',
'video/mp4; codecs="hev1.1.4.L120.90"',
'video/mp4; codecs="hvc1.1.4.L120.90"',
'video/mp4; codecs="hev1"',
'video/mp4; codecs="hvc1"',
].some(ct => MediaSource.isTypeSupported?.(ct));
async function _probeIsHevcOnly(url) {
try {
url = (typeof proxyUrl === 'function') ? proxyUrl(url) : url; // sarılmışsa doğrudan CDN'e
const ctrl = typeof AbortSignal !== 'undefined' && AbortSignal.timeout
? AbortSignal.timeout(6000) : undefined;
const opts = { referrerPolicy: 'no-referrer' };
if (ctrl) opts.signal = ctrl;
const r = await fetch(url, opts);
if (!r.ok) return false;
const txt = await r.text();
const streams = [...txt.matchAll(/#EXT-X-STREAM-INF:([^\n]+)/g)];
if (!streams.length) return false;
const _isHevc = c => /hev1|hvc1|dvh1/i.test(c || '');
// H.264 stream var mı? (CODECS etiketi yoksa bilinmiyor → güvenli say)
const hasH264 = streams.some(m => {
const codecs = (m[1].match(/CODECS="([^"]+)"/) || [])[1] || '';
return !codecs || !_isHevc(codecs);
});
return !hasH264;
} catch { return false; }
}
function _reportHevcSource(url) {
if (!url) return;
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content || '';
fetch('/api/sources/flag-hevc', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({ url }),
keepalive: true,
}).catch(() => {});
}
(async function _filterHevcSources() {
if (_hevcOk) return; // tarayıcı H.265 destekliyor, filtreleme gerekmez
const btns = document.querySelectorAll(
'.vmi-dub[data-url], .src-tab[data-url], .vmi-src[data-url]'
);
for (const btn of btns) {
const url = btn.dataset.url;
if (!url || !url.includes('.m3u8')) continue;
const hevcOnly = await _probeIsHevcOnly(url);
if (hevcOnly) {
btn.style.display = 'none';
btn.classList.remove('on');
_reportHevcSource(url); // admin paneline raporla
}
}
})();
// ── Loading screen ────────────────────────────────────────────────────────────
(function(){
const bg = document.getElementById('vload-bg');
const coverUrl = @json($ep->thumbnailUrl ?: ($anime->coverUrl ?? null));
if (bg && coverUrl) bg.style.backgroundImage = 'url(' + coverUrl + ')';
})();
const _TIPS = [
'💡 Sol/sağ ok tuşları ile 5 sn ileri/geri sar',
'⌨️ J ve L tuşları 10 sn ileri/geri sarmak için',
'🎧 M tuşu ile sesi kapat/aç',
'🖥️ F tuşu veya çift tıkla tam ekrana geç',
'📺 Ayarlar menüsünden video kalitesini değiştirebilirsin',
'⏸️ Boşluk tuşu ile durdur / devam et',
'🔊 Yukarı/aşağı ok tuşları ses seviyesini ayarlar',
'💾 Kaldığın yerden devam etmek için giriş yap',
'🎯 İzleme geçmişin otomatik kaydediliyor',
'⭐ Beğendiğin bölümlere yorum bırakmayı unutma',
'🎮 Birlikte İzle özelliğiyle arkadaşlarınla izle',
'🔔 Favori animeler yeni bölüm gelince seni bilgilendirir',
'🌙 Geceleri ses yüksekliğini düşük tut, komşuları düşün 😄',
'🎬 Bölüm bittikten sonra Ruh Hali Motoru ile sonra ne izleyeceğini seç',
];
let _tipIdx = Math.floor(Math.random() * _TIPS.length);
let _tipTimer = null, _tipDelayTimer = null;
const _vloadTipEl = document.getElementById('vload-tip');
const _vloadEl = document.getElementById('vload');
const _vloadPlayBtn = document.getElementById('vload-playbtn');
let _vloadFirstPlay = false; // ilk playing eventi sonrası vload kalıcı kapanır
function _showNextTip() {
if (!_vloadTipEl) return;
_vloadTipEl.classList.add('fade');
setTimeout(() => {
_vloadTipEl.textContent = _TIPS[_tipIdx % _TIPS.length];
_vloadTipEl.classList.remove('fade');
_tipIdx++;
}, 350);
}
const _vloadRetryBtn = document.getElementById('vload-retrybtn');
let _vloadRetryTimer = null;
function showVload() {
if (!_vloadEl) return;
_vloadEl.style.display = ''; // hideVload tarafından gizlenmiş olabilir
_vloadFirstPlay = false;
_vloadEl.classList.add('on');
// Label'ı "Yükleniyor"a geri döndür (_showVloadPlayBtn "Oynatmaya hazır" yapmış olabilir)
const _lbl = document.querySelector('.vload-lbl');
if (_lbl) _lbl.innerHTML = 'Yükleniyor<span class="vload-ldots"></span>';
if (_vloadPlayBtn) _vloadPlayBtn.classList.remove('show');
if (_vloadRetryBtn) _vloadRetryBtn.classList.remove('show');
// İpuçları yükleme uzarsa (>1.2s) göster
clearTimeout(_tipDelayTimer);
clearInterval(_tipTimer);
clearTimeout(_vloadRetryTimer);
_tipDelayTimer = setTimeout(() => {
_showNextTip();
_tipTimer = setInterval(_showNextTip, 3800);
}, 1200);
// 12s sonra hâlâ yükleniyorsa "Yenile" butonu göster
_vloadRetryTimer = setTimeout(() => {
if (_vloadEl.classList.contains('on') && _vloadRetryBtn) {
_vloadRetryBtn.classList.add('show');
}
}, 12000);
}
function hideVload() {
if (!_vloadEl || _vloadFirstPlay) return;
_vloadFirstPlay = true;
_vloadEl.classList.remove('on');
// Transition tamamlanmayabilir — 420ms sonra display:none ile garantile
setTimeout(() => { if (_vloadEl && !_vloadEl.classList.contains('on')) _vloadEl.style.display = 'none'; }, 420);
clearTimeout(_tipDelayTimer);
clearInterval(_tipTimer);
clearTimeout(_vloadRetryTimer);
if (_vloadRetryBtn) _vloadRetryBtn.classList.remove('show');
}
function _showVloadPlayBtn() {
// Unmute chip varsa kaldır — zaten "tıkla" butonu var
hideUnmute();
// "Yükleniyor" yanılgısını kır: kaynak hazır, sadece autoplay engeli var
const _lbl = document.querySelector('.vload-lbl');
if (_lbl) _lbl.innerHTML = 'Oynatmaya hazır';
clearTimeout(_tipDelayTimer); clearInterval(_tipTimer);
if (_vloadTipEl) _vloadTipEl.textContent = '';
if (_vloadPlayBtn) _vloadPlayBtn.classList.add('show');
}
function _vloadPlay() {
if (_vloadPlayBtn) _vloadPlayBtn.classList.remove('show');
// Sessizi kaldırarak dene, olmazsa sesli olmadan devam et
vd.muted = false;
vd.play()
.then(hideVload)
.catch(() => {
vd.muted = true;
vd.play()
.then(() => { hideVload(); showUnmute(); })
.catch(() => { _vloadPlayBtn?.classList.add('show'); });
});
}
const HAS_NXT = @json((bool)$next);
const NXT_URL = @json($nextUrl);
// ─── Intro video ──────────────────────────────────────────────────────────────
const INTRO_URL = @json($introUrl); // null ise intro yok
const INTRO_SKIP_AFTER = @json($introSkipAfter); // kaç saniye sonra skip butonu çıkar
const MAIN_SKIP_SEC = @json($mainVideoSkipSec); // ana videoda kaç saniye atlanacak
const WM_COVER_SECS = @json($wmCoverSeconds); // rakip watermark kaç saniye kapalı kalacak
let ANISKIP = @json($aniSkip ?? null); // {op:{start,end}, ed:{start,end}} veya null — also fetched async below
// debug: console'dan kontrol: window._introDebug
window._introDebug = { ANISKIP, ep_intro_start: @json($ep->intro_start ?? null), ep_intro_end: @json($ep->intro_end ?? null) };
(function initIntro() {
if (!INTRO_URL) return;
const overlay = document.getElementById('intro-overlay');
const introVd = document.getElementById('intro-vd');
const skipBtn = document.getElementById('intro-skip');
const timerEl = document.getElementById('intro-timer');
if (!overlay || !introVd) return;
let skipCountdown = null;
function skipIntro() { // global da tanımlı
introVd.pause();
overlay.classList.add('hidden');
clearInterval(skipCountdown);
startMainVideo();
}
window.skipIntro = skipIntro;
// Skip butonu geri sayımı
let remaining = INTRO_SKIP_AFTER;
if (remaining <= 0) {
skipBtn.classList.add('visible');
timerEl.textContent = '';
} else {
timerEl.textContent = '(' + remaining + ')';
skipCountdown = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(skipCountdown);
skipBtn.classList.add('visible');
timerEl.textContent = '';
} else {
timerEl.textContent = '(' + remaining + ')';
}
}, 1000);
}
// Intro bitti → ana video başlat
introVd.addEventListener('ended', skipIntro);
introVd.addEventListener('error', skipIntro); // yüklenemezse geç
// Intro'yu otomatik oynat — reklam bekliyorsa reklam bitince başlatılır (_vadDone)
if (!window._imaAdPending) {
introVd.play().catch(() => {
introVd.muted = true;
introVd.play().catch(skipIntro);
});
}
})();
function startMainVideo() {
if (window._imaAdPending) return;
_mainSkipSec = MAIN_SKIP_SEC;
applyDubPref();
}
let _mainSkipSec = 0; // intro geçildikten sonra ana videoda atlanacak süre
// ─── HLS setup ────────────────────────────────────────────────────────────────
const vd = document.getElementById('vd');
let hls = null;
function loadHLS(url, seekTo = 0, onFail = null) {
if (window._imaAdPending) return;
// HLS.js CDN'den yüklenemedi — kullanıcıya hata göster
if (window._hlsLoadFailed || typeof Hls === 'undefined') {
showVload();
// Native HLS (Safari/iOS) destekliyorsa yine de dene
if (url && vd.canPlayType('application/vnd.apple.mpegurl')) {
vd.src = url;
vd.play().catch(_showVloadPlayBtn);
return;
}
hideVload();
showNoVideo();
showToast('Video oynatıcı yüklenemedi. Sayfayı yenileyin veya reklam engelleyicinizi kapatın.', 8000);
return;
}
const _rawUrl = url; // Proxy öncesi orijinal URL (CORS hatasında retry için)
url = proxyUrl(url);
// Doğrudan CDN yüklemesi mi? (kendi origin'imiz değil = cross-origin CDN'e direkt gidiyoruz)
let _isDirect = false;
try { _isDirect = url.indexOf('http') === 0 && new URL(url).hostname !== location.hostname; } catch(e) {}
vd.pause(); // Ses bozulmasını önle — destroy öncesi durdur
if (hls) { hls.destroy(); hls = null; }
// MediaSource'u tam sıfırla — önceki bufferAppendError (HEVC) SourceBuffer'ı
// bozuk bırakmış olabilir; temizlemezsek sonraki (çalışan) kaynak da patlar.
try {
vd.pause();
vd.removeAttribute('src');
while (vd.firstChild) vd.removeChild(vd.firstChild);
vd.load(); // networkState=EMPTY, eski MediaSource bırakılır
} catch(e) {}
document.getElementById('q-levels').innerHTML = '';
const qLab = document.querySelector('#q-auto span:last-child');
if (qLab) qLab.textContent = 'Otomatik';
if (!url) { showNoVideo(); return; }
vd.style.display = ''; // showNoVideo() tarafından gizlenmiş olabilir — sıfırla
const noVidBox = document.getElementById('no-video-msg');
if (noVidBox) noVidBox.style.display = 'none';
showVload(); // ← Yükleme başlıyor, hemen loading screen göster
// Seek helper — video src değişince loadedmetadata'da uygula
function _seekAndPlay() {
if (seekTo > 0) vd.currentTime = seekTo;
vd.play().catch(() => {
vd.muted = true;
vd.play().catch(_showVloadPlayBtn);
showUnmute();
});
}
function _seekAfterMeta(playFn) {
if (seekTo > 0) {
vd.addEventListener('loadedmetadata', function onMeta() {
vd.removeEventListener('loadedmetadata', onMeta);
vd.currentTime = seekTo;
(playFn || (() => {}))();
});
} else {
(playFn || (() => {}))();
}
}
if (Hls.isSupported()) {
hls = new Hls({
startPosition: seekTo > 0 ? seekTo : -1,
startFragPrefetch: true,
maxBufferLength: 20,
maxMaxBufferLength: 300,
enableWorker: true,
manifestLoadingTimeOut: 8000,
// Direct CDN denemesinde CORS hatasında anında proxy'e geç (0 retry)
// Proxy URL'inde transient hatalar için 2 retry tut
manifestLoadingMaxRetry: _isDirect ? 0 : 2,
levelLoadingTimeOut: 8000,
levelLoadingMaxRetry: _isDirect ? 0 : 2,
fragLoadingTimeOut: 15000,
fragLoadingMaxRetry: 3,
nudgeMaxRetry: 5,
xhrSetup: function(xhr) { xhr.withCredentials = false; },
});
hls.loadSource(url);
hls.attachMedia(vd);
const _thisHls = hls; // race-guard: stale timers/callbacks check this
const _hlsTimeout = setTimeout(() => {
if (hls !== _thisHls) return; // newer loadHLS already took over
hls.destroy(); hls = null;
hideVload();
if (onFail) onFail(); else showNoVideo();
}, 15000);
hls.on(Hls.Events.MANIFEST_PARSED, (_, d) => {
clearTimeout(_hlsTimeout);
// ── Codec uyumluluk filtresi ────────────────────────────────────
const _isHevc = c => /hev1|hvc1|dvh1|hvc/i.test(c || '');
if (!_hevcOk) {
const h264Levels = d.levels.filter(l => !_isHevc(l.attrs?.CODECS));
if (h264Levels.length === 0) {
// H.264 level yok — yine de oynamayı dene.
// Chrome 108+ hardware HEVC destekleyebilir (MediaSource.isTypeSupported
// yanlış false döner). Gerçekten oynatılamazsa bufferAppendError gelir.
console.warn('[HLS] HEVC-only stream, hardware decode deneniyor...');
} else if (h264Levels.length < d.levels.length) {
// Bazı level'lar HEVC — en iyi H.264'ü seç, auto-switch kapat
let bestH264 = 0, bestBw = 0;
d.levels.forEach((l, i) => { if (!_isHevc(l.attrs?.CODECS) && l.bitrate > bestBw) { bestBw = l.bitrate; bestH264 = i; } });
hls.currentLevel = bestH264;
hls.autoLevelEnabled = false;
}
}
// ────────────────────────────────────────────────────────────────
// startPosition HLS config'de set edildi — burada tekrar currentTime set etme,
// ikisi çakışınca video başa atlıyor
// Autoplay: önce sesli, engellenirse sessiz, o da olmazsa tıkla-oynat butonu.
vd.play().catch(() => {
vd.muted = true;
vd.play()
.then(showUnmute)
.catch(_showVloadPlayBtn);
});
// Video oynatılabilir hale gelince (buffer doldu) ama hâlâ duruyorsa →
// autoplay engeli. Sessiz oynatmayı dene, olmazsa DERHAL buton göster (failover YOK).
vd.addEventListener('canplay', function _cpAuto() {
vd.removeEventListener('canplay', _cpAuto);
if (_vloadFirstPlay || hls !== _thisHls) return;
if (vd.paused) {
vd.muted = true;
vd.play().then(showUnmute).catch(_showVloadPlayBtn);
}
});
// Stuck watchdog: buffer VARSA (readyState>=2) autoplay engelidir → buton göster,
// failover YAPMA (kaynak sağlam). Hiç veri yoksa gerçek stall/codec → sıradakine geç.
const _playStuck = setTimeout(() => {
if (_vloadFirstPlay || hls !== _thisHls) return;
if (vd.readyState >= 2) {
// Video hazır ama oynamıyor = autoplay bloğu. Son bir sessiz deneme + buton.
vd.muted = true;
vd.play().then(showUnmute).catch(() => {});
_showVloadPlayBtn();
return;
}
// Hiç veri yüklenmedi — gerçek sorun, sonraki kaynağa geç
setTimeout(() => {
if (!_vloadFirstPlay && hls === _thisHls && vd.readyState < 2) {
hls?.destroy(); hls = null;
hideVload();
if (onFail) onFail(); else _showVloadPlayBtn();
} else if (!_vloadFirstPlay && vd.readyState >= 2) {
_showVloadPlayBtn();
}
}, 3000);
}, 5000);
vd.addEventListener('playing', () => clearTimeout(_playStuck), { once: true });
const lvls = document.getElementById('q-levels');
lvls.innerHTML = '';
document.querySelectorAll('#mnu-set .vmi[data-q]').forEach(e => e.classList.remove('on'));
const qAuto = document.getElementById('q-auto');
if (qAuto) { qAuto.classList.add('on'); }
if (hls && hls.autoLevelEnabled) hls.currentLevel = -1;
d.levels.forEach((l, i) => {
const h = l.height || Math.round(l.width * 9/16);
const isHevcLvl = _isHevc(l.attrs?.CODECS);
const div = document.createElement('div');
div.className = 'vmi' + (isHevcLvl && !_hevcOk ? ' disabled' : '');
div.dataset.q = i;
div.title = isHevcLvl ? 'H.265 — tarayıcı desteği yok' : '';
div.innerHTML = `<span class="dot"></span><span>${h}p${isHevcLvl ? ' H.265' : (l.attrs?.CODECS?.includes('avc1')||h>=1080?' HD':'')}</span>`;
if (!isHevcLvl || _hevcOk) div.onclick = () => doQuality(i, div);
lvls.appendChild(div);
});
});
let _hevcRecovered = false, _mediaRecovered = false, _netErrCount = 0, _mediaErrCount = 0;
hls.on(Hls.Events.ERROR, (_, data) => {
if (hls !== _thisHls) return; // stale event from a destroyed/replaced instance
if (!data.fatal) {
// Manifest hiç yüklenemiyorsa (proxy 502 / ölü mirror) tekrar deneme —
// doğrudan failover ile sıradaki kaynağa geç (aynı URL'yi dövme).
if (data.type === Hls.ErrorTypes.NETWORK_ERROR &&
(data.details === 'manifestLoadError' || data.details === 'manifestLoadTimeOut' ||
data.details === 'levelLoadError' || data.details === 'levelLoadTimeOut')) {
const _reUrl = hls.url, _reTime = vd.currentTime;
hls.destroy(); hls = null;
if (onFail) { onFail(); }
else { loadHLS(_reUrl, _reTime, showNoVideo); }
return;
} else if (data.type === Hls.ErrorTypes.NETWORK_ERROR && _netErrCount < 3) {
// Segment (fragment) hatası: birkaç kez mevcut pozisyondan yüklemeyi dene
_netErrCount++;
setTimeout(() => { if (hls) hls.startLoad(); }, 800 * _netErrCount);
} else if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
// 3 retry sonrası hâlâ segment hatası — failover ile sıradaki kaynağa geç
_netErrCount = 0;
const _reTime = vd.currentTime;
hls.destroy(); hls = null;
if (onFail) { onFail(); }
else { showNoVideo(); }
return;
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
// Non-fatal media hataları birikirse (codec sorun) → fatal gibi işle
_mediaErrCount++;
if (_mediaErrCount >= 4) {
data.fatal = true; // aşağıdaki fatal handler'a düş
} else {
return;
}
} else {
return;
}
}
if (!data.fatal) return;
_netErrCount = 0;
clearTimeout(_hlsTimeout);
console.error('[HLS] Fatal:', data.type, data.details, data);
// bufferAppendError = codec uyumsuzluğu (genelde H.265 → Chrome'da olmaz)
if (data.details === 'bufferAppendError' || data.details === 'bufferAddCodecError') {
// TEŞHİS: bu kaynağın gerçek codec'lerini yazdır (HEVC mi H.264 mü?)
try {
const _codecs = (hls && hls.levels || []).map(l => (l.attrs && l.attrs.CODECS) || l.videoCodec || '?').join(', ');
console.warn('[HEVC-teşhis] Kaynak codecs:', _codecs || '(bilinmiyor)', '| _hevcOk:', _hevcOk);
} catch(e) {}
showVload(); // Siyah ekran yerine loading göster
// Sadece bir kez H.264 level'a geçmeyi dene — tekrar gelirse direkt destroy
if (!_hevcRecovered) {
const _isH265 = c => /hev1|hvc1|dvh1|hvc/i.test(c || '');
const levels = hls?.levels || [];
// Sadece CODECS açıkça belirtilmiş ve H.265 olmayan level'a geç
// CODECS bilinmiyorsa (undefined) → o da HEVC olabilir, deneme
const h264Idx = levels.findIndex(l => { const c = l.attrs?.CODECS || ''; return c && !_isH265(c); });
if (h264Idx >= 0) {
_hevcRecovered = true;
hls.currentLevel = h264Idx;
hls.autoLevelEnabled = false;
hls.recoverMediaError();
return;
}
}
hls.destroy(); hls = null;
hideVload();
// Bu oturumda hatırla + backend'e raporla (admin görsün, sonraki kullanıcı hızlı açsın)
if (_rawUrl) _knownHevcUrls[_rawUrl] = 1;
_reportHevcSource(_rawUrl);
if (onFail) {
onFail();
} else {
showToast('Bu video H.265 (HEVC) formatında — Chrome desteklemiyor. Başka kaynak deneyin.', 8000);
showNoVideo();
}
return;
}
// Media decode / buffer hatası → bir kez recoverMediaError dene
if (data.type === Hls.ErrorTypes.MEDIA_ERROR && !_mediaRecovered) {
_mediaRecovered = true;
hls.recoverMediaError();
return;
}
hls.destroy(); hls = null;
// Doğrudan yükleme başarısız (CORS/network) → sunucu proxy'si IP-bloklu (502),
// ona dönmek anlamsız. Doğrudan sıradaki kaynağa (failover) geç.
hideVload();
if (onFail) onFail(); else showNoVideo();
});
} else if (vd.canPlayType('application/vnd.apple.mpegurl') && url) {
// Safari / iOS native HLS
vd.src = url;
_seekAfterMeta(() => vd.play().catch(() => { _showVloadPlayBtn(); }));
document.getElementById('q-levels').innerHTML = '';
document.querySelectorAll('#mnu-set .vmi[data-q]').forEach(e => e.classList.remove('on'));
const qAuto = document.getElementById('q-auto');
if (qAuto) { qAuto.classList.add('on'); qAuto.querySelector('span:last-child').textContent = 'Otomatik (cihaz)'; }
} else if (MP4) {
vd.src = MP4;
_seekAfterMeta(() => vd.play().catch(() => { _showVloadPlayBtn(); }));
document.getElementById('q-levels').innerHTML = '';
document.querySelectorAll('#mnu-set .vmi[data-q]').forEach(e => e.classList.remove('on'));
const qAuto = document.getElementById('q-auto');
if (qAuto) { qAuto.classList.add('on'); qAuto.querySelector('span:last-child').textContent = 'Tek kalite (MP4)'; }
}
}
function showNoVideo() {
hideVload();
const vdEl = document.getElementById('vd');
if (vdEl) vdEl.style.display = 'none';
let box = document.getElementById('no-video-msg');
if (!box) {
box = document.createElement('div');
box.id = 'no-video-msg';
box.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#0a0a0a;color:#aaa;gap:10px;z-index:10;';
box.innerHTML = '<i class="bi bi-wifi-off" style="font-size:2.5rem;color:#444"></i>' +
'<div style="font-size:.95rem;font-weight:700;color:#bbb">Video yüklenemedi</div>' +
'<div style="font-size:.78rem;color:#555;margin-bottom:2px">Bağlantı hatası veya kaynak erişim sorunu</div>' +
'<button onclick="location.reload()" style="margin-top:4px;padding:9px 22px;border-radius:50px;border:1px solid rgba(0,245,255,.35);background:rgba(0,245,255,.07);color:#00f5ff;font-size:.82rem;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:6px;transition:background .15s" onmouseover="this.style.background=\'rgba(0,245,255,.14)\'" onmouseout="this.style.background=\'rgba(0,245,255,.07)\'">' +
'<i class="bi bi-arrow-clockwise"></i> Yeniden Dene</button>';
const wrap = vdEl?.parentElement || document.getElementById('vd-clip') || document.getElementById('vw');
if (wrap) { wrap.style.position = 'relative'; wrap.appendChild(box); }
}
box.style.display = 'flex';
}
function _startMp4(sk) {
if (window._imaAdPending) return;
vd.style.display = '';
const noVidBox = document.getElementById('no-video-msg');
if (noVidBox) noVidBox.style.display = 'none';
showVload();
vd.src = MP4;
const _canPlay = () => { vd.removeEventListener('canplay', _canPlay); };
vd.addEventListener('canplay', _canPlay);
_triedUrls = new Set(); if (MP4) _triedUrls.add(MP4); // failover için MP4'ü işaretle
const _err = () => {
vd.removeEventListener('error', _err); vd.removeEventListener('canplay', _canPlay);
console.warn('[Player] MP4 err:', vd.error && vd.error.code);
// CDN MP4 açılmazsa anizium HLS / diğer kaynaklara geç (siyah ekran yerine)
_failoverNext(sk);
};
vd.addEventListener('error', _err);
if (sk > 0) {
vd.addEventListener('loadedmetadata', function _meta() {
vd.removeEventListener('loadedmetadata', _meta);
vd.removeEventListener('error', _err);
vd.currentTime = sk;
vd.play().catch(() => { vd.muted = true; vd.play().then(showUnmute).catch(() => {}); });
});
} else {
vd.play().catch(() => { vd.muted = true; vd.play().then(showUnmute).catch(() => {}); });
}
}
// ═══ EVRENSEL KAYNAK FAILOVER ══════════════════════════════════════════════════
// Bir kaynak 403/HEVC/timeout ile ölürse, sıradaki oynatılabilir kaynağa otomatik geç.
// Denenen URL'ler _triedUrls'te tutulur → sonsuz döngü olmaz.
let _triedUrls = new Set();
let _bsRetryUrl = null; // siyah ekran aynı URL'de tekrarlarsa failover tetikle (aşağıda kullanılıyor)
// Tüm oynatılabilir kaynakları öncelik sırasıyla topla (tekilleştirilmiş).
// Sıra: aktif/kaydedilmiş dub → diğer dublajlar → kalite sekmeleri → VIDEO_SOURCES → M3U8 → MP4
function _allPlayableUrls() {
const list = [];
const add = (url, type, opts) => {
if (!url) return;
list.push(Object.assign({ url: url, type: type || 'hls' }, opts || {}));
};
// 1) Aktif dub önce, sonra diğerleri (original'ı öne al — en stabil)
const dubEls = Array.from(document.querySelectorAll('.vmi-dub[data-url]'));
const activeDub = dubEls.find(e => e.classList.contains('on'));
const origDub = dubEls.find(e => e.dataset.key === 'original');
const orderedDubs = [];
if (activeDub) orderedDubs.push(activeDub);
if (origDub && origDub !== activeDub) orderedDubs.push(origDub);
dubEls.forEach(e => { if (orderedDubs.indexOf(e) === -1) orderedDubs.push(e); });
orderedDubs.forEach(e => add(e.dataset.url, 'hls', { dubEl: e, label: e.dataset.lbl || 'Dublaj' }));
// 2) Kaynak/çevirmen sekmeleri
document.querySelectorAll('.vmi-src[data-url], .src-tab[data-url]').forEach(e =>
add(e.dataset.url, e.dataset.type || 'hls', { srcEl: e, label: e.dataset.lbl || 'Kaynak' })
);
// 3) VIDEO_SOURCES (PHP tarafında 1080→720→4K sıralı geliyor)
(VIDEO_SOURCES || []).forEach(s => add(s.url, s.type || 'hls', { label: s.label || s.quality || 'Kaynak' }));
// 4) Ana M3U8 ve MP4 son yedek
add(M3U8, 'hls', { label: 'Ana kaynak' });
if (MP4) add(MP4, (MP4.indexOf('.m3u8') !== -1 || MP4.indexOf('/stream/') === 0) ? 'hls' : 'mp4', { label: 'MP4' });
// Tekilleştir
const seen = {}, out = [];
for (const c of list) {
if (c.url && !seen[c.url]) { seen[c.url] = 1; out.push(c); }
}
// HEVC-farkında sıralama: tarayıcı H.265 oynatamıyorsa, daha önce HEVC işaretlenmiş
// kaynakları en sona at → failover önce çalışan H.264'leri dener (hızlı açılış).
if (!_hevcOk) {
const hevcUrls = {};
(VIDEO_SOURCES || []).forEach(s => { if (s.is_hevc && s.url) hevcUrls[s.url] = 1; });
if (typeof _knownHevcUrls === 'object') Object.keys(_knownHevcUrls).forEach(u => hevcUrls[u] = 1);
out.sort((a, b) => (hevcUrls[a.url] ? 1 : 0) - (hevcUrls[b.url] ? 1 : 0)); // stabil: HEVC sona
}
return out;
}
// Bu oturumda bufferAppendError almış (HEVC) URL'ler — failover'da sona atılır
var _knownHevcUrls = {};
// Sıradaki denenmemiş kaynağı yükle. Hepsi bittiyse hata göster.
function _failoverNext(sk) {
const all = _allPlayableUrls();
let next = null;
for (const c of all) { if (!_triedUrls.has(c.url)) { next = c; break; } }
if (!next) {
console.warn('[Player] Tüm kaynaklar denendi, oynatılamadı (' + _triedUrls.size + ')');
showNoVideo();
return;
}
_triedUrls.add(next.url);
console.log('[Player] Failover →', next.label, '(' + _triedUrls.size + '/' + all.length + ')');
// UI: aktif dub/kaynak butonunu işaretle
if (next.dubEl) {
document.querySelectorAll('.vmi-dub').forEach(e => e.classList.toggle('on', e === next.dubEl));
const lblEl = document.getElementById('dub-lbl');
if (lblEl && next.dubEl.dataset.lbl) lblEl.textContent = next.dubEl.dataset.lbl;
}
if (next.srcEl) {
document.querySelectorAll('.vmi-src,.src-tab').forEach(e => e.classList.toggle('on', e === next.srcEl));
}
// Önceki bufferAppendError'dan sonra MediaSource'un temizlenmesi için kısa gecikme.
// Gecikmesiz ardışık yükleme, çalışan kaynağı da bozuk state'te patlatıyor (elle
// tıklayınca araya zaman girdiği için çalışıyordu).
showVload();
setTimeout(function() {
if (next.type === 'mp4') {
_playMp4Direct(next.url, sk, function() { _failoverNext(sk); });
} else {
loadHLS(next.url, sk, function() { _failoverNext(sk); });
}
}, 500);
}
// Yeni bir oynatma başlat: denenen listesini sıfırla, ilk URL'yi işaretle, hata → failover
function _beginPlayback(url, type, sk) {
_triedUrls = new Set();
_bsRetryUrl = null; // siyah ekran sayacını sıfırla
if (url) _triedUrls.add(url);
if (type === 'mp4') {
_playMp4Direct(url, sk, () => _failoverNext(sk));
} else {
loadHLS(url, sk, () => _failoverNext(sk));
}
}
// MP4 doğrudan oynatma (HLS değil) — hata olursa onFail çağır
function _playMp4Direct(url, t, onFail) {
showVload();
vd.pause();
if (hls) { hls.destroy(); hls = null; }
vd.src = url;
const _err = () => { vd.removeEventListener('error', _err); if (onFail) onFail(); else showNoVideo(); };
vd.addEventListener('error', _err, { once: true });
const _play = () => vd.play().catch(() => { vd.muted = true; vd.play().catch(() => {}); });
if (t > 0) {
vd.addEventListener('loadedmetadata', function _sm() {
vd.removeEventListener('loadedmetadata', _sm);
vd.currentTime = t;
_play();
}, { once: true });
} else {
_play();
}
}
function _startM3u8(sk, srcOverride) {
// srcOverride: embed modda M3U8 null ama video_url HLS olabilir
// VIDEO_SOURCES PHP tarafında kaliteye göre sıralandı (1080p → 720p → 4K)
const _preferredVS = !srcOverride && (VIDEO_SOURCES || []).find(
s => s.source === 'anizium' && s.type === 'hls' && s.url
);
// İlk denenecek kaynağı seç: kaydedilmiş dub > original dub > tercihli kalite > M3U8
let firstUrl = null, firstDubEl = null;
const savedDub = PREF.dub;
if (savedDub) {
const dubEl = document.querySelector('.vmi-dub[data-key="' + savedDub + '"]');
if (dubEl && dubEl.dataset.url) { firstUrl = dubEl.dataset.url; firstDubEl = dubEl; }
}
if (!firstUrl) {
const jpEl = document.querySelector('.vmi-dub[data-key="original"]');
if (jpEl && jpEl.dataset.url) { firstUrl = jpEl.dataset.url; firstDubEl = jpEl; }
}
if (!firstUrl) firstUrl = srcOverride || (_preferredVS ? _preferredVS.url : M3U8);
if (!firstUrl) { showNoVideo(); return; }
// UI: aktif dub/kaynak sekmesini işaretle
if (firstDubEl) {
document.querySelectorAll('.vmi-dub').forEach(e => e.classList.toggle('on', e === firstDubEl));
const lblEl = document.getElementById('dub-lbl');
if (lblEl && firstDubEl.dataset.lbl) lblEl.textContent = firstDubEl.dataset.lbl;
} else if (_preferredVS) {
document.querySelectorAll('.vmi-src, .src-tab').forEach(e =>
e.classList.toggle('on', e.dataset.url === _preferredVS.url)
);
}
_beginPlayback(firstUrl, 'hls', sk);
}
// Kaydedilmiş dub tercihi varsa onu uygula; yoksa Japonca (original) önceliklidir.
function applyDubPref() {
const sk = _mainSkipSec || 0;
if (!M3U8 && !MP4) { showNoVideo(); return; }
// Embed modda anizium M3U8 URL'si video_url alanına kaydediliyor —
// native <video src> HLS okuyamaz (Safari hariç), HLS.js ile aç.
// Proxy URL'leri (/stream/proxy?u=...) de HLS olarak işlenir.
const mp4IsHls = MP4 && (MP4.includes('.m3u8') || MP4.startsWith('/stream/'));
if (IS_AUTH && MP4 && !mp4IsHls) {
_startMp4(sk); // Üye: CDN MP4 birincil, anizium yedek
} else if (M3U8 || mp4IsHls) {
_startM3u8(sk, mp4IsHls ? MP4 : null); // anizium HLS birincil
} else {
_startMp4(sk); // Sadece CDN MP4 mevcut
}
}
// Quality badge update
function updateQualityLabel() {
const qlbl = document.getElementById('qlbl');
if (!qlbl || !hls) return;
const lvl = hls.currentLevel;
if (lvl < 0) { qlbl.textContent = 'Auto'; return; }
const l = hls.levels[lvl];
qlbl.textContent = l ? (l.height || Math.round(l.width * 9/16)) + 'p' : 'Auto';
}
// Quality
function doQuality(q, el) {
document.querySelectorAll('#mnu-set .vmi[data-q]').forEach(e => e.classList.remove('on'));
el.classList.add('on');
if (hls && Hls.isSupported()) { hls.currentLevel = q; setTimeout(updateQualityLabel, 100); }
closeMenus();
}
// ─── Zoom / Fill ─────────────────────────────────────────────────────────────
let fillEnabled = false;
function toggleFill() {
fillEnabled = !fillEnabled;
const btn = document.getElementById('fill-toggle');
const row = document.getElementById('zoom-row');
btn.classList.toggle('on', fillEnabled);
row.classList.toggle('on', fillEnabled);
if (fillEnabled) {
applyZoom(document.getElementById('zoom-slider').value);
} else {
vd.style.objectFit = 'contain';
vd.style.transform = '';
document.getElementById('zoom-slider').value = 100;
document.getElementById('zoom-lbl').textContent = '100%';
}
}
function applyZoom(val) {
const pct = parseInt(val);
const scale = pct / 100;
document.getElementById('zoom-lbl').textContent = pct + '%';
vd.style.objectFit = 'cover';
// scale 1.0'dan büyükse ek büyütme (cover zaten sığdırır, scale daha da büyütür)
vd.style.transform = scale > 1 ? `scale(${scale})` : '';
}
// Muted chip (autoplay policy)
const vunmuteEl = document.getElementById('vunmute');
function showUnmute() {
if (!vunmuteEl) return;
vunmuteEl.style.display = 'block';
}
function hideUnmute() {
if (!vunmuteEl) return;
vunmuteEl.style.display = 'none';
}
function doUnmute() {
vd.muted = false;
vd.volume = Math.max(vd.volume || 0.8, 0.5);
hideUnmute();
updVol();
// Eğer video durduysa başlat
if (vd.paused && vd.readyState >= 2) vd.play().catch(() => {});
}
vd.addEventListener('volumechange', () => { updVol(); if (!vd.muted) hideUnmute(); });
// ─── Tercih hafızası (localStorage) ─────────────────────────────────────────
const PREF = {
get dub() { return localStorage.getItem('axe_dub'); },
set dub(v) { localStorage.setItem('axe_dub', v); },
get sub() { return localStorage.getItem('axe_sub'); }, // dil kodu veya 'off'
set sub(v) { localStorage.setItem('axe_sub', v); },
get fs() { return localStorage.getItem('axe_fs') === '1'; },
set fs(v) { localStorage.setItem('axe_fs', v ? '1' : '0'); },
get subStyle() { try { return JSON.parse(localStorage.getItem('axe_sub_style') || 'null') || {}; } catch { return {}; } },
set subStyle(v){ localStorage.setItem('axe_sub_style', JSON.stringify(v)); },
};
// PREF hazır — intro yoksa ana videoyu hemen başlat; varsa intro bitince startMainVideo() çağırır
function startPlayer() { _mainSkipSec = MAIN_SKIP_SEC; applyDubPref(); }
if (!INTRO_URL) {
if (window._imaAdPending) {
// IMA reklam yükleniyor — reklam bitince startPlayer() çağrılacak
} else {
startPlayer();
}
}
// Dub (hem mnu-dub hem ayarlar içi .vmi-dub senkron)
function doDub(el) {
const url = el.dataset.url, lbl = el.dataset.lbl, key = el.dataset.key, t = vd.currentTime;
document.querySelectorAll('.vmi-dub').forEach(e => e.classList.toggle('on', e.dataset.url === url));
const lblEl = document.getElementById('dub-lbl');
if (lblEl) lblEl.textContent = lbl;
if (key) PREF.dub = key;
// Bu dub ölürse evrensel failover diğer dub/kaynaklara geçer (siyah ekran yerine)
_beginPlayback(url, 'hls', t);
closeMenus();
}
// Çeviri kaynağı seçici (animecix çok-çevirmenli bölümler)
function doSource(el) {
const url = el.dataset.url, lbl = el.dataset.lbl, key = el.dataset.key;
const t = vd.currentTime;
document.querySelectorAll('.vmi-src, .src-tab').forEach(e => e.classList.toggle('on', e.dataset.key === key));
const lblEl = document.getElementById('src-lbl');
if (lblEl) lblEl.textContent = lbl;
// Seçilen kaynak ölürse evrensel failover devreye girer
_beginPlayback(url, el.dataset.type === 'mp4' ? 'mp4' : 'hls', t);
closeMenus();
}
// Speed
function doSpeed(s, el) {
vd.playbackRate = s;
document.querySelectorAll('[data-spd]').forEach(e => e.classList.remove('on'));
el.classList.add('on');
closeMenus();
// Yüksek hızda HLS buffer'ı zorla yeniden başlat — 2x'te mevcut buffer çabuk tükenir
if (hls && s > 1) {
hls.startLoad(vd.currentTime);
}
}
// ─── Subtitle engine ─────────────────────────────────────────────────────────
let cues = [], activeSub = @json(collect($subtitlesData)->search(fn($s)=>$s['is_default']));
const subEl = document.getElementById('vsub');
async function loadSub(idx) {
cues = []; subEl.innerHTML = ''; subEl.classList.remove('on');
if (idx < 0 || !SUBS[idx]) return;
const subUrl = SUBS[idx].url;
try {
const r = await fetch(subUrl);
if (!r.ok) {
console.warn('[Sub] HTTP', r.status, subUrl);
if (r.status >= 500) subEl.title = 'Altyazı sunucudan alınamadı';
return;
}
const txt = await r.text();
if (!txt.trim() || !txt.includes('-->')) {
console.warn('[Sub] VTT formatı geçersiz:', subUrl);
return;
}
cues = parseVTT(txt);
console.log('[Sub] Yüklendi:', SUBS[idx].lang, '—', cues.length, 'satır');
if (!cues.length) console.warn('[Sub] Parse boş sonuç:', subUrl);
if (activeSub === idx) renderSub(vd.currentTime || 0);
} catch(e) { console.warn('[Sub] Fetch hatası:', subUrl, e); }
}
function parseVTT(raw) {
const out = [];
// BOM ve baştaki boşlukları at, satır sonlarını normalize et
const normalized = raw.replace(/^/, '').replace(/\r\n?/g, '\n');
const blocks = normalized.split(/\n{2,}/);
for (const b of blocks) {
const ls = b.trim().split('\n');
const ti = ls.findIndex(l => l.includes('-->'));
if (ti < 0) continue;
const timeParts = ls[ti].split('-->');
if (timeParts.length < 2) continue;
const s = toSec(timeParts[0].trim());
const e = toSec(timeParts[1].trim().split(/\s/)[0]);
if (isNaN(s) || isNaN(e)) continue;
const txt = ls.slice(ti + 1).join('\n')
.replace(/<[^>]+>/g, '')
.replace(/\{[^}]+\}/g, '')
.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&nbsp;/g, ' ')
.trim();
if (txt) out.push({ s, e, t: txt });
}
return out;
}
function toSec(t) {
const p = t.replace(',','.').split(':');
return p.length === 3 ? +p[0]*3600 + +p[1]*60 + parseFloat(p[2]) : +p[0]*60 + parseFloat(p[1]);
}
function renderSub(time) {
// Aynı anda aktif tüm cue'ları topla (ikili konuşma desteği)
const active = cues.filter(c => time >= c.s && time <= c.e);
if (!active.length) { subEl.classList.remove('on'); subEl.innerHTML = ''; return; }
subEl.classList.add('on');
// Sadece içerik değiştiyse DOM'u güncelle (titreme önleme)
const newHtml = active.map((c, i) =>
`<span class="vsub-line${i > 0 ? ' vsub-b' : ''}">${c.t.replace(/\n/g,'<br>')}</span>`
).join('');
if (subEl.innerHTML !== newHtml) subEl.innerHTML = newHtml;
}
function doSub(idx, menuEl) {
activeSub = idx;
loadSub(idx);
// Tüm altyazı menülerindeki (mnu-set + mnu-sub) vmi'ları güncelle
document.querySelectorAll('.vmi[data-i]').forEach(el => {
const i = parseInt(el.dataset.i, 10);
el.classList.toggle('on', i === idx);
});
// Tercihi kaydet — dil kodu veya 'off'
PREF.sub = idx >= 0 && SUBS[idx] ? SUBS[idx].lang : 'off';
closeMenus();
}
// Altyazı tercihini uygula: önce kayıtlı dil, sonra varsayılan, sonra kapalı
(function applySubPref() {
const savedLang = PREF.sub;
if (savedLang === 'off') {
// Kullanıcı "kapalı" seçmişti
activeSub = -1;
return;
}
if (savedLang) {
// Kayıtlı dil koduna göre subtitle bul
const idx = SUBS.findIndex(s => s.lang === savedLang);
if (idx >= 0) { activeSub = idx; loadSub(idx); return; }
}
// Kayıtlı tercih yoksa veya bulunamadıysa varsayılana dön
if (activeSub !== false && activeSub >= 0) loadSub(activeSub);
})();
// ─── Altyazı stil özelleştirme ────────────────────────────────────────────────
const _SUB_SIZES = {
small: 'clamp(.72rem,1.6vw,.95rem)',
medium: 'clamp(.88rem,2.2vw,1.2rem)',
large: 'clamp(1rem,2.8vw,1.5rem)',
xlarge: 'clamp(1.2rem,3.5vw,1.9rem)',
};
const _SUB_BGS = {
none: 'transparent',
semi: 'rgba(0,0,0,.55)',
full: 'rgba(0,0,0,.88)',
};
function applySubStyle(st) {
if (!subEl) return;
st = st || {};
const sz = _SUB_SIZES[st.size] || _SUB_SIZES.medium;
const clr = st.color || '#fff';
const bg = _SUB_BGS[st.bg] || 'transparent';
const hasBg = st.bg && st.bg !== 'none';
const shadow = hasBg ? 'none' : '0 0 4px #000,0 1px 6px #000,1px 1px 0 #000,-1px 1px 0 #000,1px -1px 0 #000,-1px -1px 0 #000';
const pad = hasBg ? '1px 6px 2px' : '0 4px';
subEl.style.setProperty('--sub-size', sz);
subEl.style.setProperty('--sub-color', clr);
subEl.style.setProperty('--sub-bg', bg);
subEl.style.setProperty('--sub-shadow', shadow);
subEl.style.setProperty('--sub-pad', pad);
// İkinci konuşmacı — arka plan varsa aynı rengi hafif opak yap, yoksa klasik krem ton
subEl.style.setProperty('--sub-color-b', hasBg ? clr : 'rgba(255,255,200,0.92)');
}
function setSubSize(sz, btn) {
document.querySelectorAll('.sub-sz-btn').forEach(b => b.classList.remove('on'));
if (btn) btn.classList.add('on');
const st = PREF.subStyle; st.size = sz; PREF.subStyle = st;
applySubStyle(st);
}
function setSubColor(clr, btn) {
document.querySelectorAll('.sub-clr-btn').forEach(b => b.classList.remove('on'));
if (btn) btn.classList.add('on');
const st = PREF.subStyle; st.color = clr; PREF.subStyle = st;
applySubStyle(st);
}
function setSubBg(bg, btn) {
document.querySelectorAll('.sub-bg-btn').forEach(b => b.classList.remove('on'));
if (btn) btn.classList.add('on');
const st = PREF.subStyle; st.bg = bg; PREF.subStyle = st;
applySubStyle(st);
}
// Kayıtlı tercihleri uygula + butonlarda aktif işareti koy
(function initSubStyle() {
const st = PREF.subStyle;
applySubStyle(st);
const defSz = st.size || 'medium';
const defClr = st.color || '#ffffff';
const defBg = st.bg || 'none';
document.querySelector(`.sub-sz-btn[data-sz="${defSz}"]`)?.classList.add('on');
document.querySelector(`.sub-clr-btn[data-clr="${defClr}"]`)?.classList.add('on');
document.querySelector(`.sub-bg-btn[data-bg="${defBg}"]`)?.classList.add('on');
})();
// ─── Controls ─────────────────────────────────────────────────────────────────
const vc = document.getElementById('vc');
const vmBackdrop = document.getElementById('vm-backdrop');
const vwEl = document.getElementById('vw');
let hideT = null, dragging = false;
vwEl.classList.add('ctrls-off');
/** Mobil / dokunmatik: boş alana tıklayınca kontroller aç-kapa. Sadece (pointer:coarse) yetmez; emülatör ve birçok tablet fine pointer raporlar. */
function useChromeToggle() {
if (typeof window.matchMedia !== 'function') {
return 'ontouchstart' in window || ((navigator.maxTouchPoints || 0) > 0);
}
if (window.matchMedia('(hover: none)').matches) return true;
if (window.matchMedia('(max-width: 768px)').matches) return true;
if ((navigator.maxTouchPoints || 0) > 0 && window.matchMedia('(pointer: coarse)').matches) return true;
return false;
}
const ctrlHideMs = () => (useChromeToggle() ? 4000 : 3200);
function showCtrls() {
vc.classList.add('on');
vwEl.classList.remove('ctrls-off');
document.getElementById('nico-input-row')?.classList.remove('ctrls-hidden');
clearTimeout(hideT);
hideT = setTimeout(() => {
if (dragging || document.querySelector('.vmenu.on')) return;
hideCtrls();
}, ctrlHideMs());
}
function hideCtrls() {
vc.classList.remove('on');
vwEl.classList.add('ctrls-off');
document.getElementById('nico-input-row')?.classList.add('ctrls-hidden');
clearTimeout(hideT);
}
vwEl.addEventListener('mousemove', () => { if (!useChromeToggle()) showCtrls(); });
vwEl.addEventListener('touchstart', () => { if (!useChromeToggle()) showCtrls(); }, { passive: true });
vwEl.addEventListener('mouseleave', () => {
if (useChromeToggle()) return;
if (document.querySelector('.vmenu.on')) return;
hideCtrls();
});
/* Bazı tarayıcılar aynı dokunuşta iki click üretebilir */
let chromeTapLock = 0;
function applyChromeTapToggle() {
if (document.querySelector('.vmenu.on')) {
closeMenus();
return;
}
if (vc.classList.contains('on')) hideCtrls();
else showCtrls();
}
vwEl.addEventListener('click', e => {
if (e.target.closest('.cb,.vmenu,.vnx,.vp-wrap,.vdrop,#vm-backdrop,.vcb,#vm-mobile-backdrop')) return;
if (!useChromeToggle()) {
showCtrls();
return;
}
const now = Date.now();
if (now - chromeTapLock < 450) return;
chromeTapLock = now;
applyChromeTapToggle();
});
function doPlay() {
if (window._imaAdPending) return;
if (!vd.src && !vd.currentSrc) { applyDubPref(); return; }
if (vd.paused) vd.play().catch(() => { vd.muted = true; vd.play().then(showUnmute).catch(() => {}); });
else vd.pause();
}
vd.addEventListener('play', () => { document.getElementById('cb-play-i').className = 'bi bi-pause-fill'; });
/* pause: ikon güncelle + kontrolleri göster; zamanlayıcıyı SİLME (showCtrls kendi kuruyordu, clear hemen ardından kapatmayı iptal ediyordu) */
vd.addEventListener('pause', () => {
document.getElementById('cb-play-i').className = 'bi bi-play-fill';
showCtrls();
});
// Seek
function seek(s) {
vd.currentTime = Math.max(MAIN_SKIP_SEC, Math.min(vd.duration || 0, vd.currentTime + s));
flash(s < 0 ? 'l' : 'r', s);
}
function flash(side, secs) {
const el = document.getElementById('rf-' + side);
if (!el) return;
const lbl = el.querySelector('.vrf-sec');
if (lbl) lbl.textContent = Math.abs(secs || 10) + ' saniye';
el.classList.remove('go'); void el.offsetWidth; el.classList.add('go');
}
// Double tap (mobile)
let lastTap = 0;
document.getElementById('vw').addEventListener('touchend', e => {
const now = Date.now();
const vwRect = document.getElementById('vw').getBoundingClientRect();
const x = e.changedTouches[0].clientX - vwRect.left;
if (now - lastTap < 280) { e.preventDefault(); seek(x < vwRect.width / 2 ? -10 : 10); }
lastTap = now;
}, { passive: false });
// Double click (desktop)
vwEl.addEventListener('dblclick', e => {
if (e.target.closest('.cb,.vmenu,.vnx,.vp-wrap,.vdrop,#vm-backdrop,#aniskip-btn,#skip-seg-btn')) return;
const x = e.clientX - vwEl.getBoundingClientRect().left;
seek(x < vwEl.offsetWidth / 2 ? -10 : 10);
});
// Hold to 2x speed (mobile long-press + desktop mousedown hold)
let _holdTimer = null, _holding2x = false, _prevRate = 1;
const _holdDelay = 400;
const _holdInd = document.getElementById('hold-ind');
function _startHold() {
clearTimeout(_holdTimer);
_holdTimer = setTimeout(() => {
_prevRate = vd.playbackRate;
vd.playbackRate = 2;
_holding2x = true;
if (_holdInd) _holdInd.style.display = 'block';
}, _holdDelay);
}
function _endHold() {
clearTimeout(_holdTimer);
_holdTimer = null;
if (_holding2x) {
vd.playbackRate = _prevRate;
_holding2x = false;
if (_holdInd) _holdInd.style.display = 'none';
}
}
vwEl.addEventListener('touchstart', e => {
if (e.target.closest('.cb,.vmenu,.vnx,.vp-wrap,.vdrop,#aniskip-btn,#skip-seg-btn')) return;
_startHold();
}, { passive: true });
vwEl.addEventListener('touchend', _endHold);
vwEl.addEventListener('touchcancel', _endHold);
vwEl.addEventListener('touchmove', _endHold, { passive: true });
// Tarayıcının "Video İndir / Kaydet" context menüsünü engelle
vwEl.addEventListener('contextmenu', e => e.preventDefault());
vd.addEventListener('contextmenu', e => e.preventDefault());
vwEl.addEventListener('mousedown', e => {
if (e.button !== 0 || e.target.closest('.cb,.vmenu,.vnx,.vp-wrap,.vdrop,#aniskip-btn,#skip-seg-btn')) return;
_startHold();
});
vwEl.addEventListener('mouseup', _endHold);
vwEl.addEventListener('mouseleave', _endHold);
// Next-ep link tıklandığında fullscreen flag'i aktar
if (NXT_URL) {
document.addEventListener('click', e => {
if (!(e.target instanceof Element)) return;
const a = e.target.closest('a');
if (!a) return;
const going = a.href === NXT_URL || a.closest('#vnx') || a.classList.contains('pnav-next');
if (going && (document.fullscreenElement || document.webkitFullscreenElement)) {
sessionStorage.setItem('axe_auto_fs', '1');
}
});
}
// Progress
const vpbar = document.getElementById('vpbar');
const vpfill = document.getElementById('vpfill');
const vpbuf = document.getElementById('vpbuf');
const vptip = document.getElementById('vptip');
const vtimeEl = document.getElementById('vtime');
const _wmCover = document.getElementById('wm-cover');
// Mid-roll watermark: her 5 dakikada bir 8 saniyeliğine göster (5:04-5:12, 10:04-10:12 ...)
const WM_MIDROLL_INTERVAL = 300; // saniye (5 dakika)
const WM_MIDROLL_OFFSET = 4; // interval başından kaç saniye sonra açılsın
const WM_MIDROLL_DURATION = 8; // kaç saniye görünsün
function _wmShouldShow(t) {
if (!_wmCover) return false;
// Başlangıç kapağı (ilk N saniye)
if (WM_COVER_SECS > 0 && t < MAIN_SKIP_SEC + WM_COVER_SECS) return true;
// Mid-roll aralıkları
const mark = Math.floor(t / WM_MIDROLL_INTERVAL);
if (mark < 1) return false;
const windowStart = mark * WM_MIDROLL_INTERVAL + WM_MIDROLL_OFFSET;
const windowEnd = windowStart + WM_MIDROLL_DURATION;
return t >= windowStart && t < windowEnd;
}
function upProgress() {
if (!vd.duration) return;
// Yasaklı bölge: rakip intro — asla izletme
if (MAIN_SKIP_SEC > 0 && vd.currentTime < MAIN_SKIP_SEC) {
vd.currentTime = MAIN_SKIP_SEC;
return;
}
// Watermark cover: başlangıç + mid-roll
if (_wmCover) {
const shouldShow = _wmShouldShow(vd.currentTime);
if (shouldShow && _wmCover.classList.contains('hide')) {
_wmCover.style.display = 'flex';
_wmCover.classList.remove('hide');
} else if (shouldShow && _wmCover.style.display === 'none') {
_wmCover.style.display = 'flex';
_wmCover.classList.remove('hide');
} else if (!shouldShow && !_wmCover.classList.contains('hide')) {
_wmCover.classList.add('hide');
setTimeout(() => { if(_wmCover && _wmCover.classList.contains('hide')) _wmCover.style.display = 'none'; }, 650);
}
}
if (dragging) return;
const p = vd.currentTime / vd.duration;
vpfill.style.width = (p * 100) + '%';
if (vd.buffered.length) vpbuf.style.width = (vd.buffered.end(vd.buffered.length-1)/vd.duration*100) + '%';
vtimeEl.textContent = fmt(vd.currentTime) + ' / ' + fmt(vd.duration);
renderSub(vd.currentTime);
if (HAS_NXT && vd.duration && vd.duration - vd.currentTime <= 30) triggerNext();
}
vd.addEventListener('timeupdate', upProgress);
// ── Skip Segments ────────────────────────────────────────────────────────────
const EPISODE_ID_FOR_SKIP = {{ $ep->id }};
let skipSegments = [];
let activeSegTo = null;
let seekFromSec = null; // track seek start for recording skip events
// Fetch segments — intro tespiti (1 kişi bile ileri sardıysa yeter)
fetch(`/api/episodes/${EPISODE_ID_FOR_SKIP}/skip-segments`)
.then(r => r.json())
.then(d => {
skipSegments = d.segments || [];
// skip-seg verisini ANISKIP formatına çevir (öncelik: manuel/aniskip > skip-seg)
if (!ANISKIP && skipSegments.length) {
const s = skipSegments[0];
ANISKIP = { op: { start: s.from, end: s.to } };
}
renderSkipMarkers();
}).catch(() => {});
function renderSkipMarkers() {
document.querySelectorAll('.vp-skip-mark').forEach(el => el.remove());
if (!vd.duration || !skipSegments.length) return;
skipSegments.forEach(seg => {
const pct = (seg.from / vd.duration) * 100;
const mark = document.createElement('div');
mark.className = 'vp-skip-mark';
mark.style.left = pct + '%';
mark.title = 'İntro bölgesi';
document.getElementById('vpbar').appendChild(mark);
});
}
vd.addEventListener('loadedmetadata', renderSkipMarkers);
function checkSkipSegment(t) { /* skip-seg artık ANISKIP'e merge edildi */ }
function skipToSegmentEnd() {}
// ── AniSkip ──────────────────────────────────────────────────────────────────
let aniSkipActiveType = null; // 'op' | 'ed' | null
// Fetch AniSkip async if blade didn't supply it (first-time visit for this anime)
if (!ANISKIP) {
fetch('/api/aniskip/{{ $anime->slug }}/{{ $seasonModel->season_number }}/{{ $ep->episode_number }}')
.then(r => r.json())
.then(d => { if (d.aniskip) ANISKIP = d.aniskip; })
.catch(() => {});
}
function checkAniSkip(t) {
const btn = document.getElementById('aniskip-btn');
if (!btn || !ANISKIP) return;
let type = null;
if (ANISKIP.op && t >= ANISKIP.op.start && t < ANISKIP.op.end) type = 'op';
else if (ANISKIP.ed && t >= ANISKIP.ed.start && t < ANISKIP.ed.end) type = 'ed';
if (type !== aniSkipActiveType) {
aniSkipActiveType = type;
if (type) {
if (type === 'ed' && HAS_NXT) {
triggerNext();
btn.classList.remove('visible');
} else {
const lbl = document.getElementById('aniskip-label');
if (lbl) lbl.textContent = type === 'op' ? 'İntroyu Atla' : 'Outroyu Atla';
btn.classList.add('visible');
}
} else {
btn.classList.remove('visible');
}
}
}
function skipAniSkip() {
if (!aniSkipActiveType || !ANISKIP) return;
const end = ANISKIP[aniSkipActiveType]?.end;
if (end != null) vd.currentTime = end;
aniSkipActiveType = null;
document.getElementById('aniskip-btn')?.classList.remove('visible');
}
// Hook into timeupdate
const _origUpProgress = upProgress;
vd.removeEventListener('timeupdate', upProgress);
vd.addEventListener('timeupdate', () => {
_origUpProgress();
checkSkipSegment(vd.currentTime);
checkAniSkip(vd.currentTime);
_captureFrame();
});
// Record skip events when user seeks forward >= 5s
// ── Seek thumbnail frame cache ────────────────────────────────────────────────
const _thumbCache = {}, _thumbImgs = {}, _THUMB_INT = 15;
const _vptipTime = document.getElementById('vptip-time');
const _vptipThumb = document.getElementById('vptip-thumb');
const _vptipCtx = _vptipThumb?.getContext('2d');
let _pendingPct = null; // seek position while dragging (not applied yet)
function _captureFrame() {
if (vd.readyState < 2 || dragging) return;
const bucket = Math.floor(vd.currentTime / _THUMB_INT);
if (_thumbCache[bucket]) return;
try {
const tc = document.createElement('canvas');
tc.width = 160; tc.height = 90;
tc.getContext('2d').drawImage(vd, 0, 0, 160, 90);
_thumbCache[bucket] = tc.toDataURL('image/jpeg', 0.6);
} catch(_) {}
}
function _showTip(pct) {
const t = (vd.duration || 0) * pct;
if (_vptipTime) _vptipTime.textContent = fmt(t);
vptip.style.left = (pct * 100) + '%';
const hasThumbs = Object.keys(_thumbCache).length > 0;
if (_vptipThumb) _vptipThumb.style.display = hasThumbs ? 'block' : 'none';
if (!_vptipCtx || !hasThumbs) return;
const bucket = Math.floor(t / _THUMB_INT);
const keys = Object.keys(_thumbCache).map(Number);
const nearest = keys.reduce((a, b) => Math.abs(b - bucket) < Math.abs(a - bucket) ? b : a);
if (!_thumbImgs[nearest]) {
_thumbImgs[nearest] = new Image();
_thumbImgs[nearest].src = _thumbCache[nearest];
}
const img = _thumbImgs[nearest];
if (img.complete) _vptipCtx.drawImage(img, 0, 0, 160, 90);
else img.onload = () => _vptipCtx.drawImage(img, 0, 0, 160, 90);
}
function _hideTip() {
vptip.classList.remove('force-show');
}
function _applyPendingSeek(fromSec) {
if (_pendingPct === null) return;
const toSec = clampSeek((vd.duration || 0) * _pendingPct);
vd.currentTime = toSec;
// Record skip event
if (fromSec !== null && toSec - fromSec >= 5) {
fetch(`/api/episodes/${EPISODE_ID_FOR_SKIP}/skip-event`, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content || ''},
body: JSON.stringify({from_sec: Math.round(fromSec), to_sec: Math.round(toSec)})
}).catch(() => {});
}
_pendingPct = null;
}
// Hover: just show tip (no seek)
vpbar.addEventListener('mousemove', e => {
const r = vpbar.getBoundingClientRect();
const p = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
if (!dragging) _showTip(p);
});
vpbar.addEventListener('mouseleave', () => { if (!dragging) _hideTip(); });
function clampSeek(t) { return Math.max(MAIN_SKIP_SEC, Math.min(vd.duration || 0, t)); }
// Drag: update visual + tip, seek only on release
function _seekVisual(pct) {
_pendingPct = pct;
vpfill.style.width = (pct * 100) + '%';
vtimeEl.textContent = fmt(clampSeek((vd.duration || 0) * pct)) + ' / ' + fmt(vd.duration);
vptip.classList.add('force-show');
_showTip(pct);
}
function _pctFromMouse(e) { const r = vpbar.getBoundingClientRect(); return Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)); }
function _pctFromTouch(e) { const r = vpbar.getBoundingClientRect(); return Math.max(0, Math.min(1, (e.touches[0].clientX - r.left) / r.width)); }
vpbar.addEventListener('mousedown', e => {
dragging = true;
seekFromSec = vd.currentTime;
_seekVisual(_pctFromMouse(e));
const up = () => {
dragging = false;
_hideTip();
_applyPendingSeek(seekFromSec);
seekFromSec = null;
document.removeEventListener('mousemove', mv);
document.removeEventListener('mouseup', up);
};
const mv = ev => _seekVisual(_pctFromMouse(ev));
document.addEventListener('mousemove', mv);
document.addEventListener('mouseup', up);
});
vpbar.addEventListener('touchstart', e => {
dragging = true;
seekFromSec = vd.currentTime;
_seekVisual(_pctFromTouch(e));
const up = () => {
dragging = false;
_hideTip();
_applyPendingSeek(seekFromSec);
seekFromSec = null;
document.removeEventListener('touchmove', mv);
document.removeEventListener('touchend', up);
};
const mv = ev => _seekVisual(_pctFromTouch(ev));
document.addEventListener('touchmove', mv, { passive: true });
document.addEventListener('touchend', up);
}, { passive: true });
// Volume
function doVol(v) { vd.volume=parseFloat(v); vd.muted=(v==0); updVol(); document.getElementById('cb-vvr').style.setProperty('--v',(v*100)+'%'); }
function doMute() { vd.muted=!vd.muted; if(!vd.muted&&vd.volume===0) vd.volume=1; updVol(); }
function updVol() {
const lv = vd.muted ? 0 : vd.volume, vi = document.getElementById('cb-vol-i');
vi.className = lv===0?'bi bi-volume-mute-fill':lv<.5?'bi bi-volume-down-fill':'bi bi-volume-up-fill';
document.getElementById('cb-vvr').style.setProperty('--v',(lv*100)+'%');
}
// Fullscreen
// Yatay kilit + çıkış için orientation API yardımcıları
function _lockLandscape() {
try {
const so = screen.orientation || screen.msOrientation;
if (so && so.lock) {
so.lock('landscape').catch(() => {});
} else if (screen.lockOrientation) {
screen.lockOrientation('landscape');
}
} catch(e) {}
}
function _unlockOrientation() {
try {
const so = screen.orientation || screen.msOrientation;
if (so && so.unlock) so.unlock();
else if (screen.unlockOrientation) screen.unlockOrientation();
} catch(e) {}
}
function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}
function doFS() {
const fsi = document.getElementById('cb-fs-i');
const wrap = document.getElementById('vw');
if (isIOS()) {
const inFS = wrap.classList.contains('ios-fullscreen');
if (!inFS) {
wrap.classList.add('ios-fullscreen');
document.body.classList.add('ios-fs-active');
fsi.className = 'bi bi-fullscreen-exit';
_lockLandscape();
} else {
wrap.classList.remove('ios-fullscreen');
document.body.classList.remove('ios-fs-active');
fsi.className = 'bi bi-fullscreen';
_unlockOrientation();
}
return;
}
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
const fsReq = wrap.requestFullscreen || wrap.webkitRequestFullscreen;
if (fsReq) {
fsReq.call(wrap).catch(() => {});
}
fsi.className = 'bi bi-fullscreen-exit';
_lockLandscape();
} else {
(document.exitFullscreen || document.webkitExitFullscreen)?.call(document);
fsi.className = 'bi bi-fullscreen';
_unlockOrientation();
}
}
document.addEventListener('webkitfullscreenchange', () => {
const inFS = !!(document.fullscreenElement || document.webkitFullscreenElement);
document.getElementById('cb-fs-i').className = inFS ? 'bi bi-fullscreen-exit' : 'bi bi-fullscreen';
PREF.fs = inFS;
if (!inFS) _unlockOrientation();
});
document.addEventListener('fullscreenchange', () => {
const inFS = !!document.fullscreenElement;
document.getElementById('cb-fs-i').className = inFS ? 'bi bi-fullscreen-exit' : 'bi bi-fullscreen';
PREF.fs = inFS;
if (!inFS) _unlockOrientation();
});
// Dikey konuma dönünce fullscreen'den otomatik çık
if (screen.orientation) {
screen.orientation.addEventListener('change', () => {
const type = screen.orientation.type;
if (type === 'portrait-primary' || type === 'portrait-secondary') {
if (document.fullscreenElement || document.webkitFullscreenElement) {
(document.exitFullscreen || document.webkitExitFullscreen)?.call(document);
}
const wrap = document.getElementById('vw');
if (wrap && wrap.classList.contains('ios-fullscreen')) {
wrap.classList.remove('ios-fullscreen');
document.body.classList.remove('ios-fs-active');
document.getElementById('cb-fs-i').className = 'bi bi-fullscreen';
_unlockOrientation();
}
}
});
}
// Menus
const vmMobileBackdrop = document.getElementById('vm-mobile-backdrop');
// Backdrop'ı vwEl'e taşı — fullscreen'de body'deki elemanlar görünmez, #vw içinde kalmalı
if (vmMobileBackdrop) vwEl.appendChild(vmMobileBackdrop);
const _isMobileMenu = () => window.matchMedia('(max-width:768px)').matches;
function openMenu(id) {
const m = document.getElementById(id), was = m.classList.contains('on');
closeMenus();
if (!was) {
const dropEl = m.closest('.vdrop');
m.classList.add('on');
if (_isMobileMenu()) {
// Vmenu'yü taşı — fullscreen'de vwEl'e, normalde body'e (#vw overflow:hidden'dan kaçmak için)
if (!m._origParent) {
m._origParent = m.parentElement;
m._origDrop = dropEl;
const fsEl = document.fullscreenElement || document.webkitFullscreenElement;
(fsEl || document.body).appendChild(m);
}
} else {
dropEl?.classList.add('drop-open');
}
if (vmMobileBackdrop) vmMobileBackdrop.classList.add('on');
vmBackdrop?.classList.add('on');
vc.classList.add('on');
clearTimeout(hideT);
}
}
function closeMenus() {
const hadOpen = !!document.querySelector('.vmenu.on');
document.querySelectorAll('.vmenu.on').forEach(m => {
m.classList.remove('on');
if (m._origParent) {
m._origParent.appendChild(m);
m._origDrop?.classList.remove('drop-open');
delete m._origParent;
delete m._origDrop;
}
});
document.querySelectorAll('.vdrop.drop-open').forEach(d => d.classList.remove('drop-open'));
vmBackdrop?.classList.remove('on');
vmMobileBackdrop?.classList.remove('on');
if (hadOpen) showCtrls();
}
document.addEventListener('click', e => {
if (!(e.target instanceof Element)) return;
if (e.target.closest('.vdrop') || e.target.closest('#vm-backdrop') || e.target.closest('.vmenu')) return;
closeMenus();
});
// Buffer spinner (mid-play) + loading screen (ilk yükleme)
// Stall recovery: 5s waiting → loader restart; 8s more → full HLS reload
let _stallTimer = null, _stallAttempt = 0;
const _vsMsg = document.getElementById('vs-msg');
function _clearStall() {
clearTimeout(_stallTimer); _stallTimer = null;
if (_vsMsg) { _vsMsg.textContent = ''; _vsMsg.classList.remove('on'); }
}
function _tryStallRecover() {
if (!hls || vd.paused) { _clearStall(); return; }
_stallAttempt++;
const ct = vd.currentTime;
if (_stallAttempt === 1) {
// İlk deneme: loading screen göster, HLS loader'ı mevcut pozisyondan yeniden başlat
showVload();
hls.startLoad(ct);
if (_vsMsg) { _vsMsg.textContent = 'Yeniden bağlanıyor...'; _vsMsg.classList.add('on'); }
_stallTimer = setTimeout(_tryStallRecover, 8000);
} else {
// İkinci deneme: HLS instance'ı yeniden oluştur (aynı URL, aynı pozisyon).
// Bu da başarısız olursa evrensel failover ile sıradaki kaynağa geç.
_stallAttempt = 0;
const savedUrl = hls.url;
hls.destroy(); hls = null;
if (_vsMsg) { _vsMsg.textContent = 'Akış yeniden başlatılıyor...'; }
loadHLS(savedUrl, ct, () => _failoverNext(ct));
}
}
vd.addEventListener('waiting', () => {
document.getElementById('vs').classList.add('on');
if (hls && !vd.paused && !_stallTimer) {
_stallAttempt = 0;
_stallTimer = setTimeout(_tryStallRecover, 5000);
}
});
// Seeking: hemen spinner göster, yeni pozisyon için loading zorla
let _seekStallTimer = null;
vd.addEventListener('seeking', () => {
clearTimeout(_seekStallTimer);
clearTimeout(_blackScreenTimer); _blackScreenTimer = null; // intro-skip / geri-sar false positive önle
document.getElementById('vs').classList.add('on');
});
vd.addEventListener('seeked', () => {
clearTimeout(_seekStallTimer);
// HLS yeni pozisyon için buffer doldurmamışsa yükleyi zorla
if (hls && vd.readyState < 3) {
hls.startLoad(vd.currentTime);
}
// 3s sonra hâlâ readyState < 3 → stall recovery tetikle
_seekStallTimer = setTimeout(() => {
if (!vd.paused && vd.readyState < 3 && hls) {
_stallAttempt = 0;
_tryStallRecover();
}
}, 3000);
});
// ratechange: yüksek hızda HLS buffer'ı yeniden başlat
vd.addEventListener('ratechange', () => {
if (!hls || vd.paused) return;
if (vd.playbackRate > 1) {
hls.startLoad(vd.currentTime);
}
});
let _blackScreenTimer = null;
vd.addEventListener('playing', () => {
_clearStall(); _stallAttempt = 0;
clearTimeout(_seekStallTimer);
document.getElementById('vs').classList.remove('on');
hideVload();
setTimeout(_captureFrame, 500);
// Siyah ekran tespiti: 4s içinde currentTime ilerlemiyor → takılı kalmış
clearTimeout(_blackScreenTimer);
const _bsCheckTime = vd.currentTime;
const _bsRate = vd.playbackRate || 1;
_blackScreenTimer = setTimeout(() => {
if (vd.paused) return;
const q = vd.getVideoPlaybackQuality?.();
const noFrames = q && q.totalVideoFrames === 0;
// currentTime en az (rate * 1.5) saniye ilerlemiş olmalı
const notAdvancing = vd.currentTime < _bsCheckTime + (_bsRate * 1.5);
if (noFrames || notAdvancing) {
console.warn('[Player] Siyah ekran / takılı:', { frames: q?.totalVideoFrames, timeDiff: vd.currentTime - _bsCheckTime, rate: _bsRate });
const retryUrl = (hls && hls.url) || vd.currentSrc;
const retryTime = Math.max(0, vd.currentTime - 0.5);
if (retryUrl && hls) {
// Aynı URL'yi bir kez yeniden dene; yine siyah kalırsa failover ile
// sıradaki kaynağa geç (HEVC / bozuk codec siyah ekranını kırar)
if (_bsRetryUrl === retryUrl) {
console.warn('[Player] Siyah ekran tekrarladı → failover');
_failoverNext(retryTime);
} else {
_bsRetryUrl = retryUrl;
loadHLS(retryUrl, retryTime, () => _failoverNext(retryTime));
}
} else {
_showBlackScreenHelp();
}
}
}, 4000);
});
function _showBlackScreenHelp() {
showToast(
'Video görüntüsü gelmiyor. Tarayıcı donanım hızlandırmasını kapatın veya farklı kaynak deneyin.',
10000
);
const nextSrc = document.querySelector('.src-tab:not(.on)[data-url]');
if (nextSrc) nextSrc.click();
}
vd.addEventListener('canplay', () => {
_clearStall();
document.getElementById('vs').classList.remove('on');
});
// Next episode
let nxtStarted = false, nxtT = null, nxtCancelled = false;
function cancelNext() {
clearInterval(nxtT);
nxtStarted = false;
nxtCancelled = true;
const vnx = document.getElementById('vnx');
if (vnx) vnx.classList.remove('on');
const f = document.getElementById('vnx-fill');
if (f) f.style.width = '0%';
}
function triggerNext() {
if (!HAS_NXT || nxtStarted || nxtCancelled) return;
nxtStarted = true;
document.getElementById('vnx')?.classList.add('on');
let pct = 0;
nxtT = setInterval(() => {
pct += 10;
const f = document.getElementById('vnx-fill');
if (f) f.style.width = pct + '%';
if (pct >= 100) { clearInterval(nxtT); if (document.fullscreenElement || document.webkitFullscreenElement) sessionStorage.setItem('axe_auto_fs','1'); window.location.href = NXT_URL; }
}, 500);
}
vd.addEventListener('pause', () => {
clearInterval(nxtT); nxtStarted = false;
document.getElementById('vnx')?.classList.remove('on');
});
// Keyboard
document.addEventListener('keydown', e => {
if (['INPUT','TEXTAREA'].includes(e.target.tagName)) return;
switch(e.code) {
case 'Space': case 'KeyK': e.preventDefault(); doPlay(); break;
case 'ArrowLeft': e.preventDefault(); seek(-5); break;
case 'ArrowRight': e.preventDefault(); seek(5); break;
case 'KeyJ': seek(-10); break;
case 'KeyL': seek(10); break;
case 'ArrowUp': e.preventDefault(); doVol(Math.min(1, vd.volume + 0.1)); break;
case 'ArrowDown': e.preventDefault(); doVol(Math.max(0, vd.volume - 0.1)); break;
case 'KeyM': doMute(); break;
case 'KeyF': doFS(); break;
case 'Slash': case 'KeyH': if (e.shiftKey || e.code === 'Slash') { e.preventDefault(); toggleKbHelp(); } break;
}
});
// Keyboard shortcut help modal
function toggleKbHelp() {
let modal = document.getElementById('kb-help-modal');
if (modal) { modal.remove(); return; }
modal = document.createElement('div');
modal.id = 'kb-help-modal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.82);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(10px)';
modal.innerHTML = `
<div style="background:#0e0e1a;border:1px solid rgba(255,255,255,.14);border-radius:20px;width:100%;max-width:440px;overflow:hidden">
<div style="padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.07);display:flex;justify-content:space-between;align-items:center">
<span style="font-size:1rem;font-weight:800;color:#fff"><i class="bi bi-keyboard" style="color:var(--accent2)"></i> Klavye Kısayolları</span>
<button onclick="document.getElementById('kb-help-modal')?.remove()" style="background:none;border:none;color:rgba(255,255,255,.4);font-size:1.1rem;cursor:pointer"><i class="bi bi-x-lg"></i></button>
</div>
<div style="padding:18px 20px;display:grid;grid-template-columns:1fr 1fr;gap:8px">
${[
['Space / K', 'Oynat / Durdur'],
['J', '← 10 saniye'],
['L', '→ 10 saniye'],
['← →', '← → 5 saniye'],
['↑ ↓', 'Ses Aç / Kıs'],
['M', 'Sessiz'],
['F', 'Tam Ekran'],
['?', 'Bu yardım'],
].map(([k,v])=>`
<div style="display:flex;align-items:center;gap:8px;padding:8px;background:rgba(255,255,255,.03);border-radius:8px;border:1px solid rgba(255,255,255,.06)">
<kbd style="background:rgba(0,245,255,.1);border:1px solid rgba(0,245,255,.25);color:var(--accent2);border-radius:6px;padding:3px 8px;font-size:.75rem;font-weight:800;font-family:monospace;white-space:nowrap">${k}</kbd>
<span style="font-size:.78rem;color:rgba(255,255,255,.65)">${v}</span>
</div>`).join('')}
</div>
<div style="padding:10px 20px 14px;text-align:center;color:rgba(255,255,255,.25);font-size:.72rem">Herhangi bir yere tıkla veya ? tuşuna bas</div>
</div>`;
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
}
// Sidebar season switch
function swS(n, btn) {
document.querySelectorAll('[id^="psl"]').forEach(el => el.style.display = 'none');
document.querySelectorAll('.pst').forEach(b => b.classList.remove('on'));
document.getElementById('psl' + n).style.display = 'block';
btn.classList.add('on');
}
// Sayfa yüklenince: sidebar scroll + fullscreen tercihi
window.addEventListener('load', () => {
// Sidebar: mevcut bölüme scroll (sayfa scroll değil)
const nowEp = document.getElementById('now-ep');
if (nowEp) {
const list = nowEp.closest('.ps-list');
if (list) {
const targetTop = nowEp.offsetTop - list.offsetTop;
list.scrollTop = Math.max(0, targetTop - list.clientHeight / 2 + nowEp.clientHeight / 2);
}
}
// Fullscreen tercihi: önceki tercih (PREF.fs) veya next-ep geçişinden gelen tek seferlik flag
const _autoFs = sessionStorage.getItem('axe_auto_fs') === '1';
if (_autoFs) sessionStorage.removeItem('axe_auto_fs');
if (PREF.fs || _autoFs) {
setTimeout(() => {
document.getElementById('vw')?.requestFullscreen?.().catch(() => { PREF.fs = false; });
}, 800);
}
});
// Helpers
function fmt(s) {
if (!s || isNaN(s)) return '0:00';
const h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sc=Math.floor(s%60);
return h>0 ? `${h}:${pad(m)}:${pad(sc)}` : `${m}:${pad(sc)}`;
}
function pad(n) { return String(n).padStart(2,'0'); }
// ─── COMMENT SYSTEM ───────────────────────────────────────────────────────────
const CMT_EP_ID = {{ $ep->id }};
const CMT_EP_TYPE = 'episode';
const CMT_CSRF = '{{ csrf_token() }}';
const CMT_LOGGED = {{ auth()->check() ? 'true' : 'false' }};
const CMT_ME_ID = {{ auth()->id() ?? 0 }};
const CMT_HAS_GIF = {{ (auth()->check() && auth()->user()->hasPerk('comment_gif')) ? 'true' : 'false' }};
const CMT_MAX_CHARS = {{ (auth()->check() && auth()->user()->hasPerk('extended_comments')) ? 1000 : 500 }};
const CMT_USER_URL = '{{ url("/u") }}';
const CMT_AVA_HTML = {!! json_encode(
auth()->check()
? (auth()->user()->avatar
? '<img src="' . \App\Support\MediaUrl::fromStoragePath(auth()->user()->avatar) . '" style="width:100%;height:100%;object-fit:cover">'
: mb_substr(auth()->user()->name ?? '?', 0, 1))
: '?'
) !!};
const CMT_URLS = {
store: '{{ route('comments.store') }}',
like: (id) => `/comments/${id}/like`,
index: '{{ route('comments.index') }}',
gifSearch: '{{ route('comments.gif-search') }}',
};
let cmtPage = 1;
let cmtLoading = false;
let cmtSelectedGif = null;
let cmtReplyTo = null; // { id, name }
let gifSearchTimer = null;
// Initial load
document.addEventListener('DOMContentLoaded', () => { loadComments(true); });
async function loadComments(reset = false) {
if (cmtLoading) return;
if (reset) { cmtPage = 1; document.getElementById('cmt-list').innerHTML = ''; }
cmtLoading = true;
try {
const url = `${CMT_URLS.index}?type=${CMT_EP_TYPE}&id=${CMT_EP_ID}&page=${cmtPage}`;
const r = await fetch(url);
const d = await r.json();
const list = document.getElementById('cmt-list');
if (d.data.length === 0 && cmtPage === 1) {
document.getElementById('cmt-empty').style.display = 'block';
} else {
document.getElementById('cmt-empty').style.display = 'none';
d.data.forEach(c => list.insertAdjacentHTML('beforeend', renderComment(c)));
}
document.getElementById('cmt-load-more').style.display = d.has_more ? 'block' : 'none';
if (d.has_more) cmtPage++;
if (d.total !== undefined) {
const lbl = document.getElementById('cmt-count-lbl');
if (lbl) lbl.textContent = d.total + ' yorum';
updateCmtTabBadge(d.total);
}
} finally {
cmtLoading = false;
}
}
function renderComment(c, isReply = false) {
const avaClass = c.user?.role === 'admin' ? 'cmt-ava admin' : 'cmt-ava';
const name = c.user?.name ?? 'Silinmiş Kullanıcı';
// Avatar + frame ring
const frame = c.user?.profile_frame;
const avaInner = c.user?.avatar
? `<img src="${c.user.avatar}" alt="${escHtml(name)}" style="width:100%;height:100%;object-fit:cover">`
: `<span>${escHtml(name.charAt(0).toUpperCase() || '?')}</span>`;
// Frame ring: wrapper div with ::before pseudo-element (stays within bounds, no overflow clip issue)
const avaHtml = frame
? `<div class="cmt-ava-wrap fr-${frame}"><div class="${avaClass}">${avaInner}</div></div>`
: `<div class="${avaClass}">${avaInner}</div>`;
const isOwnComment = c.user?.id === CMT_ME_ID;
const followBtn = CMT_LOGGED && c.user?.id && !isOwnComment
? `<button class="cmt-follow-btn${c.user?.is_following?' following':''}" data-uid="${c.user.id}" data-follow-url="${CMT_USER_URL}/${c.user.id}/follow" onclick="cmtFollow(this,event)" title="${c.user?.is_following?'Takip Ediliyor':'Takip Et'}">${c.user?.is_following?'<i class="bi bi-check2"></i>':'<i class="bi bi-person-plus"></i>'}</button>`
: '';
// Username: gradient + effect classes on <a>
const colorClass = c.user?.username_color ? ` username-color-${c.user.username_color}` : '';
const effectClass = c.user?.username_effect ? ` username-effect-${c.user.username_effect}` : '';
const glitchAttr = c.user?.username_effect === 'glitch' ? ` data-text="${escHtml(name)}"` : '';
const nameHtml = c.user?.id
? `<a href="${CMT_USER_URL}/${c.user.id}" class="cmt-name${colorClass}${effectClass}" data-uid="${c.user.id}"${glitchAttr} style="text-decoration:none">${escHtml(name)}</a>${followBtn}`
: `<span class="cmt-name">${escHtml(name)}</span>`;
const adminBadge = c.user?.role === 'admin' ? `<span class="cmt-badge cmt-badge-admin">Admin</span>` : '';
const adminLabel = c.user?.admin_badge ? `<span class="cmt-badge cmt-badge-staff">${escHtml(c.user.admin_badge)}</span>` : '';
const customBadge = c.user?.profile_badge ? `<span class="premium-badge user-badge-custom">${escHtml(c.user.profile_badge)}</span>` : '';
const rankBadge = c.user?.watch_rank ? `<span class="cmt-badge" style="background:rgba(255,255,255,.05);border:1px solid ${escHtml(c.user.watch_rank.color)}44;color:${escHtml(c.user.watch_rank.color)};font-size:.62rem"><i class="bi ${escHtml(c.user.watch_rank.icon)}"></i> ${escHtml(c.user.watch_rank.label)}</span>` : '';
const pinnedBadge = c.is_pinned ? `<span class="cmt-badge cmt-badge-pinned"><i class="bi bi-pin-fill"></i> Sabitlendi</span>` : '';
const likeClass = c.is_liked ? 'cmt-action-btn liked' : 'cmt-action-btn';
const likeIcon = c.is_liked ? 'bi-heart-fill' : 'bi-heart';
// Replies to a reply go to the top-level parent (flat thread model)
const replyTargetId = isReply ? (c.parent_id ?? c.id) : c.id;
const replyBtn = CMT_LOGGED ? `<button class="cmt-action-btn" onclick="showReplyForm(${replyTargetId},'${escHtml(name)}')"><i class="bi bi-reply"></i> Yanıtla</button>` : '';
const gifHtml = c.gif_url ? `<img src="${c.gif_url}" alt="gif" class="cmt-gif" loading="lazy">` : '';
const textHtml = c.content ? `<div class="cmt-text">${linkifyMentions(escHtml(c.content))}</div>` : '';
const sigHtml = c.user?.comment_signature
? `<span class="comment-signature">— ${escHtml(c.user.comment_signature)}</span>` : '';
const repliesHtml = (!isReply && c.replies?.length) ? `
<div class="cmt-replies" id="replies-${c.id}">
${c.replies.map(r => renderComment(r, true)).join('')}
</div>` : (!isReply ? `<div class="cmt-replies" id="replies-${c.id}" style="display:none"></div>` : '');
// comment_bg and comment_glow — always use comment-wrap class when either is set
const bg = c.user?.comment_bg;
const glow = c.user?.comment_glow;
const wrapClass = (bg || glow) ? `cmt-item comment-wrap` : `cmt-item`;
const bgAttr = bg ? ` data-bg="${bg}"` : '';
const glowAttr = glow ? ` data-glow="${glow}"` : '';
return `
<div class="${wrapClass}" id="cmt-${c.id}"${bgAttr}${glowAttr}>
${avaHtml}
<div class="cmt-body">
<div class="cmt-meta">
${nameHtml}
${adminBadge}${adminLabel}${customBadge}${rankBadge}${pinnedBadge}
<span class="cmt-time">${c.created_at}</span>
</div>
${textHtml}
${gifHtml}
${sigHtml}
<div class="cmt-actions">
<button class="${likeClass}" id="like-btn-${c.id}" onclick="toggleLike(${c.id})">
<i class="bi ${likeIcon}" id="like-icon-${c.id}"></i>
<span id="like-count-${c.id}">${c.like_count}</span>
</button>
${replyBtn}
</div>
${repliesHtml}
</div>
</div>`;
}
function cmtPendingMsg(reason) {
if (reason === 'rude') return '🚫 Yorumunuz uygunsuz ifade içerdiği için moderatör onayına alındı.';
if (reason === 'spoiler') return '⚠️ Yorumunuz spoiler içerdiği için moderatör onayına alındı.';
return '⏳ Yorumunuz incelemeye alındı, onaylandıktan sonra yayımlanacak.';
}
async function submitComment() {
if (!CMT_LOGGED) return;
const input = document.getElementById('cmt-input');
const content = input.value.trim();
if (!content && !cmtSelectedGif) return;
if (content.length > CMT_MAX_CHARS) {
showToast(`Yorum en fazla ${CMT_MAX_CHARS} karakter olabilir.`);
return;
}
const btn = document.getElementById('cmt-submit-btn');
btn.disabled = true;
const origHTML = btn.innerHTML;
if (content) btn.innerHTML = '<i class="bi bi-robot"></i> AI İnceliyor...';
try {
const body = {
commentable_type: CMT_EP_TYPE,
commentable_id: CMT_EP_ID,
content: content,
gif_url: cmtSelectedGif,
parent_id: cmtReplyTo?.id ?? null,
};
const r = await fetch(CMT_URLS.store, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CMT_CSRF },
body: JSON.stringify(body),
});
const d = await r.json();
if (!r.ok || d.error) { showToast(d.error || 'Hata oluştu.'); return; }
input.value = '';
input.style.height = '';
clearGif();
clearReplyTarget();
if (d.pending) {
showToast(cmtPendingMsg(d.pending_reason));
return;
}
const list = document.getElementById('cmt-list');
if (cmtReplyTo) {
let repliesDiv = document.getElementById(`replies-${cmtReplyTo.id}`);
if (repliesDiv) {
repliesDiv.style.display = 'block';
repliesDiv.insertAdjacentHTML('beforeend', renderComment(d.comment, true));
}
} else {
document.getElementById('cmt-empty').style.display = 'none';
list.insertAdjacentHTML('afterbegin', renderComment(d.comment));
}
} finally {
btn.disabled = false;
btn.innerHTML = origHTML;
}
}
function linkifyMentions(html) {
const searchBase = '{{ route('search') }}';
return html.replace(/@([A-Za-z0-9_]{2,30})/g, (m, u) =>
`<a href="${searchBase}?q=%40${u}" class="cmt-mention">@${u}</a>`
);
}
async function cmtFollow(btn, e) {
e.preventDefault(); e.stopPropagation();
if (!CMT_LOGGED) { window.location = '{{ route('frontend.login') }}'; return; }
const url = btn.dataset.followUrl;
try {
const r = await fetch(url, { method:'POST', headers:{'X-CSRF-TOKEN':CMT_CSRF,'Accept':'application/json'} });
const d = await r.json();
const f = d.following;
btn.className = `cmt-follow-btn${f?' following':''}`;
btn.innerHTML = f ? '<i class="bi bi-check2"></i>' : '<i class="bi bi-person-plus"></i>';
btn.title = f ? 'Takip Ediliyor' : 'Takip Et';
} catch(err) {}
}
async function toggleLike(id) {
if (!CMT_LOGGED) { window.location = '{{ route('frontend.login') }}'; return; }
const r = await fetch(`/comments/${id}/like`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': CMT_CSRF, 'Content-Type': 'application/json' },
});
const d = await r.json();
if (!d.ok) return;
const btn = document.getElementById(`like-btn-${id}`);
const icon = document.getElementById(`like-icon-${id}`);
const cnt = document.getElementById(`like-count-${id}`);
btn.classList.toggle('liked', d.liked);
icon.className = d.liked ? 'bi bi-heart-fill' : 'bi bi-heart';
cnt.textContent = d.like_count;
}
function showReplyForm(parentId, parentName) {
// Önceki yanıt formunu temizle
document.querySelectorAll('.reply-form-wrap').forEach(el => el.remove());
cmtReplyTo = { id: parentId, name: parentName };
const form = `
<div class="reply-form-wrap" id="reply-form-${parentId}">
<div class="cmt-ava" style="width:28px;height:28px;font-size:.68rem;overflow:hidden">${CMT_AVA_HTML}</div>
<textarea class="reply-input" id="reply-input-${parentId}" placeholder="@${escHtml(parentName)} yanıtla..." rows="2" oninput="autoResize(this)"></textarea>
<button class="reply-submit-btn" onclick="submitReply(${parentId})"><i class="bi bi-send-fill"></i></button>
</div>`;
const repliesDiv = document.getElementById(`replies-${parentId}`);
if (repliesDiv) {
repliesDiv.style.display = 'block';
repliesDiv.insertAdjacentHTML('beforeend', form);
document.getElementById(`reply-input-${parentId}`)?.focus();
}
}
async function submitReply(parentId) {
const input = document.getElementById(`reply-input-${parentId}`);
const content = input?.value?.trim();
if (!content) return;
await submitCommentWithData({ content, parent_id: parentId });
document.getElementById(`reply-form-${parentId}`)?.remove();
cmtReplyTo = null;
}
async function submitCommentWithData(extra) {
const body = {
commentable_type: CMT_EP_TYPE,
commentable_id: CMT_EP_ID,
content: extra.content ?? '',
gif_url: extra.gif_url ?? null,
parent_id: extra.parent_id ?? null,
};
const r = await fetch(CMT_URLS.store, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CMT_CSRF },
body: JSON.stringify(body),
});
const d = await r.json();
if (!r.ok || d.error) { showToast(d.error || 'Hata oluştu.', 'error'); return; }
if (d.pending) { showToast(cmtPendingMsg(d.pending_reason)); return; }
if (extra.parent_id) {
const repliesDiv = document.getElementById(`replies-${extra.parent_id}`);
if (repliesDiv) { repliesDiv.style.display='block'; repliesDiv.insertAdjacentHTML('beforeend', renderComment(d.comment, true)); }
} else {
document.getElementById('cmt-empty').style.display = 'none';
document.getElementById('cmt-list').insertAdjacentHTML('afterbegin', renderComment(d.comment));
}
}
function clearReplyTarget() {
cmtReplyTo = null;
document.querySelectorAll('.reply-form-wrap').forEach(el => el.remove());
}
// GIF Picker
function toggleGifPicker() {
const p = document.getElementById('gif-picker');
if (p.style.display === 'none') {
p.style.display = 'block';
if (!document.getElementById('gif-search-input').value) searchGif('anime');
document.getElementById('gif-search-input').focus();
} else {
p.style.display = 'none';
}
}
function searchGif(q) {
clearTimeout(gifSearchTimer);
gifSearchTimer = setTimeout(async () => {
if (!q.trim()) q = 'anime';
document.getElementById('gif-results').innerHTML = '<div class="gif-loading"><i class="bi bi-arrow-repeat"></i> Aranıyor...</div>';
try {
const r = await fetch(`${CMT_URLS.gifSearch}?q=${encodeURIComponent(q)}`);
const d = await r.json();
if (d.error === 'no_key') {
document.getElementById('gif-results').innerHTML = '<div class="gif-loading" style="font-size:.8rem;line-height:1.6">GIF özelliği için admin panelinden<br><strong>Giphy API key</strong> tanımlanmalı.</div>';
return;
}
if (!d.results?.length) {
document.getElementById('gif-results').innerHTML = '<div class="gif-loading">Sonuç bulunamadı.</div>';
return;
}
document.getElementById('gif-results').innerHTML = d.results.map(g =>
`<img src="${g.preview}" data-full="${g.url}" alt="${escHtml(g.title)}" onclick="selectGif('${g.url}')" loading="lazy">`
).join('');
} catch(e) {
document.getElementById('gif-results').innerHTML = '<div class="gif-loading">GIF yüklenemedi.</div>';
}
}, 400);
}
function selectGif(url) {
cmtSelectedGif = url;
document.getElementById('cmt-gif-img').src = url;
document.getElementById('cmt-gif-preview').style.display = 'block';
document.getElementById('gif-picker').style.display = 'none';
}
function clearGif() {
cmtSelectedGif = null;
document.getElementById('cmt-gif-img').src = '';
document.getElementById('cmt-gif-preview').style.display = 'none';
}
// Helpers
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 180) + 'px';
}
function updateCharCount() {
const el = document.getElementById('cmt-input');
const cnt = document.getElementById('cmt-char-count');
if (!el || !cnt) return;
const len = el.value.length;
cnt.textContent = len + ' / ' + CMT_MAX_CHARS;
cnt.style.color = len > CMT_MAX_CHARS * .9 ? (len >= CMT_MAX_CHARS ? '#ff4f4f' : '#ffbf00') : 'rgba(255,255,255,.25)';
}
function escHtml(s) {
return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
function showToast(msg, type, duration) {
duration = (typeof type === 'number' ? type : duration) || 3500;
const colors = { error: 'rgba(255,45,125,.15)', warning: 'rgba(251,191,36,.12)' };
const borders = { error: 'rgba(255,45,125,.4)', warning: 'rgba(251,191,36,.3)' };
const textClr = { error: 'var(--accent)', warning: '#fbbf24' };
const bg = colors[type] || 'rgba(0,245,255,.15)';
const bdr = borders[type] || 'rgba(0,245,255,.35)';
const clr = textClr[type] || 'var(--accent2)';
const t = document.createElement('div');
t.innerHTML = msg;
t.style.cssText = `position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:${bg};border:1px solid ${bdr};color:${clr};padding:12px 24px;border-radius:10px;font-size:.86rem;font-weight:700;z-index:99999;backdrop-filter:blur(12px);white-space:nowrap;animation:toast-in .28s cubic-bezier(.22,1,.36,1) forwards`;
document.body.appendChild(t);
setTimeout(() => t.remove(), duration);
}
// ── Tab switch ────────────────────────────────────────────────────────────────
function switchTab(name, btn) {
document.querySelectorAll('.eb-tab').forEach(t => t.classList.remove('on'));
document.querySelectorAll('.eb-panel').forEach(p => p.classList.remove('on'));
btn.classList.add('on');
const panel = document.getElementById('panel-' + name);
if (panel) panel.classList.add('on');
if (name === 'cmt') {
const list = document.getElementById('cmt-list');
if (list && !list.children.length) loadComments(true);
}
if (name === 'notes') {
loadNotes();
}
}
// Badge güncelleme (loadComments'tan çağırılır)
function updateCmtTabBadge(count) {
const badge = document.getElementById('tab-cmt-badge');
if (!badge) return;
if (count > 0) { badge.textContent = count; badge.style.display = ''; }
else badge.style.display = 'none';
}
// ── Açıklama expand/collapse ──────────────────────────────────────────────────
function toggleDesc() {
const el = document.getElementById('ep-desc');
const btn = document.getElementById('ep-expand-btn');
if (!el || !btn) return;
const expanded = el.classList.toggle('expanded');
btn.innerHTML = expanded
? 'Daha Az Göster <i class="bi bi-chevron-up"></i>'
: 'Devamını Oku <i class="bi bi-chevron-down"></i>';
}
// ── AI Panel ──────────────────────────────────────────────────────────────────
@auth
const CSRF_TOKEN = document.querySelector('meta[name=csrf-token]')?.content || '';
const AI_ANIME = @json($anime->title);
const AI_EP_NUM = @json($ep->episode_number);
const AI_EP_TITLE = @json($ep->title ?? '');
const AI_EP_DESC = @json($ep->description ?? $anime->description ?? '');
let aiEpAnalyzed = false;
let playerAiMsgs = [{role:'user',content:'Şu an ' + AI_ANIME + ' izliyorum, ' + AI_EP_NUM + '. bölümdeyim.'}];
async function loadEpAi() {
if (aiEpAnalyzed) return;
const btn = document.getElementById('ai-ep-btn');
const loader = document.getElementById('ai-ep-loader');
const result = document.getElementById('ai-ep-result');
if (!btn) return;
btn.style.display = 'none';
loader.style.display = 'flex';
try {
const res = await fetch('{{ route("ai.episode-info") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF_TOKEN},
body: JSON.stringify({
anime_title: AI_ANIME,
episode_number: AI_EP_NUM,
episode_title: AI_EP_TITLE,
description: AI_EP_DESC
})
});
const data = await res.json();
loader.style.display = 'none';
if (data.info) {
result.textContent = data.info;
result.style.display = 'block';
aiEpAnalyzed = true;
} else {
btn.style.display = '';
btn.textContent = '⚠ Analiz yapılamadı, tekrar dene';
}
} catch(e) {
loader.style.display = 'none';
btn.style.display = '';
}
}
function aiPlayerAsk(q) {
document.getElementById('ai-player-inp').value = q;
sendPlayerAiMsg();
}
async function sendPlayerAiMsg() {
const inp = document.getElementById('ai-player-inp');
const msgs = document.getElementById('ai-player-msgs');
const send = document.getElementById('ai-player-send');
const txt = inp.value.trim();
if (!txt) return;
inp.value = '';
send.disabled = true;
// User bubble
const ub = document.createElement('div');
ub.style.cssText = 'align-self:flex-end;max-width:85%;background:linear-gradient(135deg,rgba(255,45,125,.18),rgba(184,77,255,.12));border:1px solid rgba(255,45,125,.25);border-radius:10px 10px 2px 10px;padding:9px 12px;font-size:.83rem;color:var(--text);line-height:1.5';
ub.textContent = txt;
msgs.appendChild(ub);
playerAiMsgs.push({role: 'user', content: txt});
// Typing
const tb = document.createElement('div');
tb.style.cssText = 'display:flex;gap:4px;padding:10px 13px;align-self:flex-start';
tb.innerHTML = '<div style="width:7px;height:7px;border-radius:50%;background:var(--accent3);animation:aip-b .9s ease-in-out infinite"></div><div style="width:7px;height:7px;border-radius:50%;background:var(--accent3);animation:aip-b .9s .18s ease-in-out infinite"></div><div style="width:7px;height:7px;border-radius:50%;background:var(--accent3);animation:aip-b .9s .36s ease-in-out infinite"></div>';
msgs.appendChild(tb);
msgs.scrollTop = msgs.scrollHeight;
try {
const res = await fetch('{{ route("ai.chat") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF_TOKEN},
body: JSON.stringify({messages: playerAiMsgs})
});
const data = await res.json();
tb.remove();
const reply = data.reply || data.error || 'Bir hata oluştu.';
playerAiMsgs.push({role: 'assistant', content: reply});
const ab = document.createElement('div');
ab.style.cssText = 'align-self:flex-start;max-width:92%;background:linear-gradient(135deg,rgba(184,77,255,.12),rgba(0,245,255,.07));border:1px solid rgba(184,77,255,.2);border-radius:10px 10px 10px 2px;padding:9px 12px;font-size:.83rem;color:var(--text);line-height:1.55;white-space:pre-wrap';
ab.textContent = reply;
msgs.appendChild(ab);
} catch(e) {
tb.remove();
}
send.disabled = false;
msgs.scrollTop = msgs.scrollHeight;
}
@endauth
// ── Video izleme takibi ───────────────────────────────────────────────────────
window.addEventListener('load', function() {
if (typeof TrackWatch === 'function') {
TrackWatch(
{{ $anime->id }},
{{ $ep->id }},
{{ $seasonModel->season_number }},
{{ $ep->episode_number }}
);
}
});
// ── Continue watching ─────────────────────────────────────────────────────────
@auth
(function(){
const vd = document.querySelector('video');
if (!vd) return;
const CSRF_T = document.querySelector('meta[name=csrf-token]')?.content||'';
let lastSent = 0;
function sendContinue() {
const sec = Math.floor(vd.currentTime);
if (sec < 5 || sec === lastSent) return;
lastSent = sec;
const total = Math.floor(vd.duration)||0;
const pct = total>0 ? Math.min(100,Math.round(sec/total*100)) : 0;
fetch('{{ route("continue.update") }}', {
method:'POST',
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF_T},
keepalive:true,
body:JSON.stringify({
anime_id:{{ $anime->id }},
episode_id:{{ $ep->id }},
season_number:{{ $seasonModel->season_number }},
episode_number:{{ $ep->episode_number }},
seconds:sec, total, percent:pct
})
}).catch(()=>{});
}
setInterval(sendContinue, 30000);
vd.addEventListener('pause', sendContinue);
window.addEventListener('beforeunload', sendContinue);
document.addEventListener('visibilitychange', ()=>{ if(document.hidden) sendContinue(); });
})();
@endauth
// ── Episode like/dislike ─────────────────────────────────────────────────────
@auth
async function voteEpisode(vote) {
const CSRF_T = document.querySelector('meta[name=csrf-token]')?.content||'';
const res = await fetch('{{ route("episode.vote", $ep) }}', {
method:'POST',
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF_T},
body:JSON.stringify({vote})
});
const d = await res.json();
document.getElementById('like-cnt').textContent = d.likes;
document.getElementById('dislike-cnt').textContent = d.dislikes;
const lb = document.getElementById('btn-like');
const db = document.getElementById('btn-dislike');
lb.style.borderColor = d.my_vote===1 ? '#3fb950' : '#30363d';
lb.style.background = d.my_vote===1 ? 'rgba(63,185,80,.15)' : 'transparent';
lb.style.color = d.my_vote===1 ? '#3fb950' : 'var(--text2)';
document.getElementById('like-icon').className = d.my_vote===1 ? 'bi bi-hand-thumbs-up-fill' : 'bi bi-hand-thumbs-up';
db.style.borderColor = d.my_vote===-1 ? '#f85149' : '#30363d';
db.style.background = d.my_vote===-1 ? 'rgba(248,81,73,.15)' : 'transparent';
db.style.color = d.my_vote===-1 ? '#f85149' : 'var(--text2)';
document.getElementById('dislike-icon').className = d.my_vote===-1 ? 'bi bi-hand-thumbs-down-fill' : 'bi bi-hand-thumbs-down';
}
@else
function voteEpisode(){ window.location='{{ route("frontend.login") }}'; }
@endauth
function showPlayerLoginPrompt() {
window.location = '{{ route("frontend.login") }}?redirect={{ urlencode(request()->fullUrl()) }}';
}
// ── Bölüm Notları ────────────────────────────────────────────────────────────
@auth
const NOTE_EPISODE_ID = {{ $ep->id }};
const NOTE_CSRF = document.querySelector('meta[name=csrf-token]')?.content||'';
let currentNoteTs = null;
function captureTimestamp() {
const vd = document.getElementById('vd');
if (vd && !isNaN(vd.currentTime) && vd.currentTime > 0) {
currentNoteTs = Math.floor(vd.currentTime);
const m = Math.floor(currentNoteTs / 60);
const s = currentNoteTs % 60;
document.getElementById('note-ts-label').textContent = `${m}:${s.toString().padStart(2,'0')} anında`;
}
}
async function saveNote() {
const content = document.getElementById('note-input').value.trim();
if (!content) return;
try {
const res = await fetch(`/episode/${NOTE_EPISODE_ID}/note`, {
method:'POST',
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':NOTE_CSRF},
body: JSON.stringify({ content, timestamp_at: currentNoteTs })
});
const d = await res.json();
if (d.ok) {
document.getElementById('note-input').value = '';
currentNoteTs = null;
document.getElementById('note-ts-label').textContent = 'Şu anki zaman';
prependNote(d.note);
}
} catch(e) {}
}
async function loadNotes() {
const list = document.getElementById('notes-list');
if (list.dataset.loaded) return;
list.dataset.loaded = '1';
try {
const res = await fetch(`/episode/${NOTE_EPISODE_ID}/notes`);
const d = await res.json();
renderNotes(d.notes || []);
} catch(e) {}
}
function renderNotes(notes) {
const list = document.getElementById('notes-list');
const empty = document.getElementById('notes-empty');
const badge = document.getElementById('tab-notes-badge');
list.innerHTML = '';
if (!notes.length) {
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
if (badge) { badge.textContent = notes.length; badge.style.display = ''; }
notes.forEach(n => prependNote(n, true));
}
function prependNote(n, append = false) {
const empty = document.getElementById('notes-empty');
const list = document.getElementById('notes-list');
const badge = document.getElementById('tab-notes-badge');
empty.style.display = 'none';
const el = document.createElement('div');
el.id = `note-${n.id}`;
el.style.cssText = 'background:rgba(255,215,0,.06);border:1px solid rgba(255,215,0,.15);border-radius:9px;padding:10px 12px;position:relative';
el.innerHTML = `
${n.timestamp_label ? `<div style="font-size:.68rem;color:#ffd700;font-weight:700;margin-bottom:5px"><i class="bi bi-clock"></i> ${n.timestamp_label}</div>` : ''}
<div style="font-size:.84rem;color:var(--text2);line-height:1.5">${escHtml(n.content)}</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px">
<span style="font-size:.65rem;color:var(--text3)">${n.created_at}</span>
<button onclick="deleteNote(${n.id})" style="background:none;border:none;color:var(--text3);cursor:pointer;font-size:.75rem;padding:2px 6px" title="Sil"><i class="bi bi-trash"></i></button>
</div>`;
if (append) list.appendChild(el);
else list.prepend(el);
if (badge) {
const cnt = list.children.length;
badge.textContent = cnt;
badge.style.display = cnt ? '' : 'none';
}
}
async function deleteNote(id) {
if (!confirm('Notu sil?')) return;
try {
await fetch(`/notes/${id}`, {
method:'DELETE',
headers:{'X-CSRF-TOKEN':NOTE_CSRF}
});
document.getElementById(`note-${id}`)?.remove();
const list = document.getElementById('notes-list');
const badge = document.getElementById('tab-notes-badge');
if (!list.children.length) {
document.getElementById('notes-empty').style.display = 'block';
if (badge) badge.style.display = 'none';
} else if (badge) badge.textContent = list.children.length;
} catch(e) {}
}
@endauth
</script>
@auth
<script>
// ── Share modal ────────────────────────────────────────────
const SHARE_ANIME_SLUG = '{{ $anime->slug }}';
const SHARE_ANIME_TITLE = {!! json_encode($anime->title) !!};
const SHARE_COVER = {!! json_encode($anime->coverUrl ?? '') !!};
const SHARE_SEASON = {{ $seasonModel->season_number }};
const SHARE_EP = {{ $ep->episode_number }};
const SHARE_EP_TITLE = {!! json_encode($ep->title ?? '') !!};
const SHARE_CSRF = '{{ csrf_token() }}';
const SHARE_API_USERS = '{{ url("/search") }}';
const SHARE_QUICK_URL = '{{ route("messages.quick-share") }}';
let shareSelectedUser = null;
let shareSearchTimer = null;
function openShareModal() {
document.getElementById('share-modal-overlay').style.display = 'flex';
document.getElementById('share-search').value = '';
document.getElementById('share-results').innerHTML = '';
setShareSelected(null);
setTimeout(() => document.getElementById('share-search').focus(), 100);
}
function closeShareModal() {
document.getElementById('share-modal-overlay').style.display = 'none';
}
function shareSearch(q) {
clearTimeout(shareSearchTimer);
q = q.trim();
if (!q) { document.getElementById('share-results').innerHTML = ''; return; }
shareSearchTimer = setTimeout(async () => {
try {
const r = await fetch(`/api/users/search?q=${encodeURIComponent(q)}`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': SHARE_CSRF }
});
if (!r.ok) throw new Error();
const users = await r.json();
renderShareResults(users);
} catch(e) {
// fallback: search via user card endpoint pattern not possible, show nothing
document.getElementById('share-results').innerHTML = '';
}
}, 350);
}
function renderShareResults(users) {
const el = document.getElementById('share-results');
if (!users.length) { el.innerHTML = '<div style="font-size:.78rem;color:rgba(255,255,255,.3);text-align:center;padding:8px">Kullanıcı bulunamadı</div>'; return; }
el.innerHTML = users.map(u => `
<div onclick="setShareSelected(${JSON.stringify(u).replace(/</g,'\\u003c').replace(/"/g,'&quot;')})" style="display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:background .15s" onmouseover="this.style.background='rgba(255,255,255,.06)'" onmouseout="this.style.background=''">
<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--accent2),var(--accent));display:flex;align-items:center;justify-content:center;font-size:.8rem;font-weight:800;color:#fff;flex-shrink:0;overflow:hidden">
${u.avatar ? `<img src="${u.avatar}" style="width:100%;height:100%;object-fit:cover">` : (u.name||'?').charAt(0).toUpperCase()}
</div>
<div>
<div style="font-size:.84rem;font-weight:700;color:#fff">${escHtml(u.name||'')}</div>
${u.username ? `<div style="font-size:.7rem;color:rgba(255,255,255,.35)">@${escHtml(u.username)}</div>` : ''}
</div>
</div>`).join('');
}
function setShareSelected(user) {
shareSelectedUser = user;
const el = document.getElementById('share-selected');
const btn = document.getElementById('share-send-btn');
if (!user) {
el.style.display = 'none';
btn.disabled = true; btn.style.opacity = '.5'; btn.style.cursor = 'default';
return;
}
el.style.display = 'block';
el.innerHTML = `<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.2);border-radius:10px">
<i class="bi bi-person-check" style="color:var(--accent2)"></i>
<span style="font-size:.82rem;color:#fff">Kime: <strong>${escHtml(user.name||'')}</strong></span>
<button onclick="setShareSelected(null)" style="margin-left:auto;background:none;border:none;color:rgba(255,255,255,.3);cursor:pointer;font-size:.8rem"><i class="bi bi-x"></i></button>
</div>`;
document.getElementById('share-results').innerHTML = '';
document.getElementById('share-search').value = '';
btn.disabled = false; btn.style.opacity = '1'; btn.style.cursor = 'pointer';
}
async function doShareSend() {
if (!shareSelectedUser) return;
const vd = document.getElementById('vd');
const ts = (document.getElementById('share-ts-chk')?.checked && vd)
? Math.floor(vd.currentTime) : 0;
const payload = JSON.stringify({
slug: SHARE_ANIME_SLUG,
title: SHARE_ANIME_TITLE,
cover: SHARE_COVER,
s: SHARE_SEASON,
e: SHARE_EP,
et: SHARE_EP_TITLE,
ts: ts
});
const btn = document.getElementById('share-send-btn');
btn.disabled = true; btn.textContent = 'Gönderiliyor…';
try {
const r = await fetch(SHARE_QUICK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SHARE_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ to_user_id: shareSelectedUser.id, body: 'ANIMESHARE::' + payload }),
});
const d = await r.json();
if (d.ok) {
closeShareModal();
showToast('Paylaşıldı! <a href="/messages/' + d.conversation_id + '" style="color:var(--accent2);font-weight:700">Konuşmaya git →</a>', 4000);
} else {
showToast('Gönderilemedi.');
}
} catch(e) {
showToast('Bağlantı hatası.');
}
btn.disabled = false; btn.textContent = 'Gönder';
}
</script>
@endauth
{{-- ═══════════════════════════════════════════════════════════════
SOSYAL ÖZELLİKLER JS: NicoNico + Tahminler + Watch Party + First-Watch
═══════════════════════════════════════════════════════════════ --}}
<script>
const SOCIAL_EPISODE_ID = {{ $ep->id }};
const SOCIAL_CSRF = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
const SOCIAL_MY_USER_ID = {{ auth()->id() ?? 'null' }};
const SOCIAL_FOLLOWING_IDS = @auth @json(\App\Models\UserFollow::where('follower_id', auth()->id())->pluck('following_id')->toArray()) @else [] @endauth;
// ──────────────────────────────────────────────────────────────
// NicoNico Timestamp Yorumları
// ──────────────────────────────────────────────────────────────
let nicoComments = []; // [{id, timestamp_sec, body, color, username, user_id}]
let nicoActive = []; // ekranda aktif uçuşanlar
let nicoEnabled = localStorage.getItem('nico_pref') === '1'; // default OFF
let nicoFollowedOnly = false;
function applyNicoState() {
const row = document.getElementById('nico-input-row');
const btn = document.getElementById('nico-toggle-btn');
const layer = document.getElementById('nico-layer');
if (nicoEnabled) {
row?.classList.remove('nico-off');
btn?.classList.add('on');
if (layer) layer.style.display = '';
} else {
row?.classList.add('nico-off');
btn?.classList.remove('on');
if (layer) layer.style.display = 'none';
// Ekranda uçan yorumları temizle
document.querySelectorAll('#nico-layer > div').forEach(el => el.remove());
nicoActive = [];
}
}
function toggleNico() {
nicoEnabled = !nicoEnabled;
localStorage.setItem('nico_pref', nicoEnabled ? '1' : '0');
applyNicoState();
if (nicoEnabled && nicoComments.length === 0) loadNicoComments();
}
function toggleNicoFilter() {
nicoFollowedOnly = !nicoFollowedOnly;
document.getElementById('nico-filter-btn')?.classList.toggle('on', nicoFollowedOnly);
}
async function loadNicoComments() {
try {
const r = await fetch(`/episode/${SOCIAL_EPISODE_ID}/timestamp-comments`);
const d = await r.json();
nicoComments = d.comments || [];
} catch(e) {}
}
function nicoTick() {
if (!nicoEnabled) return;
const vd = document.getElementById('vd');
if (!vd || vd.paused) return;
const now = Math.floor(vd.currentTime);
const toShow = nicoComments.filter(c =>
c.timestamp_sec === now && !nicoActive.find(a => a.id === c.id) &&
(!nicoFollowedOnly || c.user_id === SOCIAL_MY_USER_ID || SOCIAL_FOLLOWING_IDS.includes(c.user_id))
);
toShow.forEach(c => spawnNicoComment(c));
nicoActive = nicoActive.filter(a => document.getElementById('nico-' + a.id));
}
function spawnNicoComment(c) {
const layer = document.getElementById('nico-layer');
if (!layer) return;
const el = document.createElement('div');
el.id = 'nico-' + c.id;
el.textContent = c.body;
const topPct = 5 + Math.random() * 80;
el.style.cssText = `
position:absolute;
top:${topPct}%;
left:100%;
white-space:nowrap;
font-size:clamp(.75rem,2vw,1rem);
font-weight:700;
color:${c.color};
text-shadow:1px 1px 3px rgba(0,0,0,.9);
pointer-events:none;
animation:nicofly 8s linear forwards;
z-index:18;
`;
layer.appendChild(el);
nicoActive.push({ id: c.id });
el.addEventListener('animationend', () => el.remove());
}
// CSS animasyonu ekle
(function() {
if (document.getElementById('nico-keyframes')) return;
const s = document.createElement('style');
s.id = 'nico-keyframes';
s.textContent = '@keyframes nicofly{from{transform:translateX(0)}to{transform:translateX(calc(-100% - 100vw))}}';
document.head.appendChild(s);
})();
// Yorum gönder
async function sendNicoComment() {
if (!IS_AUTH) { showToast('Yorum yazmak için giriş yapmalısın.'); return; }
const input = document.getElementById('nico-input');
const color = document.getElementById('nico-color-pick')?.value ?? '#ffffff';
const body = input?.value.trim();
if (!body) return;
const vd = document.getElementById('vd');
const ts = vd ? Math.floor(vd.currentTime) : 0;
try {
const r = await fetch(`/episode/${SOCIAL_EPISODE_ID}/timestamp-comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ timestamp_sec: ts, body, color }),
});
let d;
try { d = await r.json(); } catch(_) {
console.error('NicoNico HTTP ' + r.status, r.statusText);
showToast('Sunucu hatası (' + r.status + '). migrate_run.php çalıştırıldı mı?');
return;
}
if (d.ok) {
input.value = '';
spawnNicoComment({ id: 'tmp-' + Date.now(), timestamp_sec: ts, body, color, username: 'sen' });
nicoComments.push({ id: d.id, timestamp_sec: ts, body, color });
} else {
showToast(d.error || d.message || 'Gönderilemedi.');
}
} catch(e) { showToast('Ağ hatası: ' + e.message); }
}
// Video timeupdate ile NicoNico tick
document.addEventListener('DOMContentLoaded', () => {
applyNicoState();
if (nicoEnabled) loadNicoComments();
const vd = document.getElementById('vd');
if (vd) {
let lastNicoSec = -1;
vd.addEventListener('timeupdate', () => {
const s = Math.floor(vd.currentTime);
if (s !== lastNicoSec) { lastNicoSec = s; nicoTick(); }
});
}
});
// ──────────────────────────────────────────────────────────────
// Tahmin Oyunu
// ──────────────────────────────────────────────────────────────
let myPredictionId = null;
async function loadPredictions() {
try {
const r = await fetch(`/episode/${SOCIAL_EPISODE_ID}/predictions`);
const d = await r.json();
myPredictionId = d.my_prediction;
renderPredictions(d.predictions || []);
const badge = document.getElementById('tab-predict-badge');
if (badge && d.predictions?.length) {
badge.textContent = d.predictions.length;
badge.style.display = '';
}
} catch(e) {}
}
function renderPredictions(preds) {
const list = document.getElementById('predictions-list');
const empty = document.getElementById('predictions-empty');
if (!list) return;
if (!preds.length) {
list.innerHTML = '';
if (empty) empty.style.display = '';
return;
}
if (empty) empty.style.display = 'none';
list.innerHTML = preds.map(p => {
const cls = p.is_correct === true ? 'correct' : p.is_correct === false ? 'wrong' : '';
const icon = p.is_correct === true ? '✅' : p.is_correct === false ? '❌' : '';
const voteCls = p.voted ? 'voted' : '';
return `
<div class="pred-item ${cls}" id="pred-${p.id}">
<div style="display:flex;align-items:flex-start;gap:8px">
<div style="flex:1;font-size:.84rem;color:${p.is_correct===false?'rgba(255,255,255,.45)':'#fff'};line-height:1.4">
${icon} ${escHtml(p.body)}
</div>
<button class="pred-vote-btn ${voteCls}" onclick="votePrediction(${p.id},this)">
<i class="bi bi-arrow-up"></i> ${p.vote_count}
</button>
</div>
<div style="font-size:.7rem;color:rgba(255,255,255,.3);margin-top:6px">@${escHtml(p.username||'anonim')} · ${escHtml(p.created_at)}</div>
</div>`;
}).join('');
}
async function submitPrediction() {
const input = document.getElementById('predict-input');
const body = input?.value.trim();
if (!body || body.length < 5) { showToast('En az 5 karakter yaz.'); return; }
try {
const r = await fetch(`/episode/${SOCIAL_EPISODE_ID}/predictions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ body }),
});
const d = await r.json();
if (d.ok) {
input.value = '';
showToast('Tahmin kaydedildi!');
loadPredictions();
} else {
showToast(d.error || 'Gönderilemedi.');
}
} catch(e) { showToast('Bağlantı hatası.'); }
}
async function votePrediction(id, btn) {
try {
const r = await fetch(`/predictions/${id}/vote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: '{}',
});
const d = await r.json();
btn.classList.toggle('voted', d.voted);
btn.innerHTML = `<i class="bi bi-arrow-up"></i> ${d.vote_count}`;
} catch(e) {}
}
// ──────────────────────────────────────────────────────────────
// İlk Kez İzleyenler
// ──────────────────────────────────────────────────────────────
let firstWatchRegistered = false;
async function fetchFirstWatchCount() {
try {
const r = await fetch(`/episode/${SOCIAL_EPISODE_ID}/first-watch-count`);
const d = await r.json();
const badge = document.getElementById('firstwatch-badge');
const cnt = document.getElementById('firstwatch-count');
if (badge && cnt) {
if (d.count > 0) {
cnt.textContent = d.count;
badge.style.display = '';
} else {
badge.style.display = 'none';
}
}
} catch(e) {}
}
async function registerFirstWatch(isFirst) {
if (firstWatchRegistered) return;
firstWatchRegistered = true;
try {
await fetch(`/episode/${SOCIAL_EPISODE_ID}/first-watch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ is_first_time: isFirst }),
});
fetchFirstWatchCount();
} catch(e) {}
}
// Kullanıcı videoyu ilk 30 saniyesinde izlemeye başlarsa "ilk kez izleyen" sayıl
document.addEventListener('DOMContentLoaded', () => {
fetchFirstWatchCount();
setInterval(fetchFirstWatchCount, 60000); // Her dakika güncelle
const vd = document.getElementById('vd');
if (vd && IS_AUTH) {
vd.addEventListener('play', () => {
if (vd.currentTime < 30) registerFirstWatch(true);
}, { once: true });
}
});
// ──────────────────────────────────────────────────────────────
// Watch Party
// ──────────────────────────────────────────────────────────────
let partyRoomCode = null;
let partyIsHost = false;
let partySyncInterval = null;
function openPartyModal() {
const bg = document.getElementById('party-modal-bg');
if (bg) { bg.classList.add('open'); document.body.style.overflow = 'hidden'; }
document.getElementById('party-create-view').style.display = '';
document.getElementById('party-room-view').style.display = 'none';
document.getElementById('party-private').onchange = function() {
document.getElementById('party-pw-wrap').style.display = this.checked ? '' : 'none';
};
if (partyRoomCode) showPartyRoom(partyRoomCode);
}
function closePartyModal() {
const bg = document.getElementById('party-modal-bg');
if (bg) { bg.classList.remove('open'); document.body.style.overflow = ''; }
}
async function createParty() {
const isPrivate = document.getElementById('party-private')?.checked ?? false;
const password = document.getElementById('party-pw')?.value.trim() ?? '';
try {
const r = await fetch('/party/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ episode_id: SOCIAL_EPISODE_ID, is_private: isPrivate, password: password || null }),
});
const d = await r.json();
if (d.ok) {
partyRoomCode = d.room_code;
partyIsHost = true;
showPartyRoom(d.room_code);
startPartySync();
} else {
showToast(d.error || 'Oda oluşturulamadı.');
}
} catch(e) { showToast('Bağlantı hatası.'); }
}
async function joinParty() {
const code = document.getElementById('party-join-code')?.value.trim().toUpperCase();
if (!code || code.length < 4) { showToast('Geçerli bir oda kodu gir.'); return; }
const password = prompt('Şifreli bir oda ise şifre gir (boş bırak)') ?? '';
try {
const r = await fetch(`/party/${code}/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ password }),
});
const d = await r.json();
if (d.ok) {
partyRoomCode = code;
partyIsHost = false;
showPartyRoom(code);
startPartySync();
// Sunucudaki pozisyona sıçra
const vd = document.getElementById('vd');
if (vd && d.current_sec > 0) {
vd.currentTime = d.current_sec;
if (d.is_playing) vd.play().catch(()=>{});
else vd.pause();
}
renderPartyMembers(d.members || []);
} else {
showToast(d.error || 'Katılınamadı.');
}
} catch(e) { showToast('Bağlantı hatası.'); }
}
function showPartyRoom(code) {
document.getElementById('party-create-view').style.display = 'none';
document.getElementById('party-room-view').style.display = '';
document.getElementById('party-room-code').textContent = code;
const fab = document.getElementById('party-fab');
if (fab) { fab.style.display = ''; fab.innerHTML = `<i class="bi bi-people-fill" style="color:#ff2d7d"></i> Oda: ${code}`; }
}
function copyRoomCode() {
if (!partyRoomCode) return;
navigator.clipboard?.writeText(partyRoomCode).then(() => showToast('Oda kodu kopyalandı!'));
}
function renderPartyMembers(members) {
const el = document.getElementById('party-members-list');
if (!el) return;
el.innerHTML = members.map(m => `
<div style="display:flex;align-items:center;gap:6px;padding:5px 10px;background:rgba(255,255,255,.06);border-radius:20px;font-size:.76rem;color:#fff">
${m.is_host ? '<i class="bi bi-star-fill" style="color:#ffd700;font-size:.65rem"></i>' : ''}
${escHtml(m.name || m.username || '?')}
</div>`).join('');
}
function startPartySync() {
if (partySyncInterval) clearInterval(partySyncInterval);
partySyncInterval = setInterval(doPartySync, 3000);
}
async function doPartySync() {
if (!partyRoomCode) return;
const vd = document.getElementById('vd');
try {
const body = partyIsHost
? { current_sec: Math.floor(vd?.currentTime ?? 0), is_playing: !(vd?.paused ?? true) }
: {};
const r = await fetch(`/party/${partyRoomCode}/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify(body),
});
const d = await r.json();
renderPartyMembers(d.members || []);
// Non-host: senkronize ol (2 saniyeden fazla sapma varsa)
if (!partyIsHost && 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();
}
} catch(e) {}
}
async function leaveParty() {
if (!partyRoomCode) return;
if (partySyncInterval) { clearInterval(partySyncInterval); partySyncInterval = null; }
try {
await fetch(`/party/${partyRoomCode}/leave`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF },
body: '{}',
});
} catch(e) {}
partyRoomCode = null;
const fab = document.getElementById('party-fab');
if (fab) { fab.style.display = 'none'; }
closePartyModal();
showToast('Odadan çıkıldı.');
}
// Watch Party FAB'ini video yüklendikten sonra göster
document.addEventListener('DOMContentLoaded', () => {
const fab = document.getElementById('party-fab');
if (fab) setTimeout(() => { fab.style.display = ''; }, 2000);
loadPredictions();
});
// ── Ruh Hali Motoru ──────────────────────────────────────────────────────────
vd.addEventListener('ended', () => {
setTimeout(() => {
const mo = document.getElementById('mood-overlay');
if (mo) mo.classList.add('on');
}, 800);
});
async function fetchMood(mood) {
document.getElementById('mood-picker').style.display = 'none';
const res = document.getElementById('mood-results');
const grid = document.getElementById('mood-grid');
res.style.display = 'block';
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:20px;color:rgba(255,255,255,.3)"><i class="bi bi-hourglass"></i></div>';
const labels = { sad:'😭 Ağlatacak animeler', funny:'🤣 Güldürecek animeler', hype:'🔥 Heyecan verici animeler', think:'🧠 Düşündürücü animeler', romance:'😍 Romantik animeler', scary:'👻 Korkutucu animeler' };
document.getElementById('mood-results-title').textContent = labels[mood] || 'Öneriler';
try {
const r = await fetch('/mood/recommend', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ mood }),
});
const d = await r.json();
if (!d.animes?.length) { grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:rgba(255,255,255,.3);font-size:.82rem">Öneri bulunamadı.</div>'; return; }
grid.innerHTML = d.animes.map(a => `
<a href="${a.url}" class="mood-anime-card">
${a.cover ? `<img src="${escHtml(a.cover)}" alt="${escHtml(a.title)}">` : `<div style="width:100%;aspect-ratio:2/3;background:rgba(255,255,255,.05);display:flex;align-items:center;justify-content:center"><i class="bi bi-film" style="font-size:2rem;color:rgba(255,255,255,.15)"></i></div>`}
<div class="mac-t">${escHtml(a.title)}</div>
</a>`).join('');
} catch(e) {
grid.innerHTML = '<div style="grid-column:1/-1;color:rgba(255,80,80,.7);font-size:.82rem;text-align:center">Hata oluştu.</div>';
}
}
function closeMoodOverlay() {
const mo = document.getElementById('mood-overlay');
if (mo) mo.classList.remove('on');
}
// ── Ambiyans Işığı ───────────────────────────────────────────────────────────
(function(){
const pwMain = document.querySelector('.pw-main');
const ac = document.getElementById('ambi-canvas');
if (!ac || !pwMain) return;
const ctx = ac.getContext('2d', { willReadFrequently: false });
let raf = null, _last = 0;
// Varsayılan: açık. Kullanıcı kapatırsa '0' saklanır.
let enabled = localStorage.getItem('axe_ambi') !== '0';
function applyState() {
pwMain.classList.toggle('ambi-on', enabled);
const tog = document.getElementById('ambi-toggle');
if (tog) tog.classList.toggle('on', enabled);
if (enabled) start(); else stop();
}
function start() {
if (raf) return;
function tick(ts) {
if (!enabled) { raf = null; return; }
if (ts - _last > 80) {
_last = ts;
if (!vd.paused && !vd.ended && vd.readyState >= 2) {
try { ctx.drawImage(vd, 0, 0, 32, 18); } catch(e) {}
}
}
raf = requestAnimationFrame(tick);
}
raf = requestAnimationFrame(tick);
}
function stop() {
if (raf) { cancelAnimationFrame(raf); raf = null; }
ctx.clearRect(0, 0, 32, 18);
}
window.toggleAmbi = function() {
enabled = !enabled;
localStorage.setItem('axe_ambi', enabled ? '1' : '0');
applyState();
closeMenus();
};
applyState();
})();
// ── Sidebar tab switcher ──────────────────────────────────────────────────────
let sbLoaded = false;
function switchSideTab(tab) {
['eps','spoiler'].forEach(t => {
document.getElementById('tab-' + t)?.classList.toggle('on', t === tab);
document.getElementById('tc-' + t)?.classList.toggle('on', t === tab);
});
if (tab === 'spoiler' && !sbLoaded) { sbLoaded = true; loadSpoilerBoxes(); }
}
// ── Spoiler Kilitli Kutu ──────────────────────────────────────────────────────
async function loadSpoilerBoxes() {
const panel = document.getElementById('sb-panel');
try {
const r = await fetch(`/episode/${SOCIAL_EPISODE_ID}/spoiler-boxes`, {
headers: { 'Accept': 'application/json' },
});
const d = await r.json();
renderSpoilerBoxes(d.boxes || []);
} catch(e) {
panel.innerHTML = '<div style="text-align:center;padding:24px;color:rgba(255,80,80,.5);font-size:.8rem">Yüklenemedi.</div>';
}
}
function renderSpoilerBoxes(boxes) {
const panel = document.getElementById('sb-panel');
if (!boxes.length) {
panel.innerHTML = '<div style="text-align:center;padding:32px 12px;color:rgba(255,255,255,.25);font-size:.8rem"><i class="bi bi-lock" style="font-size:2rem;display:block;margin-bottom:8px"></i>Henüz spoiler kutusu yok.<br>İlk sen ekle!</div>';
return;
}
panel.innerHTML = boxes.map(b => `
<div class="sb-item ${b.is_spoiler ? 'is-spoiler' : ''}" id="sb-${b.id}">
${b.is_spoiler ? `<div class="sb-spoiler-mask" onclick="revealSpoiler(${b.id})"><i class="bi bi-exclamation-triangle"></i> Spoiler — görmek için tıkla</div>` : ''}
<div class="sb-body">${escHtml(b.body)}</div>
<div class="sb-meta">
<span>${escHtml(b.name || b.username || '?')}</span>
<span>${b.created_at}</span>
<button class="sb-like-btn ${b.liked ? 'on' : ''}" id="sblbtn-${b.id}" onclick="likeSpoilerBox(${b.id})">
<i class="bi bi-heart-fill"></i> <span id="sblcnt-${b.id}">${b.likes}</span>
</button>
</div>
</div>`).join('');
}
function revealSpoiler(id) {
const mask = document.querySelector(`#sb-${id} .sb-spoiler-mask`);
if (mask) mask.remove();
}
async function likeSpoilerBox(id) {
if (!IS_AUTH) { window.location.href = '{{ route('frontend.login') }}'; return; }
const r = await fetch(`/spoiler-box/${id}/like`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({}),
});
const d = await r.json();
document.getElementById('sblcnt-' + id).textContent = d.likes;
document.getElementById('sblbtn-' + id)?.classList.toggle('on', d.liked);
}
async function submitSpoilerBox() {
const inp = document.getElementById('sb-inp');
const body = inp?.value.trim();
if (!body || body.length < 3) return;
inp.disabled = true;
try {
const r = await fetch(`/episode/${SOCIAL_EPISODE_ID}/spoiler-boxes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': SOCIAL_CSRF, 'Accept': 'application/json' },
body: JSON.stringify({ body }),
});
const d = await r.json();
if (d.ok) {
inp.value = '';
loadSpoilerBoxes();
showToast(d.is_spoiler ? '⚠️ Spoiler içerik olarak işaretlendi.' : '✅ Yorum eklendi.');
} else {
showToast(d.error || 'Hata oluştu.');
}
} catch(e) {
showToast('Bağlantı hatası.');
} finally {
inp.disabled = false;
}
}
</script>
@endpush