609 lines
20 KiB
JavaScript
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(); |