let bookData = null; let extensionName = null; let bookId = null; let bookSlug = null; let allChapters = []; let filteredChapters = []; let availableExtensions = []; let isLocal = false; let currentLanguage = null; let uniqueLanguages = []; let isSortAscending = true; const chapterPagination = Object.create(PaginationManager); chapterPagination.init(6, () => renderChapterList()); document.addEventListener('DOMContentLoaded', () => { init(); setupModalClickOutside(); document.getElementById('sort-btn')?.addEventListener('click', toggleSortOrder); }); 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 loadBookMetadata(); await checkLocalLibraryEntry(); await loadAvailableExtensions(); await loadChapters(); await setupAddToListButton(); } catch (err) { console.error("Init Error:", err); showError("Error loading book"); } } async function loadBookMetadata() { const source = extensionName || 'anilist'; const fetchUrl = `/api/book/${bookId}?source=${source}`; try { 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, raw); updateExtensionPill(); } catch (e) { console.error(e); showError("Error loading metadata"); } } function updatePageTitle(title) { document.title = `${title} | WaifuBoard Books`; const titleEl = document.getElementById('title'); if (titleEl) titleEl.innerText = title; } function updateMetadata(metadata, rawData) { // 1. Cabecera (Score, Año, Status, Formato, Caps) const elements = { 'description': metadata.description, 'published-date': metadata.year, 'status': metadata.status, 'format': metadata.format, 'chapters-count': metadata.chapters ? `${metadata.chapters} Ch` : '?? Ch', 'genres': metadata.genres ? metadata.genres.replace(/,/g, ' • ') : '', 'poster': metadata.poster, 'hero-bg': metadata.banner }; if(document.getElementById('description')) document.getElementById('description').innerHTML = metadata.description; if(document.getElementById('poster')) document.getElementById('poster').src = metadata.poster; if(document.getElementById('hero-bg')) document.getElementById('hero-bg').src = metadata.banner; ['published-date','status','format','chapters-count','genres'].forEach(id => { const el = document.getElementById(id); if(el) el.innerText = elements[id]; }); const scoreEl = document.getElementById('score'); if (scoreEl) scoreEl.innerText = extensionName ? `${metadata.score}` : `${metadata.score}% Score`; // 2. Sidebar: Sinónimos (Para llenar espacio vacío) if (rawData.synonyms && rawData.synonyms.length > 0) { const sidebarInfo = document.getElementById('sidebar-info'); const list = document.getElementById('synonyms-list'); if (sidebarInfo && list) { sidebarInfo.style.display = 'block'; list.innerHTML = ''; // Mostrar máx 5 sinónimos para no alargar demasiado rawData.synonyms.slice(0, 5).forEach(syn => { const li = document.createElement('li'); li.innerText = syn; list.appendChild(li); }); } } // 3. Renderizar Personajes if (rawData.characters && rawData.characters.nodes && rawData.characters.nodes.length > 0) { renderCharacters(rawData.characters.nodes); } // 4. Renderizar Relaciones if (rawData.relations && rawData.relations.edges && rawData.relations.edges.length > 0) { renderRelations(rawData.relations.edges); } } function renderCharacters(nodes) { const container = document.getElementById('characters-list'); if(!container) return; container.innerHTML = ''; nodes.forEach(char => { const el = document.createElement('div'); el.className = 'character-item'; const img = char.image?.large || char.image?.medium || '/public/assets/no-image.png'; const name = char.name?.full || 'Unknown'; const role = char.role || 'Supporting'; el.innerHTML = `
${name}
${role}
`; container.appendChild(el); }); } function renderRelations(edges) { const container = document.getElementById('relations-list'); const section = document.getElementById('relations-section'); if(!container || !section) return; if (!edges || edges.length === 0) { section.style.display = 'none'; return; } section.style.display = 'block'; container.innerHTML = ''; edges.forEach(edge => { const node = edge.node; if (!node) return; const el = document.createElement('div'); el.className = 'relation-card-horizontal'; const img = node.coverImage?.large || node.coverImage?.medium || '/public/assets/no-image.png'; const title = node.title?.romaji || node.title?.english || node.title?.native || 'Unknown'; const type = edge.relationType ? edge.relationType.replace(/_/g, ' ') : 'Related'; el.innerHTML = ` ${title}
${type} ${title}
`; el.onclick = () => { const targetType = node.type === 'ANIME' ? 'anime' : 'book'; window.location.href = `/${targetType}/${node.id}`; }; container.appendChild(el); }); } function processChaptersData(chaptersData) { allChapters = chaptersData; const langSet = new Set(allChapters.map(ch => ch.language).filter(l => l)); uniqueLanguages = Array.from(langSet); setupLanguageSelector(); filterAndRenderChapters(); setupReadButton(); } async function loadChapters(targetProvider = null) { const listContainer = document.getElementById('chapters-list'); const loadingMsg = document.getElementById('loading-msg'); if(listContainer) listContainer.innerHTML = ''; if(loadingMsg) loadingMsg.style.display = 'block'; if (!targetProvider) { const select = document.getElementById('provider-filter'); targetProvider = select ? select.value : (availableExtensions[0] || 'all'); } try { let fetchUrl; let isLocalRequest = targetProvider === 'local'; if (isLocalRequest) { fetchUrl = `/api/library/${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(); if(loadingMsg) loadingMsg.style.display = 'none'; let rawData = []; if (isLocalRequest) { rawData = (data.units || []).map((unit, idx) => ({ id: unit.id || idx, number: unit.number, title: unit.name, provider: 'local', index: idx, format: unit.format, language: 'local' })); } else { rawData = data.chapters || []; } processChaptersData(rawData); } catch (err) { if(loadingMsg) loadingMsg.style.display = 'none'; if(listContainer) listContainer.innerHTML = '
Error loading chapters.
'; console.error(err); } } function setupLanguageSelector() { const selectorContainer = document.getElementById('language-selector-container'); const select = document.getElementById('language-select'); if (!selectorContainer || !select) return; if (uniqueLanguages.length <= 1) { selectorContainer.classList.add('hidden'); currentLanguage = uniqueLanguages[0] || null; return; } selectorContainer.classList.remove('hidden'); select.innerHTML = ''; const langNames = { 'es': 'Español', 'es-419': 'Latino', 'en': 'English', 'pt-br': 'Português', 'ja': '日本語' }; uniqueLanguages.forEach(lang => { const option = document.createElement('option'); option.value = lang; option.textContent = langNames[lang] || lang.toUpperCase(); select.appendChild(option); }); if (uniqueLanguages.includes('es-419')) currentLanguage = 'es-419'; else if (uniqueLanguages.includes('es')) currentLanguage = 'es'; else currentLanguage = uniqueLanguages[0]; select.value = currentLanguage; select.onchange = (e) => { currentLanguage = e.target.value; chapterPagination.currentPage = 1; filterAndRenderChapters(); }; } function filterAndRenderChapters() { let tempChapters = [...allChapters]; if (currentLanguage && uniqueLanguages.length > 1) { tempChapters = tempChapters.filter(ch => ch.language === currentLanguage); } const searchQuery = document.getElementById('chapter-search')?.value.toLowerCase(); if(searchQuery){ tempChapters = tempChapters.filter(ch => (ch.title && ch.title.toLowerCase().includes(searchQuery)) || (ch.number && ch.number.toString().includes(searchQuery)) ); } tempChapters.sort((a, b) => { const numA = parseFloat(a.number) || 0; const numB = parseFloat(b.number) || 0; return isSortAscending ? numA - numB : numB - numA; }); filteredChapters = tempChapters; chapterPagination.setTotalItems(filteredChapters.length); renderChapterList(); } function renderChapterList() { const container = document.getElementById('chapters-list'); if(!container) return; container.innerHTML = ''; const itemsToShow = chapterPagination.getCurrentPageItems(filteredChapters); if (itemsToShow.length === 0) { container.innerHTML = '
No chapters found.
'; return; } itemsToShow.forEach(chapter => { const el = document.createElement('div'); el.className = 'chapter-item'; el.onclick = () => openReader(chapter.id, chapter.provider); const dateStr = chapter.date ? new Date(chapter.date).toLocaleDateString() : ''; const providerLabel = chapter.provider !== 'local' ? chapter.provider : ''; el.innerHTML = `
Chapter ${chapter.number} ${chapter.title || ''}
${providerLabel ? `${providerLabel}` : ''} ${dateStr ? `${dateStr}` : ''}
`; container.appendChild(el); }); chapterPagination.renderControls('pagination', 'page-info', 'prev-page', 'next-page'); } function toggleSortOrder() { isSortAscending = !isSortAscending; const btn = document.getElementById('sort-btn'); if(btn) btn.style.transform = isSortAscending ? 'rotate(180deg)' : 'rotate(0deg)'; filterAndRenderChapters(); } function setupReadButton() { const readBtn = document.getElementById('read-start-btn'); if (!readBtn || allChapters.length === 0) return; const firstChapter = [...allChapters].sort((a,b) => a.index - b.index)[0]; if (firstChapter) readBtn.onclick = () => openReader(firstChapter.index ?? firstChapter.id, firstChapter.provider); } function openReader(chapterIndexOrId, provider) { const lang = currentLanguage ?? 'none'; window.location.href = URLUtils.buildReadUrl(bookId, chapterIndexOrId, provider, extensionName || 'anilist') + `?lang=${lang}`; } async function checkLocalLibraryEntry() { try { const libraryType = bookData?.entry_type === 'NOVEL' ? 'novels' : 'manga'; const res = await fetch(`/api/library/${libraryType}/${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:", e); } } async function loadAvailableExtensions() { try { const res = await fetch('/api/extensions/book'); const data = await res.json(); availableExtensions = data.extensions || []; setupProviderFilter(); } catch (err) { console.error(err); } } function setupProviderFilter() { const select = document.getElementById('provider-filter'); if (!select) return; select.style.display = 'inline-block'; select.innerHTML = ''; const allOpt = document.createElement('option'); allOpt.value = 'all'; allOpt.innerText = 'All Providers'; select.appendChild(allOpt); if (isLocal) { const localOpt = document.createElement('option'); localOpt.value = 'local'; localOpt.innerText = 'Local'; select.appendChild(localOpt); } availableExtensions.forEach(ext => { const opt = document.createElement('option'); opt.value = ext; opt.innerText = ext.charAt(0).toUpperCase() + ext.slice(1); select.appendChild(opt); }); if (isLocal) select.value = '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 updateExtensionPill() { const pill = document.getElementById('extension-pill'); if(pill && extensionName) { pill.innerText = extensionName; pill.style.display = 'inline-flex'; } } 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 && ListModalManager.isInList) { btn.innerHTML = '✓ In Your List'; btn.style.background = 'rgba(34, 197, 94, 0.2)'; btn.style.color = '#22c55e'; btn.style.borderColor = '#22c55e'; } } function setupModalClickOutside() { const modal = document.getElementById('add-list-modal'); if (modal) { 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; } // Exports window.openReader = openReader; window.saveToList = () => { const idToSave = extensionName ? bookSlug : bookId; ListModalManager.save(idToSave, extensionName || 'anilist'); }; window.deleteFromList = () => { const idToDelete = extensionName ? bookSlug : bookId; ListModalManager.delete(idToDelete, extensionName || 'anilist'); }; window.closeAddToListModal = () => ListModalManager.close(); window.openAddToListModal = () => ListModalManager.open(bookData, extensionName || 'anilist');