Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
@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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user