wip implementation of local library on anime
This commit is contained in:
209
desktop/src/scripts/local-library.js
Normal file
209
desktop/src/scripts/local-library.js
Normal file
@@ -0,0 +1,209 @@
|
||||
let activeFilter = 'all';
|
||||
let activeSort = 'az';
|
||||
let isLocalMode = false;
|
||||
let localEntries = [];
|
||||
|
||||
function toggleLibraryMode() {
|
||||
isLocalMode = !isLocalMode;
|
||||
|
||||
const btn = document.getElementById('library-mode-btn');
|
||||
const onlineContent = document.getElementById('online-content');
|
||||
const localContent = document.getElementById('local-content');
|
||||
const svg = btn.querySelector('svg');
|
||||
const label = btn.querySelector('span');
|
||||
|
||||
if (isLocalMode) {
|
||||
// LOCAL MODE
|
||||
btn.classList.add('active');
|
||||
onlineContent.classList.add('hidden');
|
||||
localContent.classList.remove('hidden');
|
||||
loadLocalEntries();
|
||||
|
||||
svg.innerHTML = `
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
`;
|
||||
} else {
|
||||
// ONLINE MODE
|
||||
btn.classList.remove('active');
|
||||
onlineContent.classList.remove('hidden');
|
||||
localContent.classList.add('hidden');
|
||||
|
||||
svg.innerHTML = `
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocalEntries() {
|
||||
const grid = document.getElementById('local-entries-grid');
|
||||
grid.innerHTML = '<div class="skeleton-card"></div>'.repeat(8);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/library/anime');
|
||||
const entries = await response.json();
|
||||
localEntries = entries;
|
||||
|
||||
if (entries.length === 0) {
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-text-secondary); padding: 3rem;">No anime found in your local library. Click "Scan Library" to scan your folders.</p>';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Renderizar grid
|
||||
grid.innerHTML = entries.map(entry => {
|
||||
const title = entry.metadata?.title?.romaji || entry.metadata?.title?.english || entry.id;
|
||||
const cover = entry.metadata?.coverImage?.extraLarge || entry.metadata?.coverImage?.large || '/public/assets/placeholder.jpg';
|
||||
const score = entry.metadata?.averageScore || '--';
|
||||
const episodes = entry.metadata?.episodes || '??';
|
||||
|
||||
return `
|
||||
<div class="local-card" onclick="viewLocalEntry(${entry.metadata?.id || 'null'})">
|
||||
<div class="card-img-wrap">
|
||||
<img src="${cover}" alt="${title}" loading="lazy">
|
||||
</div>
|
||||
<div class="local-card-info">
|
||||
<div class="local-card-title">${title}</div>
|
||||
<p style="font-size: 0.85rem; color: var(--color-text-secondary); margin: 0;">
|
||||
${score}% • ${episodes} Eps
|
||||
</p>
|
||||
<div class="match-status ${entry.matched ? 'status-linked' : 'status-unlinked'}">
|
||||
${entry.matched ? '● Linked' : '○ Unlinked'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
console.error('Error loading local entries:', err);
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-danger); padding: 3rem;">Error loading local library. Make sure the backend is running.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function scanLocalLibrary() {
|
||||
const btnText = document.getElementById('scan-text');
|
||||
const originalText = btnText.innerText;
|
||||
btnText.innerText = "Scanning...";
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/library/scan?mode=incremental', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadLocalEntries();
|
||||
// Mostrar notificación de éxito si tienes sistema de notificaciones
|
||||
if (window.NotificationUtils) {
|
||||
NotificationUtils.show('Library scanned successfully!', 'success');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Scan failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Scan failed", err);
|
||||
alert("Failed to scan library. Check console for details.");
|
||||
|
||||
// Mostrar notificación de error si tienes sistema de notificaciones
|
||||
if (window.NotificationUtils) {
|
||||
NotificationUtils.show('Failed to scan library', 'error');
|
||||
}
|
||||
} finally {
|
||||
btnText.innerText = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
function viewLocalEntry(anilistId) {
|
||||
if (!anilistId) {
|
||||
console.warn('Anime not linked');
|
||||
return;
|
||||
}
|
||||
window.location.href = `/anime/${anilistId}`;
|
||||
}
|
||||
|
||||
function renderLocalEntries(entries) {
|
||||
const grid = document.getElementById('local-entries-grid');
|
||||
|
||||
grid.innerHTML = entries.map(entry => {
|
||||
const title = entry.metadata?.title?.romaji
|
||||
|| entry.metadata?.title?.english
|
||||
|| entry.id;
|
||||
|
||||
const cover =
|
||||
entry.metadata?.coverImage?.extraLarge
|
||||
|| entry.metadata?.coverImage?.large
|
||||
|| '/public/assets/placeholder.jpg';
|
||||
|
||||
const score = entry.metadata?.averageScore || '--';
|
||||
const episodes = entry.metadata?.episodes || '??';
|
||||
|
||||
return `
|
||||
<div class="local-card" onclick="viewLocalEntry(${entry.metadata?.id || 'null'})">
|
||||
<div class="card-img-wrap">
|
||||
<img src="${cover}" alt="${title}" loading="lazy">
|
||||
</div>
|
||||
<div class="local-card-info">
|
||||
<div class="local-card-title">${title}</div>
|
||||
<p style="font-size: 0.85rem; color: var(--color-text-secondary); margin: 0;">
|
||||
${score}% • ${episodes} Eps
|
||||
</p>
|
||||
<div class="match-status ${entry.matched ? 'status-linked' : 'status-unlinked'}">
|
||||
${entry.matched ? '● Linked' : '○ Unlinked'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function applyLocalFilters() {
|
||||
let filtered = [...localEntries];
|
||||
|
||||
if (activeFilter === 'linked') {
|
||||
filtered = filtered.filter(e => e.matched);
|
||||
}
|
||||
|
||||
if (activeFilter === 'unlinked') {
|
||||
filtered = filtered.filter(e => !e.matched);
|
||||
}
|
||||
|
||||
if (activeSort === 'az') {
|
||||
filtered.sort((a, b) =>
|
||||
(a.metadata?.title?.romaji || a.id)
|
||||
.localeCompare(b.metadata?.title?.romaji || b.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (activeSort === 'za') {
|
||||
filtered.sort((a, b) =>
|
||||
(b.metadata?.title?.romaji || b.id)
|
||||
.localeCompare(a.metadata?.title?.romaji || a.id)
|
||||
);
|
||||
}
|
||||
|
||||
renderLocalEntries(filtered);
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.filter-btn');
|
||||
if (!btn) return;
|
||||
|
||||
if (btn.dataset.filter) {
|
||||
activeFilter = btn.dataset.filter;
|
||||
}
|
||||
|
||||
if (btn.dataset.sort) {
|
||||
activeSort = btn.dataset.sort;
|
||||
}
|
||||
|
||||
btn
|
||||
.closest('.local-filters')
|
||||
.querySelectorAll('.filter-btn')
|
||||
.forEach(b => b.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
|
||||
applyLocalFilters();
|
||||
});
|
||||
Reference in New Issue
Block a user