Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Bölüm Ekle')
|
||||
@section('page-title', 'Bölüm Ekle')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.section-card { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); margin-bottom:16px; overflow:hidden; }
|
||||
.section-header { display:flex; align-items:center; gap:10px; padding:14px 20px; border-bottom:1px solid var(--border); }
|
||||
.section-header h6 { font-size:13.5px; font-weight:700; color:var(--text); margin:0; }
|
||||
.section-body { padding:20px; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('admin.episodes.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Bölümlere Dön
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.episodes.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8 col-lg-7">
|
||||
|
||||
{{-- Bölüm Bilgileri --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-collection-play" style="color:var(--accent-lt)"></i>
|
||||
<h6>Bölüm Bilgileri</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Anime <span style="color:var(--danger)">*</span></label>
|
||||
<select name="anime_id" class="form-select" id="animeSelect" required onchange="loadSeasons(this.value)">
|
||||
<option value="">— Anime Seç —</option>
|
||||
@foreach($animes as $anime)
|
||||
<option value="{{ $anime->id }}"
|
||||
{{ (request('anime_id')==$anime->id || old('anime_id')==$anime->id) ? 'selected' : '' }}>
|
||||
{{ $anime->title }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Sezon <span style="color:var(--danger)">*</span></label>
|
||||
<select name="season_id" class="form-select" id="seasonSelect" required>
|
||||
<option value="">— Sezon Seç —</option>
|
||||
@foreach($seasons as $season)
|
||||
<option value="{{ $season->id }}"
|
||||
{{ (request('season_id')==$season->id || old('season_id')==$season->id) ? 'selected' : '' }}>
|
||||
{{ $season->season_number }}. Sezon
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Bölüm No <span style="color:var(--danger)">*</span></label>
|
||||
<input type="number" name="episode_number" class="form-control"
|
||||
value="{{ old('episode_number', 1) }}" min="1" required>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Başlık <span style="color:var(--text2);font-size:11px">(opsiyonel)</span></label>
|
||||
<input type="text" name="title" class="form-control"
|
||||
value="{{ old('title') }}" placeholder="Bölüm adı...">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Süre (saniye)</label>
|
||||
<input type="number" name="duration" class="form-control"
|
||||
value="{{ old('duration') }}" placeholder="1440">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Açıklama</label>
|
||||
<textarea name="description" class="form-control" rows="3"
|
||||
placeholder="Bölüm özeti...">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Video Kaynağı --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-play-btn" style="color:var(--info)"></i>
|
||||
<h6>Video Kaynağı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Kaynak Tipi</label>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
@foreach(['bunnycdn'=>'BunnyCDN','external'=>'External URL','direct'=>'Direct M3U8'] as $val => $lbl)
|
||||
<label style="display:flex;align-items:center;gap:7px;padding:8px 14px;border-radius:8px;border:1px solid var(--border2);cursor:pointer;transition:all .12s;font-size:13px;font-weight:600;color:var(--text2)" class="source-opt">
|
||||
<input type="radio" name="source" value="{{ $val }}"
|
||||
{{ old('source','bunnycdn')===$val?'checked':'' }}
|
||||
style="accent-color:var(--accent)" onchange="toggleSource('{{ $val }}')">
|
||||
{{ $lbl }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div id="sourceUrlField">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">M3U8 / Video URL</label>
|
||||
<input type="text" name="source_url" class="form-control"
|
||||
value="{{ old('source_url') }}"
|
||||
placeholder="https://f.aniziumserver.sbs/.../master.m3u8">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Video URL (Pull CDN)</label>
|
||||
<input type="text" name="video_url" class="form-control" value="{{ old('video_url') }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">M3U8 URL</label>
|
||||
<input type="text" name="m3u8_url" class="form-control" value="{{ old('m3u8_url') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İzinler --}}
|
||||
@if(isset($permissions) && count($permissions))
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--success)"></i>
|
||||
<h6>İzin Ayarları</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— Boş bırakınca anime/global ayarları geçerli olur</span>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
@foreach($permissions as $perm)
|
||||
<div class="perm-row">
|
||||
<div>
|
||||
<div class="perm-name">{{ $perm->label }}</div>
|
||||
</div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="ep_{{ $perm->key }}_free" value="free" checked>
|
||||
<label for="ep_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="ep_{{ $perm->key }}_prem" value="premium">
|
||||
<label for="ep_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Sağ --}}
|
||||
<div class="col-xl-4 col-lg-5">
|
||||
|
||||
{{-- Thumbnail --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-image" style="color:var(--info)"></i>
|
||||
<h6>Thumbnail</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<input type="file" name="thumbnail" class="form-control" accept="image/*">
|
||||
<div class="form-text">Önerilen: 1280×720px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yayın --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-toggle-on" style="color:var(--success)"></i>
|
||||
<h6>Yayın Ayarı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Yayınla</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Kullanıcılara görünür yap</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_published"
|
||||
value="1" id="is_published" role="switch">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg">
|
||||
<i class="bi bi-plus-circle"></i> Bölüm Ekle
|
||||
</button>
|
||||
<a href="{{ route('admin.episodes.index') }}" class="btn btn-secondary w-100 mt-2">İptal</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function loadSeasons(animeId) {
|
||||
if (!animeId) return;
|
||||
fetch('/admin/animes/' + animeId + '/seasons-json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const sel = document.getElementById('seasonSelect');
|
||||
sel.innerHTML = '<option value="">— Sezon Seç —</option>';
|
||||
data.forEach(s => {
|
||||
sel.innerHTML += `<option value="${s.id}">${s.season_number}. Sezon</option>`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSource(val) {
|
||||
// Seçili source opt'u vurgula
|
||||
document.querySelectorAll('.source-opt').forEach(el => {
|
||||
const inp = el.querySelector('input');
|
||||
el.style.borderColor = inp.value === val ? 'var(--accent)' : 'var(--border2)';
|
||||
el.style.background = inp.value === val ? 'var(--accent-dim)' : '';
|
||||
el.style.color = inp.value === val ? 'var(--accent-lt)' : 'var(--text2)';
|
||||
});
|
||||
}
|
||||
|
||||
// Init on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const checked = document.querySelector('[name="source"]:checked');
|
||||
if (checked) toggleSource(checked.value);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,371 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Bölüm Düzenle')
|
||||
@section('page-title', 'Bölüm Düzenle')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.section-card { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); margin-bottom:16px; overflow:hidden; }
|
||||
.section-header { display:flex; align-items:center; gap:10px; padding:14px 20px; border-bottom:1px solid var(--border); }
|
||||
.section-header h6 { font-size:13.5px; font-weight:700; color:var(--text); margin:0; }
|
||||
.section-body { padding:20px; }
|
||||
.stat-mini { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; }
|
||||
.stat-mini-row { display:flex; justify-content:space-between; align-items:center; padding:10px 16px; border-bottom:1px solid var(--border); font-size:13px; }
|
||||
.stat-mini-row:last-child { border-bottom:none; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||||
<a href="{{ route('admin.episodes.index', ['anime_id'=>$episode->anime_id]) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Bölümlere Dön
|
||||
</a>
|
||||
<div style="font-size:13px;color:var(--text2)">
|
||||
Son güncelleme: {{ $episode->updated_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.episodes.update', $episode) }}" enctype="multipart/form-data">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8 col-lg-7">
|
||||
|
||||
{{-- Bölüm Bilgileri --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-collection-play" style="color:var(--accent-lt)"></i>
|
||||
<h6>Bölüm Bilgileri</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Anime</label>
|
||||
<select name="anime_id" class="form-select" required>
|
||||
@foreach($animes as $anime)
|
||||
<option value="{{ $anime->id }}" {{ $episode->anime_id==$anime->id?'selected':'' }}>
|
||||
{{ $anime->title }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Sezon</label>
|
||||
<select name="season_id" class="form-select" required>
|
||||
@foreach($seasons as $season)
|
||||
<option value="{{ $season->id }}" {{ $episode->season_id==$season->id?'selected':'' }}>
|
||||
{{ $season->season_number }}. Sezon
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Bölüm No</label>
|
||||
<input type="number" name="episode_number" class="form-control"
|
||||
value="{{ old('episode_number',$episode->episode_number) }}" required>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Başlık</label>
|
||||
<input type="text" name="title" class="form-control"
|
||||
value="{{ old('title',$episode->title) }}" placeholder="Opsiyonel">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Süre (saniye)</label>
|
||||
<input type="number" name="duration" class="form-control"
|
||||
value="{{ old('duration',$episode->duration) }}">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">Açıklama</label>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="ai-ep-btn" onclick="aiGenerateEpisode()">
|
||||
<i class="bi bi-stars" style="color:var(--info)"></i> AI ile Yaz
|
||||
</button>
|
||||
</div>
|
||||
<textarea name="description" id="ep-description" class="form-control" rows="3">{{ old('description',$episode->description) }}</textarea>
|
||||
<div id="ai-ep-status" class="form-text" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İntro Ayarları --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-skip-end-fill" style="color:var(--warning)"></i>
|
||||
<h6>İntro / Outro Zaman Ayarı</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— "İntroyu Atla" butonu için</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">İntro Başlangıç (sn)</label>
|
||||
<input type="number" name="intro_start" class="form-control" min="0"
|
||||
value="{{ old('intro_start',$episode->intro_start) }}" placeholder="örn: 5">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">İntro Bitiş (sn)</label>
|
||||
<input type="number" name="intro_end" class="form-control" min="0"
|
||||
value="{{ old('intro_end',$episode->intro_end) }}" placeholder="örn: 95">
|
||||
</div>
|
||||
</div>
|
||||
@if($episode->intro_start && $episode->intro_end)
|
||||
<div class="mt-2 p-2 rounded" style="background:var(--success-dim);border:1px solid rgba(16,185,129,.2);font-size:12.5px;color:var(--success)">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>
|
||||
Mevcut: {{ $episode->intro_start }}sn — {{ $episode->intro_end }}sn
|
||||
({{ $episode->intro_end - $episode->intro_start }}sn süre)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Video Kaynağı --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-play-btn" style="color:var(--info)"></i>
|
||||
<h6>Video Kaynağı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Kaynak Tipi</label>
|
||||
<select name="source" class="form-select">
|
||||
@foreach(['bunnycdn'=>'BunnyCDN','external'=>'External URL','direct'=>'Direct M3U8'] as $v => $l)
|
||||
<option value="{{ $v }}" {{ old('source',$episode->source)===$v?'selected':'' }}>{{ $l }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Source URL</label>
|
||||
<input type="text" name="source_url" class="form-control"
|
||||
value="{{ old('source_url',$episode->source_url) }}"
|
||||
placeholder="https://...master.m3u8">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Video URL (CDN Pull)</label>
|
||||
<input type="text" name="video_url" class="form-control"
|
||||
value="{{ old('video_url',$episode->video_url) }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">M3U8 URL</label>
|
||||
<input type="text" name="m3u8_url" class="form-control"
|
||||
value="{{ old('m3u8_url',$episode->m3u8_url) }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İzinler --}}
|
||||
@if(isset($permissions) && count($permissions))
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--success)"></i>
|
||||
<h6>İzin Ayarları</h6>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
@foreach($permissions as $perm)
|
||||
@php $current = $contentPerms[$perm->key] ?? 'free'; @endphp
|
||||
<div class="perm-row">
|
||||
<div><div class="perm-name">{{ $perm->label }}</div></div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ep2_{{ $perm->key }}_free" value="free" {{ $current==='free'?'checked':'' }}>
|
||||
<label for="ep2_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ep2_{{ $perm->key }}_prem" value="premium" {{ $current==='premium'?'checked':'' }}>
|
||||
<label for="ep2_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Sağ --}}
|
||||
<div class="col-xl-4 col-lg-5">
|
||||
|
||||
{{-- İstatistikler --}}
|
||||
<div class="section-card mb-3">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-bar-chart" style="color:var(--info)"></i>
|
||||
<h6>Bölüm İstatistikleri</h6>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">İzlenme</span>
|
||||
<span style="font-weight:700;color:var(--text)">{{ number_format($episode->view_count) }}</span>
|
||||
</div>
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">Süre</span>
|
||||
<span style="font-weight:600;color:var(--text)">{{ $episode->duration_formatted ?? ($episode->duration ? floor($episode->duration/60).'dk' : '—') }}</span>
|
||||
</div>
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">Kaynak</span>
|
||||
<span class="badge bg-secondary">{{ strtoupper($episode->source) }}</span>
|
||||
</div>
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">Eklenme</span>
|
||||
<span style="font-size:12px;color:var(--text)">{{ $episode->created_at->format('d.m.Y H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Thumbnail --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-image" style="color:var(--info)"></i>
|
||||
<h6>Thumbnail</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
@if($episode->thumbnailUrl)
|
||||
<img src="{{ $episode->thumbnailUrl }}" alt=""
|
||||
style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius-sm);margin-bottom:12px">
|
||||
@endif
|
||||
<input type="file" name="thumbnail" class="form-control" accept="image/*">
|
||||
<div class="form-text">Önerilen: 1280×720px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yayın --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-toggle-on" style="color:var(--success)"></i>
|
||||
<h6>Yayın Ayarı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Yayınla</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Kullanıcılara görünür yap</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_published" value="1"
|
||||
id="is_published" role="switch" {{ $episode->is_published?'checked':'' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg">
|
||||
<i class="bi bi-floppy"></i> Değişiklikleri Kaydet
|
||||
</button>
|
||||
<a href="{{ route('admin.episodes.index', ['anime_id'=>$episode->anime_id]) }}"
|
||||
class="btn btn-secondary w-100 mt-2">İptal</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Video Kaynakları + HEVC Tarama --}}
|
||||
@php
|
||||
$videoSources = \App\Models\VideoSource::where('episode_id', $episode->id)->orderBy('sort_order')->get();
|
||||
@endphp
|
||||
@if($videoSources->count())
|
||||
<div class="section-card mt-4">
|
||||
<div class="section-header d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-collection-play" style="color:var(--warning)"></i>
|
||||
<h6 class="mb-0">Video Kaynakları ({{ $videoSources->count() }})</h6>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" id="hevc-scan-btn"
|
||||
onclick="scanHevc({{ $episode->id }})">
|
||||
<i class="bi bi-search"></i> HEVC Tara
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
<div id="hevc-sources-list">
|
||||
@foreach($videoSources as $src)
|
||||
<div class="stat-mini-row" id="src-row-{{ $src->id }}">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary" style="font-size:10px">{{ strtoupper($src->type) }}</span>
|
||||
<span style="font-size:13px;color:var(--text)">{{ $src->label ?: '—' }}</span>
|
||||
@if($src->quality)
|
||||
<span style="font-size:11px;color:var(--text2)">{{ $src->quality }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if($src->is_hevc)
|
||||
<span class="badge bg-danger" title="H.265 (HEVC) — Chrome'da oynatılamaz">H.265</span>
|
||||
@elseif($src->hevc_checked_at)
|
||||
<span class="badge bg-success" title="Tarandı: H.264 (uyumlu)">H.264 ✓</span>
|
||||
@else
|
||||
<span class="badge bg-secondary" id="hevc-badge-{{ $src->id }}">Taranmadı</span>
|
||||
@endif
|
||||
<a href="{{ $src->url }}" target="_blank" class="btn btn-xs btn-outline-secondary" style="font-size:11px;padding:2px 6px">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div id="hevc-scan-status" class="px-3 py-2" style="display:none;font-size:12px;color:var(--text2)"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
async function scanHevc(episodeId) {
|
||||
const btn = document.getElementById('hevc-scan-btn');
|
||||
const status = document.getElementById('hevc-scan-status');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:11px;height:11px"></span> Taranıyor...';
|
||||
status.style.display = 'block';
|
||||
status.textContent = 'Kaynaklar sunucu tarafında taranıyor, lütfen bekleyin...';
|
||||
try {
|
||||
const res = await fetch(`/admin/episodes/${episodeId}/scan-hevc`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content || '' },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) throw new Error('Sunucu hatası');
|
||||
data.results.forEach(r => {
|
||||
const badge = document.getElementById(`hevc-badge-${r.id}`);
|
||||
if (badge) {
|
||||
badge.className = r.is_hevc ? 'badge bg-danger' : 'badge bg-success';
|
||||
badge.textContent = r.is_hevc ? 'H.265 ⚠' : 'H.264 ✓';
|
||||
badge.title = r.is_hevc ? 'HEVC — Chrome desteklemiyor' : 'H.264 uyumlu';
|
||||
badge.id = ''; // artık dinamik badge değil, stabil
|
||||
}
|
||||
});
|
||||
const hevcCount = data.results.filter(r => r.is_hevc).length;
|
||||
status.style.color = hevcCount > 0 ? 'var(--danger)' : 'var(--success)';
|
||||
status.textContent = hevcCount > 0
|
||||
? `⚠ ${hevcCount} kaynak H.265 (HEVC) — Chrome'da açılmaz!`
|
||||
: '✓ Tüm kaynaklar H.264 uyumlu.';
|
||||
} catch(e) {
|
||||
status.style.color = 'var(--danger)';
|
||||
status.textContent = '❌ Tarama başarısız: ' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-search"></i> HEVC Tara';
|
||||
}
|
||||
}
|
||||
|
||||
async function aiGenerateEpisode() {
|
||||
const btn = document.getElementById('ai-ep-btn');
|
||||
const status = document.getElementById('ai-ep-status');
|
||||
const ta = document.getElementById('ep-description');
|
||||
const animeSelect = document.querySelector('[name="anime_id"]');
|
||||
const animeTitle = animeSelect?.options[animeSelect.selectedIndex]?.text?.trim() || '';
|
||||
const episodeNumber = document.querySelector('[name="episode_number"]')?.value?.trim();
|
||||
const episodeTitle = document.querySelector('[name="title"]')?.value?.trim();
|
||||
if (!animeTitle) { axToast('Anime seçili değil.', 'warning'); return; }
|
||||
|
||||
btn.disabled=true; btn.innerHTML='<span class="spinner-border spinner-border-sm" style="width:12px;height:12px"></span>';
|
||||
status.style.display='block'; status.style.color='var(--info)'; status.textContent='AI açıklama oluşturuyor...';
|
||||
try {
|
||||
const res = await fetch('{{ route("admin.ai.generate") }}', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':document.querySelector('meta[name=csrf-token]')?.content||''},
|
||||
body:JSON.stringify({ type:'episode', title:animeTitle, anime_title:animeTitle, episode_number:episodeNumber, episode_title:episodeTitle })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) { status.style.color='var(--danger)'; status.textContent='❌ '+data.error; }
|
||||
else { ta.value=data.description; status.style.color='var(--success)'; status.textContent='✓ Açıklama oluşturuldu.'; }
|
||||
} catch(e) { status.style.color='var(--danger)'; status.textContent='❌ Hata.'; }
|
||||
finally { btn.disabled=false; btn.innerHTML='<i class="bi bi-stars" style="color:var(--info)"></i> AI ile Yaz'; }
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,319 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Bölümler')
|
||||
@section('page-title', 'Bölümler')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.ep-anime { font-size:12.5px; font-weight:700; color:var(--text); }
|
||||
.ep-code { font-size:12px; font-weight:700; color:var(--accent-lt); font-family:monospace; background:var(--accent-dim); padding:2px 7px; border-radius:5px; }
|
||||
.ep-title { font-size:12.5px; color:var(--text2); }
|
||||
|
||||
.bulk-bar { display:none; align-items:center; gap:10px; padding:10px 16px; background:var(--accent-dim); border:1px solid rgba(124,58,237,0.3); border-radius:var(--radius-sm); margin-bottom:12px; flex-wrap:wrap; }
|
||||
.bulk-bar.on { display:flex; }
|
||||
|
||||
.intro-card { display:none; background:var(--bg2); border:1px solid var(--border2); border-radius:var(--radius); padding:20px; margin-bottom:16px; }
|
||||
.intro-card.on { display:block; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- ── Page Header ── --}}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
|
||||
Bölümler
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($episodes->total()) }} toplam</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('intro-card').classList.toggle('on')">
|
||||
<i class="bi bi-skip-end-fill"></i>
|
||||
<span class="d-none d-md-inline">Toplu İntro Ayarı</span>
|
||||
</button>
|
||||
<a href="{{ route('admin.episodes.create') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> Bölüm Ekle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Toplu İntro Card ── --}}
|
||||
<div class="intro-card" id="intro-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px">
|
||||
<i class="bi bi-skip-end-fill" style="color:var(--info)"></i> Toplu İntro Zaman Ayarı
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary" onclick="document.getElementById('intro-card').classList.remove('on')">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Anime</label>
|
||||
<select id="bi-anime" class="form-select">
|
||||
<option value="">— Anime seç —</option>
|
||||
@foreach($animes as $a)
|
||||
<option value="{{ $a->id }}">{{ $a->title }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Sezon <span style="color:var(--text3);font-size:11px">(0 = hepsi)</span></label>
|
||||
<input type="number" id="bi-season" class="form-control" value="0" min="0">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">İntro Başlangıç (sn)</label>
|
||||
<input type="number" id="bi-start" class="form-control" placeholder="örn: 5" min="0">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">İntro Bitiş (sn)</label>
|
||||
<input type="number" id="bi-end" class="form-control" placeholder="örn: 95" min="0">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button class="btn btn-info btn-sm flex-grow-1" onclick="doBulkIntro()">
|
||||
<i class="bi bi-check-lg"></i> Uygula
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bi-result" class="mt-3" style="font-size:13px"></div>
|
||||
</div>
|
||||
|
||||
{{-- ── Filters ── --}}
|
||||
<div class="filter-card mb-3">
|
||||
<form method="GET" class="d-flex flex-wrap gap-2 align-items-end">
|
||||
<div class="flex-grow-1" style="min-width:180px;max-width:280px">
|
||||
<select name="anime_id" class="form-select">
|
||||
<option value="">Tüm Animeler</option>
|
||||
@foreach($animes as $anime)
|
||||
<option value="{{ $anime->id }}" {{ request('anime_id') == $anime->id ? 'selected' : '' }}>{{ $anime->title }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-input-wrap flex-grow-1" style="min-width:150px;max-width:240px">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="search" class="form-control" placeholder="Başlık ara..." value="{{ request('search') }}">
|
||||
</div>
|
||||
<select name="status" class="form-select" style="width:auto;min-width:140px">
|
||||
<option value="">Tüm Durumlar</option>
|
||||
<option value="published" {{ request('status')=='published' ?'selected':'' }}>Yayında</option>
|
||||
<option value="pending" {{ request('status')=='pending' ?'selected':'' }}>Bekliyor</option>
|
||||
<option value="processing" {{ request('status')=='processing' ?'selected':'' }}>İşleniyor</option>
|
||||
<option value="failed" {{ request('status')=='failed' ?'selected':'' }}>Hatalı</option>
|
||||
</select>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel"></i> Filtrele</button>
|
||||
<a href="{{ route('admin.episodes.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ── Bulk Bar ── --}}
|
||||
<div class="bulk-bar" id="bulk-bar">
|
||||
<span style="font-size:13px;font-weight:700;color:var(--accent-lt)" id="bulk-count">0 bölüm seçildi</span>
|
||||
<span class="badge bg-info d-none" id="bulk-all-note" style="color:#000">Tüm sayfalardaki bölümler</span>
|
||||
<div style="margin-left:auto;display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-sm btn-secondary d-none" id="btn-sel-page" onclick="selPage()"><i class="bi bi-check2-square"></i> Sayfayı Seç</button>
|
||||
<button class="btn btn-sm btn-secondary d-none" id="btn-sel-all" onclick="selAllPages()"><i class="bi bi-check2-all"></i> Tümünü Seç ({{ $episodes->total() }})</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="bulkDelete()"><i class="bi bi-trash3"></i> Seçilenleri Sil</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="clearSel()"><i class="bi bi-x-lg"></i> İptal</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Table ── --}}
|
||||
<div class="table-wrap">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:44px;padding-left:20px">
|
||||
<input type="checkbox" class="form-check-input" id="chk-page" onchange="togglePage(this.checked)">
|
||||
</th>
|
||||
<th>Anime & Bölüm</th>
|
||||
<th>Kaynak</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th style="text-align:center">İzlenme</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($episodes as $ep)
|
||||
<tr id="ep-row-{{ $ep->id }}">
|
||||
<td style="padding-left:20px">
|
||||
<input type="checkbox" class="form-check-input chk-item" value="{{ $ep->id }}" onchange="onCheck()">
|
||||
</td>
|
||||
<td>
|
||||
<div class="ep-anime mb-1">{{ $ep->anime->title ?? '—' }}</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="ep-code">S{{ str_pad($ep->season->season_number ?? 0, 2, '0', STR_PAD_LEFT) }}E{{ str_pad($ep->episode_number, 2, '0', STR_PAD_LEFT) }}</span>
|
||||
@if($ep->title)
|
||||
<span class="ep-title">{{ Str::limit($ep->title, 45) }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ strtoupper($ep->source) }}</span>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@php
|
||||
$sBadge = match($ep->status) {
|
||||
'published' => ['bg-success', 'Yayında', ''],
|
||||
'processing' => ['bg-info', 'İşleniyor', 'color:#000'],
|
||||
'failed' => ['bg-danger', 'Hatalı', ''],
|
||||
default => ['bg-secondary','Bekliyor', ''],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $sBadge[0] }}" style="{{ $sBadge[2] }}">{{ $sBadge[1] }}</span>
|
||||
</td>
|
||||
<td style="text-align:center;font-size:13px;font-weight:600;color:var(--text2)">
|
||||
{{ number_format($ep->view_count) }}
|
||||
</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
<a href="{{ route('admin.episodes.edit', $ep) }}" class="btn btn-sm btn-secondary" title="Düzenle">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.episodes.destroy', $ep) }}" onsubmit="return confirm('Bu bölümü silmek istediğinize emin misiniz?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Sil">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-collection-play"></i>
|
||||
<h6>Bölüm bulunamadı</h6>
|
||||
<p>Filtrelerinizi değiştirin veya yeni bölüm ekleyin.</p>
|
||||
<a href="{{ route('admin.episodes.create') }}" class="btn btn-primary btn-sm mt-2">
|
||||
<i class="bi bi-plus-lg"></i> Bölüm Ekle
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $episodes->links('admin.partials.pagination') }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const BULK_DESTROY_URL = '{{ route("admin.episodes.bulk-destroy") }}';
|
||||
const TOTAL_EPS = {{ $episodes->total() }};
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
|
||||
let _selAll = false;
|
||||
|
||||
function _selBtns(show) {
|
||||
document.getElementById('btn-sel-page').classList.toggle('d-none', !show);
|
||||
document.getElementById('btn-sel-all').classList.toggle('d-none', !show);
|
||||
}
|
||||
function selPage() {
|
||||
document.querySelectorAll('.chk-item').forEach(c => c.checked = true);
|
||||
document.getElementById('chk-page').checked = true;
|
||||
_selAll = false;
|
||||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||||
onCheck();
|
||||
}
|
||||
function selAllPages() {
|
||||
selPage(); _selAll = true;
|
||||
document.getElementById('bulk-all-note').classList.remove('d-none');
|
||||
updateBar();
|
||||
}
|
||||
function clearSel() {
|
||||
document.querySelectorAll('.chk-item').forEach(c => c.checked = false);
|
||||
document.getElementById('chk-page').checked = false;
|
||||
_selAll = false;
|
||||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||||
updateBar();
|
||||
}
|
||||
function togglePage(checked) {
|
||||
document.querySelectorAll('.chk-item').forEach(c => c.checked = checked);
|
||||
_selAll = false;
|
||||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||||
onCheck();
|
||||
}
|
||||
function onCheck() {
|
||||
const all = document.querySelectorAll('.chk-item');
|
||||
const chk = document.querySelectorAll('.chk-item:checked');
|
||||
document.getElementById('chk-page').checked = all.length > 0 && all.length === chk.length;
|
||||
_selAll = false;
|
||||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||||
updateBar();
|
||||
}
|
||||
function updateBar() {
|
||||
const chk = document.querySelectorAll('.chk-item:checked');
|
||||
const bar = document.getElementById('bulk-bar');
|
||||
const cnt = document.getElementById('bulk-count');
|
||||
if (_selAll) {
|
||||
cnt.textContent = `Tüm ${TOTAL_EPS} bölüm seçildi`;
|
||||
bar.classList.add('on'); _selBtns(false);
|
||||
} else if (chk.length > 0) {
|
||||
cnt.textContent = `${chk.length} bölüm seçildi`;
|
||||
bar.classList.add('on'); _selBtns(true);
|
||||
} else {
|
||||
bar.classList.remove('on'); _selBtns(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDelete() {
|
||||
const chk = [...document.querySelectorAll('.chk-item:checked')].map(c => c.value);
|
||||
const label = _selAll ? `tüm ${TOTAL_EPS} bölümü` : `${chk.length} bölümü`;
|
||||
if (!confirm(`${label} silmek istediğinize emin misiniz? CDN dosyaları da silinecek!`)) return;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const body = _selAll
|
||||
? { all:true, filters:{ anime_id:params.get('anime_id')||'', status:params.get('status')||'', search:params.get('search')||'' } }
|
||||
: { ids:chk };
|
||||
try {
|
||||
const res = await fetch(BULK_DESTROY_URL, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Content-Type':'application/json','Accept':'application/json'}, body:JSON.stringify(body) });
|
||||
const d = await res.json();
|
||||
if (d.success) {
|
||||
if (_selAll) { location.reload(); return; }
|
||||
chk.forEach(id => document.getElementById(`ep-row-${id}`)?.remove());
|
||||
clearSel();
|
||||
axToast(`${chk.length} bölüm silindi.`, 'success');
|
||||
} else { axToast('Hata: ' + (d.message || '?'), 'error'); }
|
||||
} catch(e) { axToast('Hata: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function doBulkIntro() {
|
||||
const animeId = document.getElementById('bi-anime').value;
|
||||
const season = document.getElementById('bi-season').value;
|
||||
const start = document.getElementById('bi-start').value;
|
||||
const end = document.getElementById('bi-end').value;
|
||||
const result = document.getElementById('bi-result');
|
||||
|
||||
if (!animeId) { result.innerHTML = '<span style="color:var(--warning)">Anime seçiniz.</span>'; return; }
|
||||
if (!start || !end) { result.innerHTML = '<span style="color:var(--warning)">Başlangıç ve bitiş saniyelerini girin.</span>'; return; }
|
||||
if (parseInt(end) <= parseInt(start)) { result.innerHTML = '<span style="color:var(--warning)">Bitiş saniyesi başlangıçtan büyük olmalı.</span>'; return; }
|
||||
|
||||
result.innerHTML = '<span style="color:var(--text2)"><span class="spinner-border spinner-border-sm me-1" style="width:12px;height:12px"></span> Uygulanıyor...</span>';
|
||||
try {
|
||||
const res = await fetch('{{ route("admin.episodes.bulk-intro") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ anime_id: animeId, season: season, intro_start: start, intro_end: end }),
|
||||
});
|
||||
const d = await res.json();
|
||||
if (d.ok) {
|
||||
result.innerHTML = `<span style="color:var(--success)"><i class="bi bi-check-circle-fill me-1"></i>${d.updated} bölüm güncellendi.</span>`;
|
||||
axToast(`${d.updated} bölüm intro zamanları güncellendi.`, 'success');
|
||||
} else {
|
||||
result.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle me-1"></i>${d.error}</span>`;
|
||||
}
|
||||
} catch(e) {
|
||||
result.innerHTML = '<span style="color:var(--danger)">Bağlantı hatası.</span>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user