const bookId = window.location.pathname.split('/').pop(); let allChapters = []; let filteredChapters = []; let currentPage = 1; const itemsPerPage = 12; let extensionName = null; let bookSlug = null; let currentBookData = null; let isInList = false; let currentListEntry = null; const API_BASE = '/api'; function getBookUrl(id, source = 'anilist') { return `/api/book/${id}?source=${source}`; } function getChaptersUrl(id, source = 'anilist') { return `/api/book/${id}/chapters?source=${source}`; } function getAuthToken() { return localStorage.getItem('token'); } function getAuthHeaders() { const token = getAuthToken(); return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }; } function getSimpleAuthHeaders() { const token = getAuthToken(); return { 'Authorization': `Bearer ${token}` }; } function getBookEntryType(bookData) { if (!bookData) return 'MANGA'; const format = bookData.format?.toUpperCase() || 'MANGA'; return (format === 'MANGA' || format === 'ONE_SHOT' || format === 'MANHWA') ? 'MANGA' : 'NOVEL'; } // CORRECCIÓN: Usar el endpoint /list/entry/{id} y esperar 'found' async function checkIfInList() { if (!currentBookData) return; const entryId = extensionName ? bookSlug : bookId; const source = extensionName || 'anilist'; const entryType = getBookEntryType(currentBookData); // URL CORRECTA: /list/entry/{id}?source={source}&entry_type={entryType} const fetchUrl = `${API_BASE}/list/entry/${entryId}?source=${source}&entry_type=${entryType}`; try { const response = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() }); if (response.ok) { const data = await response.json(); // LÓGICA CORRECTA: Comprobar data.found if (data.found && data.entry) { isInList = true; currentListEntry = data.entry; } else { isInList = false; currentListEntry = null; } updateAddToListButton(); } else if (response.status === 404) { // Manejar 404 como 'no encontrado' si la API lo devuelve así isInList = false; currentListEntry = null; updateAddToListButton(); } } catch (error) { console.error('Error checking single list entry:', error); } } function updateAddToListButton() { const btn = document.getElementById('add-to-list-btn'); if (!btn) return; if (isInList) { btn.innerHTML = ` 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)'; btn.onclick = openAddToListModal; } else { btn.innerHTML = '+ Add to Library'; btn.style.background = null; btn.style.color = null; btn.style.borderColor = null; btn.onclick = openAddToListModal; } } /** * REFACTORIZADO para usar la estructura del modal completo. * Asume que el HTML usa IDs como 'entry-status', 'entry-progress', 'entry-score', etc. */ function openAddToListModal() { if (!currentBookData) return; const totalUnits = currentBookData.chapters || currentBookData.volumes || 999; const entryType = getBookEntryType(currentBookData); // Referencias a los elementos del nuevo modal (usando 'entry-' prefix) const modalTitle = document.getElementById('modal-title'); const deleteBtn = document.getElementById('modal-delete-btn'); const progressLabel = document.getElementById('progress-label'); // **VERIFICACIÓN CRÍTICA** if (!modalTitle || !deleteBtn || !progressLabel) { console.error("Error: Uno o más elementos críticos del modal (título, botón eliminar, o etiqueta de progreso) no se encontraron. Verifique los IDs en el HTML."); return; } // --- Población de Datos --- if (isInList && currentListEntry) { // Datos comunes document.getElementById('entry-status').value = currentListEntry.status || 'PLANNING'; document.getElementById('entry-progress').value = currentListEntry.progress || 0; document.getElementById('entry-score').value = currentListEntry.score || ''; // Nuevos datos // Usar formato ISO si viene como ISO, o limpiar si es necesario. Tu ejemplo JSON no tenía fechas. document.getElementById('entry-start-date').value = currentListEntry.start_date ? currentListEntry.start_date.split('T')[0] : ''; document.getElementById('entry-end-date').value = currentListEntry.end_date ? currentListEntry.end_date.split('T')[0] : ''; document.getElementById('entry-repeat-count').value = currentListEntry.repeat_count || 0; document.getElementById('entry-notes').value = currentListEntry.notes || ''; document.getElementById('entry-is-private').checked = currentListEntry.is_private === true || currentListEntry.is_private === 1; modalTitle.textContent = 'Edit Library Entry'; deleteBtn.style.display = 'block'; } else { // Valores por defecto document.getElementById('entry-status').value = 'PLANNING'; document.getElementById('entry-progress').value = 0; document.getElementById('entry-score').value = ''; document.getElementById('entry-start-date').value = ''; document.getElementById('entry-end-date').value = ''; document.getElementById('entry-repeat-count').value = 0; document.getElementById('entry-notes').value = ''; document.getElementById('entry-is-private').checked = false; modalTitle.textContent = 'Add to Library'; deleteBtn.style.display = 'none'; } // --- Configuración de Etiquetas y Máximo --- if (progressLabel) { if (entryType === 'MANGA') { progressLabel.textContent = 'Chapters Read'; } else { progressLabel.textContent = 'Volumes/Parts Read'; } } document.getElementById('entry-progress').max = totalUnits; document.getElementById('add-list-modal').classList.add('active'); } function closeAddToListModal() { document.getElementById('add-list-modal').classList.remove('active'); } /** * REFACTORIZADO para guardar TODOS los campos del modal. */ async function saveToList() { // Datos comunes const status = document.getElementById('entry-status').value; const progress = parseInt(document.getElementById('entry-progress').value) || 0; const scoreValue = document.getElementById('entry-score').value; const score = scoreValue ? parseFloat(scoreValue) : null; // Nuevos datos const start_date = document.getElementById('entry-start-date').value || null; const end_date = document.getElementById('entry-end-date').value || null; const repeat_count = parseInt(document.getElementById('entry-repeat-count').value) || 0; const notes = document.getElementById('entry-notes').value || null; const is_private = document.getElementById('entry-is-private').checked; if (!currentBookData) { showNotification('Cannot save: Book data not loaded.', 'error'); return; } const entryType = getBookEntryType(currentBookData); const idToSave = extensionName ? bookSlug : bookId; try { const response = await fetch(`${API_BASE}/list/entry`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ entry_id: idToSave, source: extensionName || 'anilist', entry_type: entryType, status: status, progress: progress, score: score, // Nuevos campos start_date: start_date, end_date: end_date, repeat_count: repeat_count, notes: notes, is_private: is_private }) }); if (!response.ok) { throw new Error('Failed to save entry'); } const data = await response.json(); isInList = true; currentListEntry = data.entry; // Usar la respuesta del servidor si está disponible updateAddToListButton(); closeAddToListModal(); showNotification(isInList ? 'Updated successfully!' : 'Added to your library!', 'success'); } catch (error) { console.error('Error saving to list:', error); showNotification('Failed to save. Please try again.', 'error'); } } // CORRECCIÓN: Usar el endpoint /list/entry/{id} con los parámetros correctos. async function deleteFromList() { if (!confirm('Remove this book from your library?')) return; const idToDelete = extensionName ? bookSlug : bookId; const source = extensionName || 'anilist'; const entryType = getBookEntryType(currentBookData); // Obtener el tipo de entrada try { // URL CORRECTA para DELETE: /list/entry/{id}?source={source}&entry_type={entryType} const response = await fetch(`${API_BASE}/list/entry/${idToDelete}?source=${source}&entry_type=${entryType}`, { method: 'DELETE', headers: getSimpleAuthHeaders() }); if (!response.ok) { throw new Error('Failed to delete entry'); } isInList = false; currentListEntry = null; updateAddToListButton(); closeAddToListModal(); showNotification('Removed from your library', 'success'); } catch (error) { console.error('Error deleting from list:', error); showNotification('Failed to remove. Please try again.', 'error'); } } function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 100px; right: 20px; background: ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : '#8b5cf6'}; color: white; padding: 1rem 1.5rem; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 9999; font-weight: 600; animation: slideInRight 0.3s ease; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOutRight 0.3s ease'; setTimeout(() => notification.remove(), 300); }, 3000); } async function init() { try { const path = window.location.pathname; const parts = path.split("/").filter(Boolean); let currentBookId; if (parts.length === 3) { extensionName = parts[1]; bookSlug = parts[2]; currentBookId = bookSlug; } else { currentBookId = parts[1]; } const idForFetch = currentBookId; const fetchUrl = getBookUrl( idForFetch, extensionName || 'anilist' ); const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() }); const data = await res.json(); if (data.error || !data) { const titleEl = document.getElementById('title'); if (titleEl) titleEl.innerText = "Book Not Found"; return; } currentBookData = data; let title, description, score, year, status, format, chapters, poster, banner, genres; if (extensionName) { title = data.title || data.name || "Unknown"; description = data.summary || "No description available."; score = data.score ? Math.round(data.score) : '?'; year = data.published || '????'; status = data.status || 'Unknown'; format = data.format || 'LN'; chapters = data.chapters || '?'; poster = data.image || ''; banner = poster; genres = Array.isArray(data.genres) ? data.genres.slice(0, 3).join(' • ') : ''; } else { title = data.title.english || data.title.romaji || "Unknown"; description = data.description || "No description available."; score = data.averageScore || '?'; year = (data.startDate && data.startDate.year) ? data.startDate.year : '????'; status = data.status || 'Unknown'; format = data.format || 'MANGA'; chapters = data.chapters || '?'; poster = data.coverImage.extraLarge || data.coverImage.large || ''; banner = data.bannerImage || poster; genres = data.genres ? data.genres.slice(0, 3).join(' • ') : ''; } document.title = `${title} | WaifuBoard Books`; const titleEl = document.getElementById('title'); if (titleEl) titleEl.innerText = title; const extensionPill = document.getElementById('extension-pill'); if (extensionName && extensionPill) { extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`; extensionPill.style.display = 'inline-flex'; } else if (extensionPill) { extensionPill.style.display = 'none'; } const descEl = document.getElementById('description'); if (descEl) descEl.innerHTML = description; const scoreEl = document.getElementById('score'); if (scoreEl) scoreEl.innerText = score + (extensionName ? '' : '% Score'); const pubEl = document.getElementById('published-date'); if (pubEl) pubEl.innerText = year; const statusEl = document.getElementById('status'); if (statusEl) statusEl.innerText = status; const formatEl = document.getElementById('format'); if (formatEl) formatEl.innerText = format; const chaptersEl = document.getElementById('chapters'); if (chaptersEl) chaptersEl.innerText = chapters; const genresEl = document.getElementById('genres'); if(genresEl) { genresEl.innerText = genres; } const posterEl = document.getElementById('poster'); if (posterEl) posterEl.src = poster; const heroBgEl = document.getElementById('hero-bg'); if (heroBgEl) heroBgEl.src = banner; loadChapters(idForFetch); await checkIfInList(); } catch (err) { console.error("Metadata Error:", err); } } async function loadChapters(idForFetch) { const tbody = document.getElementById('chapters-body'); if (!tbody) return; tbody.innerHTML = '