Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user