420 lines
12 KiB
JavaScript
420 lines
12 KiB
JavaScript
let bookData = null;
|
|
let extensionName = null;
|
|
let bookId = null;
|
|
let bookSlug = null;
|
|
|
|
let allChapters = [];
|
|
let filteredChapters = [];
|
|
|
|
let availableExtensions = [];
|
|
let isLocal = false;
|
|
const chapterPagination = Object.create(PaginationManager);
|
|
chapterPagination.init(12, () => renderChapterTable());
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
init();
|
|
setupModalClickOutside();
|
|
});
|
|
|
|
async function checkLocalLibraryEntry() {
|
|
try {
|
|
const res = await fetch(`/api/library/manga/${bookId}`);
|
|
if (!res.ok) return;
|
|
|
|
const data = await res.json();
|
|
if (data.matched) {
|
|
isLocal = true;
|
|
const pill = document.getElementById('local-pill');
|
|
if (pill) {
|
|
pill.textContent = 'Local';
|
|
pill.style.display = 'inline-flex';
|
|
pill.style.background = 'rgba(34, 197, 94, 0.2)';
|
|
pill.style.color = '#22c55e';
|
|
pill.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Error checking local status:", e);
|
|
}
|
|
}
|
|
|
|
function markAsLocal() {
|
|
isLocal = true;
|
|
const pill = document.getElementById('local-pill');
|
|
if (pill) {
|
|
pill.textContent = 'Local';
|
|
pill.style.display = 'inline-flex';
|
|
pill.style.background = 'rgba(34, 197, 94, 0.2)';
|
|
pill.style.color = '#22c55e';
|
|
pill.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
try {
|
|
const urlData = URLUtils.parseEntityPath('book');
|
|
if (!urlData) {
|
|
showError("Book Not Found");
|
|
return;
|
|
}
|
|
|
|
extensionName = urlData.extensionName;
|
|
bookId = urlData.entityId;
|
|
bookSlug = urlData.slug;
|
|
await checkLocalLibraryEntry();
|
|
await loadBookMetadata();
|
|
|
|
await loadAvailableExtensions();
|
|
await loadChapters();
|
|
|
|
await setupAddToListButton();
|
|
|
|
} catch (err) {
|
|
console.error("Metadata Error:", err);
|
|
showError("Error loading book");
|
|
}
|
|
}
|
|
|
|
async function loadAvailableExtensions() {
|
|
try {
|
|
const res = await fetch('/api/extensions/book');
|
|
const data = await res.json();
|
|
availableExtensions = data.extensions || [];
|
|
|
|
setupProviderFilter();
|
|
} catch (err) {
|
|
console.error("Error fetching extensions:", err);
|
|
}
|
|
}
|
|
|
|
async function loadBookMetadata() {
|
|
const source = extensionName || 'anilist';
|
|
const fetchUrl = `/api/book/${bookId}?source=${source}`;
|
|
|
|
const res = await fetch(fetchUrl);
|
|
const data = await res.json();
|
|
|
|
if (data.error || !data) {
|
|
showError("Book Not Found");
|
|
return;
|
|
}
|
|
|
|
const raw = Array.isArray(data) ? data[0] : data;
|
|
bookData = raw;
|
|
|
|
const metadata = MediaMetadataUtils.formatBookData(raw, !!extensionName);
|
|
bookData.entry_type =
|
|
metadata.format === 'MANGA' ? 'MANGA' : 'NOVEL';
|
|
updatePageTitle(metadata.title);
|
|
updateMetadata(metadata);
|
|
updateExtensionPill();
|
|
}
|
|
|
|
function updatePageTitle(title) {
|
|
document.title = `${title} | WaifuBoard Books`;
|
|
const titleEl = document.getElementById('title');
|
|
if (titleEl) titleEl.innerText = title;
|
|
}
|
|
|
|
function updateMetadata(metadata) {
|
|
|
|
const descEl = document.getElementById('description');
|
|
if (descEl) descEl.innerHTML = metadata.description;
|
|
|
|
const scoreEl = document.getElementById('score');
|
|
if (scoreEl) {
|
|
scoreEl.innerText = extensionName
|
|
? `${metadata.score}`
|
|
: `${metadata.score}% Score`;
|
|
}
|
|
|
|
const pubEl = document.getElementById('published-date');
|
|
if (pubEl) pubEl.innerText = metadata.year;
|
|
|
|
const statusEl = document.getElementById('status');
|
|
if (statusEl) statusEl.innerText = metadata.status;
|
|
|
|
const formatEl = document.getElementById('format');
|
|
if (formatEl) formatEl.innerText = metadata.format;
|
|
|
|
const chaptersEl = document.getElementById('chapters');
|
|
if (chaptersEl) chaptersEl.innerText = metadata.chapters;
|
|
|
|
const genresEl = document.getElementById('genres');
|
|
if (genresEl) genresEl.innerText = metadata.genres;
|
|
|
|
const posterEl = document.getElementById('poster');
|
|
if (posterEl) posterEl.src = metadata.poster;
|
|
|
|
const heroBgEl = document.getElementById('hero-bg');
|
|
if (heroBgEl) heroBgEl.src = metadata.banner;
|
|
}
|
|
|
|
function updateExtensionPill() {
|
|
const pill = document.getElementById('extension-pill');
|
|
if (!pill) return;
|
|
|
|
if (extensionName) {
|
|
pill.textContent = extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase();
|
|
pill.style.display = 'inline-flex';
|
|
} else {
|
|
pill.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function setupAddToListButton() {
|
|
const btn = document.getElementById('add-to-list-btn');
|
|
if (!btn || !bookData) return;
|
|
|
|
ListModalManager.currentData = bookData;
|
|
const entryType = ListModalManager.getEntryType(bookData);
|
|
const idForCheck = extensionName ? bookSlug : bookId;
|
|
|
|
await ListModalManager.checkIfInList(
|
|
idForCheck,
|
|
extensionName || 'anilist',
|
|
entryType
|
|
);
|
|
|
|
updateCustomAddButton();
|
|
|
|
btn.onclick = () => ListModalManager.open(bookData, extensionName || 'anilist');
|
|
}
|
|
|
|
function updateCustomAddButton() {
|
|
const btn = document.getElementById('add-to-list-btn');
|
|
if (!btn) return;
|
|
|
|
if (ListModalManager.isInList) {
|
|
btn.innerHTML = `
|
|
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
|
</svg>
|
|
In Your Library
|
|
`;
|
|
btn.style.background = 'rgba(34, 197, 94, 0.2)';
|
|
btn.style.color = '#22c55e';
|
|
btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
|
} else {
|
|
btn.innerHTML = '+ Add to Library';
|
|
btn.style.background = null;
|
|
btn.style.color = null;
|
|
btn.style.borderColor = null;
|
|
}
|
|
}
|
|
|
|
async function loadChapters(targetProvider = null) {
|
|
const tbody = document.getElementById('chapters-body');
|
|
if (!tbody) return;
|
|
|
|
if (!targetProvider) {
|
|
const select = document.getElementById('provider-filter');
|
|
targetProvider = select ? select.value : (availableExtensions[0] || 'all');
|
|
}
|
|
|
|
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Loading chapters...</td></tr>';
|
|
|
|
try {
|
|
let fetchUrl;
|
|
let isLocalRequest = targetProvider === 'local';
|
|
|
|
if (isLocalRequest) {
|
|
// Nuevo endpoint para archivos locales
|
|
fetchUrl = `/api/library/manga/${bookId}/units`;
|
|
} else {
|
|
const source = extensionName || 'anilist';
|
|
fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
|
|
if (targetProvider !== 'all') fetchUrl += `&provider=${targetProvider}`;
|
|
}
|
|
|
|
const res = await fetch(fetchUrl);
|
|
const data = await res.json();
|
|
|
|
// Mapeo de datos: Si es local usamos 'units', si no, usamos 'chapters'
|
|
if (isLocalRequest) {
|
|
allChapters = (data.units || []).map((unit, idx) => ({
|
|
number: unit.number,
|
|
title: unit.name,
|
|
provider: 'local',
|
|
index: idx, // ✅ índice (0,1,2…)
|
|
format: unit.format
|
|
}));
|
|
} else {
|
|
allChapters = data.chapters || [];
|
|
}
|
|
|
|
filteredChapters = [...allChapters];
|
|
applyChapterFilter();
|
|
|
|
const totalEl = document.getElementById('total-chapters');
|
|
if (allChapters.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters found.</td></tr>';
|
|
if (totalEl) totalEl.innerText = "0 Found";
|
|
return;
|
|
}
|
|
|
|
if (totalEl) totalEl.innerText = `${allChapters.length} Found`;
|
|
|
|
setupReadButton();
|
|
chapterPagination.setTotalItems(filteredChapters.length);
|
|
chapterPagination.reset();
|
|
renderChapterTable();
|
|
|
|
} catch (err) {
|
|
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: #ef4444;">Error loading chapters.</td></tr>';
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
function applyChapterFilter() {
|
|
const chapterParam = URLUtils.getQueryParam('chapter');
|
|
if (!chapterParam) return;
|
|
|
|
const chapterNumber = parseFloat(chapterParam);
|
|
if (isNaN(chapterNumber)) return;
|
|
|
|
filteredChapters = allChapters.filter(
|
|
ch => parseFloat(ch.number) === chapterNumber
|
|
);
|
|
|
|
chapterPagination.reset();
|
|
}
|
|
|
|
function setupProviderFilter() {
|
|
const select = document.getElementById('provider-filter');
|
|
if (!select) return;
|
|
|
|
select.style.display = 'inline-block';
|
|
select.innerHTML = '';
|
|
|
|
// Opción para cargar todo
|
|
const allOpt = document.createElement('option');
|
|
allOpt.value = 'all';
|
|
allOpt.innerText = 'Load All (Slower)';
|
|
select.appendChild(allOpt);
|
|
|
|
// NUEVO: Si es local, añadimos la opción 'local' al principio
|
|
if (isLocal) {
|
|
const localOpt = document.createElement('option');
|
|
localOpt.value = 'local';
|
|
localOpt.innerText = 'Local';
|
|
select.appendChild(localOpt);
|
|
}
|
|
|
|
// Añadir extensiones normales
|
|
availableExtensions.forEach(ext => {
|
|
const opt = document.createElement('option');
|
|
opt.value = ext;
|
|
opt.innerText = ext.charAt(0).toUpperCase() + ext.slice(1);
|
|
select.appendChild(opt);
|
|
});
|
|
|
|
// Lógica de selección automática
|
|
if (isLocal) {
|
|
select.value = 'local'; // Prioridad si es local
|
|
} else if (extensionName && availableExtensions.includes(extensionName)) {
|
|
select.value = extensionName;
|
|
} else if (availableExtensions.length > 0) {
|
|
select.value = availableExtensions[0];
|
|
}
|
|
|
|
select.onchange = () => {
|
|
loadChapters(select.value);
|
|
};
|
|
}
|
|
|
|
function setupReadButton() {
|
|
const readBtn = document.getElementById('read-start-btn');
|
|
if (!readBtn || filteredChapters.length === 0) return;
|
|
|
|
const firstChapter = filteredChapters[0];
|
|
readBtn.onclick = () => openReader(0, firstChapter.provider);
|
|
}
|
|
|
|
function renderChapterTable() {
|
|
const tbody = document.getElementById('chapters-body');
|
|
if (!tbody) return;
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
if (filteredChapters.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters match this filter.</td></tr>';
|
|
chapterPagination.renderControls(
|
|
'pagination',
|
|
'page-info',
|
|
'prev-page',
|
|
'next-page'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const pageItems = chapterPagination.getCurrentPageItems(filteredChapters);
|
|
|
|
pageItems.forEach((ch) => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${ch.number}</td>
|
|
<td>${ch.title || 'Chapter ' + ch.number}</td>
|
|
<td><span class="pill" style="font-size:0.75rem;">${ch.provider}</span></td>
|
|
<td>
|
|
<button class="read-btn-small" onclick="openReader('${ch.index}', '${ch.provider}')">
|
|
Read
|
|
</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
chapterPagination.renderControls(
|
|
'pagination',
|
|
'page-info',
|
|
'prev-page',
|
|
'next-page'
|
|
);
|
|
}
|
|
|
|
function openReader(chapterId, provider) {
|
|
const effectiveExtension = extensionName || 'anilist';
|
|
|
|
window.location.href = URLUtils.buildReadUrl(
|
|
bookId, // SIEMPRE anilist
|
|
chapterId, // número normal
|
|
provider, // 'local' o extensión
|
|
extensionName || 'anilist'
|
|
);
|
|
}
|
|
|
|
function setupModalClickOutside() {
|
|
const modal = document.getElementById('add-list-modal');
|
|
if (!modal) return;
|
|
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target.id === 'add-list-modal') {
|
|
ListModalManager.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
function showError(message) {
|
|
const titleEl = document.getElementById('title');
|
|
if (titleEl) titleEl.innerText = message;
|
|
}
|
|
|
|
function saveToList() {
|
|
const idToSave = extensionName ? bookSlug : bookId;
|
|
ListModalManager.save(idToSave, extensionName || 'anilist');
|
|
}
|
|
|
|
function deleteFromList() {
|
|
const idToDelete = extensionName ? bookSlug : bookId;
|
|
ListModalManager.delete(idToDelete, extensionName || 'anilist');
|
|
}
|
|
|
|
function closeAddToListModal() {
|
|
ListModalManager.close();
|
|
}
|
|
|
|
window.openReader = openReader;
|
|
window.saveToList = saveToList;
|
|
window.deleteFromList = deleteFromList;
|
|
window.closeAddToListModal = closeAddToListModal; |