Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -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