5582 lines
271 KiB
PHP
5582 lines
271 KiB
PHP
@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ü viewport’a 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>
|
||
· {{ $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>
|
||
· {{ $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, tau-video, zappy-net vb. CORS izni vermediği için proxy'den geçmelidir.
|
||
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',
|
||
];
|
||
|
||
// Referer gereken (CORS kapalı) CDN'ler için sunucu proxy'si
|
||
const KNOWN_REFS = {
|
||
'aniziumserver.sbs': 'https://anizium.co/',
|
||
'aniziumserver.site': 'https://anizium.co/',
|
||
'aniziumserver.com': 'https://anizium.co/',
|
||
'tau-video.xyz': 'https://anizium.co/',
|
||
'zappy-net.store': 'https://anizium.co/',
|
||
'zappy-net.online': 'https://anizium.co/',
|
||
'pixel-quirk.shop': '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 {
|
||
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 proxy'ye sarılmışsa proxy'yi koru (CORS engeli almaması için)
|
||
if (wrapped) return url;
|
||
|
||
// Sarılmamış dış URL, referer gereken CDN → sunucu proxy'si
|
||
var ref = (Object.entries(KNOWN_REFS).find(function(e){ return host.endsWith(e[0]); }) || [null,'https://anizium.co/'])[1];
|
||
return '/stream/proxy?u=' + encodeURIComponent(btoa(real)) + '&ref=' + encodeURIComponent(btoa(ref));
|
||
} 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(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/ /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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
}
|
||
|
||
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,'"')})" 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
|