Files
2026-07-14 00:01:48 +03:00

489 lines
22 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('admin.layouts.app')
@section('title', 'Animeler')
@section('page-title', 'Animeler')
@push('styles')
<style>
.anime-cover {
width: 40px; height: 58px;
object-fit: cover;
border-radius: 6px;
background: var(--surface);
flex-shrink: 0;
}
.anime-cover-placeholder {
width: 40px; height: 58px;
border-radius: 6px;
background: var(--surface);
display: flex; align-items: center; justify-content: center;
color: var(--text3);
flex-shrink: 0;
font-size: 16px;
}
.anime-title { font-size: 13.5px; font-weight: 600; color: var(--text); }
.anime-genres { font-size: 12px; color: var(--text2); margin-top: 2px; }
.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; }
.bulk-bar-count { font-size: 13px; font-weight: 700; color: var(--accent-lt); }
.mal-progress-card {
display: none;
background: var(--bg2);
border: 1px solid var(--border2);
border-radius: var(--radius);
padding: 18px 20px;
margin-bottom: 16px;
}
.mal-progress-card.on { display: block; }
.mal-log {
font-size: 12px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--text2);
max-height: 130px;
overflow-y: auto;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
margin-top: 10px;
line-height: 1.7;
}
/* Action dropdown */
.act-drop { position: relative; }
.act-drop-menu {
display: none;
position: absolute;
right: 0; top: calc(100% + 4px);
min-width: 160px;
background: var(--bg2);
border: 1px solid var(--border2);
border-radius: var(--radius-sm);
padding: 5px;
z-index: 200;
box-shadow: var(--shadow-lg);
}
.act-drop-menu.on { display: block; }
.act-drop-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 11px;
border-radius: 6px;
font-size: 13px;
color: var(--text2);
cursor: pointer;
text-decoration: none;
border: none;
background: none;
width: 100%;
text-align: left;
transition: all 0.12s;
font-family: inherit;
}
.act-drop-item:hover { background: rgba(255,255,255,0.06); color: var(--text); }
.act-drop-item.danger:hover { background: var(--danger-dim); color: var(--danger); }
.act-drop-sep { height: 1px; background: var(--border); margin: 4px 0; }
</style>
@endpush
@section('content')
{{-- ── Page Header ── --}}
<div class="page-header">
<div>
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
Animeler
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($animes->total()) }} toplam</span>
</h5>
</div>
<div class="page-header-actions">
@if($zeroEpisodeCount > 0)
<button class="btn btn-outline-warning btn-sm" onclick="destroyZeroEpisodes()" id="btn-zero-ep">
<i class="bi bi-trash3"></i>
<span class="d-none d-md-inline">0 Bölümlü Sil</span>
<span class="badge bg-warning" style="color:#000">{{ $zeroEpisodeCount }}</span>
</button>
@endif
<button class="btn btn-secondary btn-sm" onclick="bulkFindMal()" id="btn-bulk-mal">
<i class="bi bi-magic"></i>
<span class="d-none d-md-inline">MAL ID Otomatik Bul</span>
</button>
<a href="{{ route('admin.animes.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> Anime Ekle
</a>
</div>
</div>
{{-- ── MAL Progress Card ── --}}
<div class="mal-progress-card" id="bulk-mal-box">
<div class="d-flex align-items-center justify-content-between mb-3">
<div style="font-size:14px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px">
<i class="bi bi-magic" style="color:var(--info)"></i> MAL ID Otomatik Doldurma
</div>
<button class="btn btn-sm btn-outline-danger" onclick="stopBulkMal()"><i class="bi bi-stop-fill"></i> Durdur</button>
</div>
<div style="background:var(--bg);border-radius:99px;height:6px;overflow:hidden">
<div id="bulk-mal-bar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent-lt));border-radius:99px;transition:width .3s"></div>
</div>
<div class="mal-log" id="bulk-mal-log"></div>
</div>
{{-- ── Filters ── --}}
<div class="filter-card mb-3">
<form method="GET" class="d-flex flex-wrap gap-2 align-items-end">
<div class="search-input-wrap flex-grow-1" style="min-width:180px;max-width:320px">
<i class="bi bi-search"></i>
<input type="text" name="search" class="form-control" placeholder="Anime 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="ongoing" {{ request('status')=='ongoing' ?'selected':'' }}>Devam Ediyor</option>
<option value="completed" {{ request('status')=='completed' ?'selected':'' }}>Tamamlandı</option>
<option value="upcoming" {{ request('status')=='upcoming' ?'selected':'' }}>Yakında</option>
</select>
<select name="type" class="form-select" style="width:auto;min-width:120px">
<option value="">Tüm Tipler</option>
<option value="series" {{ request('type')=='series'?'selected':'' }}>Dizi</option>
<option value="movie" {{ request('type')=='movie' ?'selected':'' }}>Film</option>
<option value="ova" {{ request('type')=='ova' ?'selected':'' }}>OVA</option>
</select>
<select name="no_episodes" class="form-select" style="width:auto;min-width:150px">
<option value="">Tüm Animeler</option>
<option value="1" {{ request('no_episodes')=='1'?'selected':'' }}>Sadece 0 Bölümlüler</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.animes.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
</div>
</form>
</div>
{{-- ── Bulk Selection Bar ── --}}
<div class="bulk-bar" id="bulk-bar">
<span class="bulk-bar-count" id="bulk-count">0 anime seçildi</span>
<span class="badge bg-info d-none" id="bulk-all-note" style="color:#000">Tüm sayfalardaki animeler</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ç ({{ $animes->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</th>
<th>Tip / Durum</th>
<th>MAL ID</th>
<th style="text-align:center">Bölüm</th>
<th style="text-align:center">Yayın</th>
<th style="text-align:right;padding-right:20px">İşlem</th>
</tr>
</thead>
<tbody>
@forelse($animes as $anime)
<tr id="anime-row-{{ $anime->id }}">
<td style="padding-left:20px">
<input type="checkbox" class="form-check-input chk-item" value="{{ $anime->id }}" onchange="onCheck()">
</td>
<td>
<div class="d-flex align-items-center gap-3">
@if($anime->coverUrl)
<img src="{{ $anime->coverUrl }}" class="anime-cover" alt="">
@else
<div class="anime-cover-placeholder"><i class="bi bi-image"></i></div>
@endif
<div class="overflow-hidden">
<div class="anime-title text-truncate" style="max-width:260px">{{ $anime->title }}</div>
<div class="anime-genres">{{ $anime->genres->pluck('name')->join(', ') ?: '—' }}</div>
</div>
</div>
</td>
<td>
<div class="d-flex flex-column gap-1">
<span class="badge bg-secondary" style="width:fit-content">{{ strtoupper($anime->type) }}</span>
@if($anime->status === 'ongoing')
<span class="badge bg-success" style="width:fit-content">Devam Ediyor</span>
@elseif($anime->status === 'completed')
<span class="badge bg-info" style="width:fit-content;color:#000">Tamamlandı</span>
@else
<span class="badge bg-warning" style="width:fit-content;color:#000">Yakında</span>
@endif
</div>
</td>
<td class="mal-id-cell" data-id="{{ $anime->id }}" data-fetch-url="{{ route('admin.animes.fetch-mal', $anime) }}">
@if($anime->mal_id)
<a href="https://myanimelist.net/anime/{{ $anime->mal_id }}" target="_blank"
class="badge bg-success text-decoration-none mal-badge-{{ $anime->id }}" style="color:#fff">
<i class="bi bi-box-arrow-up-right" style="font-size:10px;margin-right:3px"></i>{{ $anime->mal_id }}
</a>
@else
<span class="mal-badge-{{ $anime->id }}" style="display:inline-flex;align-items:center;gap:5px">
<span class="badge bg-danger">Eksik</span>
<button class="btn btn-link btn-sm p-0" style="font-size:12px;color:var(--info)" title="Otomatik Bul"
onclick="fetchSingleMal({{ $anime->id }}, this)">
<i class="bi bi-magic"></i>
</button>
</span>
@endif
</td>
<td style="text-align:center">
<span style="font-size:14px;font-weight:700;color:{{ $anime->episode_count > 0 ? 'var(--text)' : 'var(--danger)' }}">
{{ $anime->episode_count }}
</span>
</td>
<td style="text-align:center">
@if($anime->is_published)
<span style="display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:var(--success)">
<i class="bi bi-circle-fill" style="font-size:7px"></i> Yayında
</span>
@else
<span style="display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:var(--text3)">
<i class="bi bi-circle" style="font-size:7px"></i> Taslak
</span>
@endif
</td>
<td style="text-align:right;padding-right:20px">
<div class="act-drop">
<button class="btn btn-secondary btn-sm" onclick="toggleDrop(this)">
<i class="bi bi-three-dots"></i>
</button>
<div class="act-drop-menu">
<a href="{{ route('admin.animes.show', $anime) }}" class="act-drop-item">
<i class="bi bi-eye"></i> Görüntüle
</a>
<a href="{{ route('admin.animes.edit', $anime) }}" class="act-drop-item">
<i class="bi bi-pencil"></i> Düzenle
</a>
<a href="{{ route('admin.episodes.index', ['anime_id' => $anime->id]) }}" class="act-drop-item">
<i class="bi bi-collection-play"></i> Bölümler
</a>
<div class="act-drop-sep"></div>
<form method="POST" action="{{ route('admin.animes.destroy', $anime) }}"
onsubmit="return confirm('{{ addslashes($anime->title) }} silinsin mi?')">
@csrf @method('DELETE')
<button type="submit" class="act-drop-item danger">
<i class="bi bi-trash3"></i> Sil
</button>
</form>
</div>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7">
<div class="empty-state">
<i class="bi bi-play-circle"></i>
<h6>Anime bulunamadı</h6>
<p>Arama kriterlerinizi değiştirin veya yeni anime ekleyin.</p>
<a href="{{ route('admin.animes.create') }}" class="btn btn-primary btn-sm mt-2">
<i class="bi bi-plus-lg"></i> Anime Ekle
</a>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- ── Pagination ── --}}
<div class="mt-3">
{{ $animes->links('admin.partials.pagination') }}
</div>
@endsection
@push('scripts')
<script>
const BULK_MAL_URL = '{{ route("admin.animes.bulk-find-mal") }}';
const BULK_DESTROY_URL = '{{ route("admin.animes.bulk-destroy") }}';
const ZERO_DESTROY_URL = '{{ route("admin.animes.destroy-zero-episodes") }}';
const TOTAL_ANIMES = {{ $animes->total() }};
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
/* ── Action dropdown ── */
function toggleDrop(btn) {
const menu = btn.nextElementSibling;
const wasOn = menu.classList.contains('on');
document.querySelectorAll('.act-drop-menu.on').forEach(m => m.classList.remove('on'));
if (!wasOn) menu.classList.add('on');
}
document.addEventListener('click', e => {
if (!e.target.closest('.act-drop')) {
document.querySelectorAll('.act-drop-menu.on').forEach(m => m.classList.remove('on'));
}
});
/* ── Zero episode delete ── */
async function destroyZeroEpisodes() {
if (!confirm('0 bölümlü tüm animeler silinsin mi? Bu işlem geri alınamaz.')) return;
const btn = document.getElementById('btn-zero-ep');
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
try {
const res = await fetch(ZERO_DESTROY_URL, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Accept':'application/json'} });
const d = await res.json();
if (d.success) { axToast(`${d.count} anime silindi.`, 'success'); setTimeout(() => location.reload(), 1200); }
else { axToast('Hata oluştu.', 'error'); if (btn) btn.disabled = false; }
} catch(e) { axToast('Hata: ' + e.message, 'error'); if (btn) btn.disabled = false; }
}
/* ── Bulk selection ── */
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_ANIMES} anime seçildi`;
bar.classList.add('on'); _selBtns(false);
} else if (chk.length > 0) {
cnt.textContent = `${chk.length} anime 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_ANIMES} animeyi` : `${chk.length} animeyi`;
if (!confirm(`${label} silmek istediğinize emin misiniz?\nBu işlem CDN dosyalarını da siler!`)) return;
const params = new URLSearchParams(location.search);
const body = _selAll
? { all: true, filters: { search: params.get('search')||'', status: params.get('status')||'', type: params.get('type')||'' } }
: { 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(`anime-row-${id}`)?.remove());
clearSel();
axToast(`${chk.length} anime silindi.`, 'success');
} else { axToast('Hata: ' + (d.message || '?'), 'error'); }
} catch(e) { axToast('Hata: ' + e.message, 'error'); }
}
/* ── Bulk MAL find ── */
let bulkRunning = false, skipIds = [], totalDone = 0, totalInitial = 0;
async function bulkFindMal() {
if (bulkRunning) return;
bulkRunning = true; skipIds = []; totalDone = 0; totalInitial = 0;
document.getElementById('bulk-mal-box').classList.add('on');
document.getElementById('btn-bulk-mal').disabled = true;
document.getElementById('bulk-mal-log').innerHTML = '';
document.getElementById('bulk-mal-bar').style.width = '0%';
log('⏳ Başlatılıyor...');
await runNext();
}
async function runNext() {
if (!bulkRunning) return;
try {
const res = await fetch(BULK_MAL_URL, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Content-Type':'application/json','Accept':'application/json'}, body:JSON.stringify({skip_ids:skipIds}) });
const d = await res.json();
if (d.done) { log('✅ ' + d.message); finish(); return; }
totalDone++;
if (!totalInitial) totalInitial = totalDone + (d.remaining ?? 0);
const pct = totalInitial > 0 ? Math.round((totalDone / totalInitial) * 100) : 0;
document.getElementById('bulk-mal-bar').style.width = pct + '%';
if (d.found) log(`✓ <b>${esc(d.anime)}</b> → ${d.mal_id}`);
else { log(`<span style="opacity:.5">— ${esc(d.anime)}: bulunamadı</span>`); if (d.skipped_id) skipIds.push(d.skipped_id); }
setTimeout(runNext, 450);
} catch(e) { log(`<span style="color:var(--danger)">❌ ${e.message}</span>`); setTimeout(runNext, 2000); }
}
function stopBulkMal() { bulkRunning = false; log('⏹ Durduruldu.'); finish(); }
function finish() { bulkRunning = false; document.getElementById('btn-bulk-mal').disabled = false; document.getElementById('bulk-mal-bar').style.width = '100%'; }
function log(html) { const el = document.getElementById('bulk-mal-log'); el.innerHTML += html + '<br>'; el.scrollTop = el.scrollHeight; }
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
async function fetchSingleMal(animeId, btn) {
const cell = btn.closest('.mal-id-cell');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:12px;height:12px;border-width:1.5px"></span>';
try {
const res = await fetch(cell.dataset.fetchUrl, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Accept':'application/json'} });
const d = await res.json();
if (d.found) {
cell.innerHTML = `<a href="https://myanimelist.net/anime/${d.mal_id}" target="_blank" class="badge bg-success text-decoration-none" style="color:#fff"><i class="bi bi-box-arrow-up-right" style="font-size:10px;margin-right:3px"></i>${d.mal_id}</a>`;
} else {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-magic"></i>';
btn.title = 'Bulunamadı — tekrar dene';
}
} catch(e) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-exclamation-triangle" style="color:var(--danger)"></i>'; }
}
</script>
@endpush