Files
WaifuBoard/src/scripts/books/book.js

609 lines
20 KiB
JavaScript

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 = `
<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)';
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 = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extensions for chapters...</td></tr>';
try {
const fetchUrl = getChaptersUrl(
idForFetch,
extensionName || 'anilist'
);
const res = await fetch(fetchUrl, { headers: getSimpleAuthHeaders() });
const data = await res.json();
allChapters = data.chapters || [];
filteredChapters = [...allChapters];
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 on loaded extensions.</td></tr>';
if (totalEl) totalEl.innerText = "0 Found";
return;
}
if (totalEl) totalEl.innerText = `${allChapters.length} Found`;
populateProviderFilter();
const readBtn = document.getElementById('read-start-btn');
if (readBtn && filteredChapters.length > 0) {
readBtn.onclick = () => openReader(idForFetch, filteredChapters[0].id, filteredChapters[0].provider);
}
renderTable(idForFetch);
} catch (err) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: #ef4444;">Error loading chapters.</td></tr>';
console.error(err);
}
}
function populateProviderFilter() {
const select = document.getElementById('provider-filter');
if (!select) return;
const providers = [...new Set(allChapters.map(ch => ch.provider))];
if (providers.length > 0) {
select.style.display = 'inline-block';
select.innerHTML = '<option value="all">All Providers</option>';
providers.forEach(prov => {
const opt = document.createElement('option');
opt.value = prov;
opt.innerText = prov;
select.appendChild(opt);
});
if (extensionName) {
const extensionProvider = providers.find(p => p.toLowerCase() === extensionName.toLowerCase());
if (extensionProvider) {
select.value = extensionProvider;
filteredChapters = allChapters.filter(ch => ch.provider === extensionProvider);
}
}
select.onchange = (e) => {
const selected = e.target.value;
if (selected === 'all') {
filteredChapters = [...allChapters];
} else {
filteredChapters = allChapters.filter(ch => ch.provider === selected);
}
currentPage = 1;
const idForFetch = extensionName ? bookSlug : bookId;
renderTable(idForFetch);
};
}
}
function renderTable(idForFetch) {
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>';
updatePagination();
return;
}
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
const pageItems = filteredChapters.slice(start, end);
pageItems.forEach((ch, idx) => {
const realIndex = start + idx;
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('${bookId}', '${ch.index}', '${ch.provider}')">
Read
</button>
</td>
`;
tbody.appendChild(row);
});
updatePagination();
}
function updatePagination() {
const totalPages = Math.ceil(filteredChapters.length / itemsPerPage);
const pagination = document.getElementById('pagination');
if (!pagination) return;
if (totalPages <= 1) {
pagination.style.display = 'none';
return;
}
pagination.style.display = 'flex';
document.getElementById('page-info').innerText = `Page ${currentPage} of ${totalPages}`;
const prevBtn = document.getElementById('prev-page');
const nextBtn = document.getElementById('next-page');
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage >= totalPages;
const idForFetch = extensionName ? bookSlug : bookId;
prevBtn.onclick = () => { currentPage--; renderTable(idForFetch); };
nextBtn.onclick = () => { currentPage++; renderTable(idForFetch); };
}
function openReader(bookId, chapterId, provider) {
const c = encodeURIComponent(chapterId);
const p = encodeURIComponent(provider);
let extension = "?source=anilist";
if (extensionName) extension = "?source=" + extensionName;
window.location.href = `/read/${p}/${c}/${bookId}${extension}`;
}
document.addEventListener('DOMContentLoaded', () => {
// El ID del modal sigue siendo 'add-list-modal' para mantener la compatibilidad con el código original.
const modal = document.getElementById('add-list-modal');
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target.id === 'add-list-modal') {
closeAddToListModal();
}
});
}
});
const style = document.createElement('style');
style.textContent = `
@keyframes slideInRight {
from { transform: translateX(400px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(400px); opacity: 0; }
}
`;
document.head.appendChild(style);
init();