const urlParams = new URLSearchParams(window.location.search); const reader = document.getElementById('reader'); const panel = document.getElementById('settings-panel'); const overlay = document.getElementById('overlay'); const settingsBtn = document.getElementById('settings-btn'); const closePanel = document.getElementById('close-panel'); const chapterLabel = document.getElementById('chapter-label'); const prevBtn = document.getElementById('prev-chapter'); const nextBtn = document.getElementById('next-chapter'); const lnSettings = document.getElementById('ln-settings'); const mangaSettings = document.getElementById('manga-settings'); const rawSource = urlParams.get('source') || 'anilist'; const sourceParts = rawSource.split('?'); const source = sourceParts[0]; let lang = urlParams.get('lang') ?? new URLSearchParams(sourceParts[1] || '').get('lang') ?? 'none'; const config = { ln: { fontSize: 18, lineHeight: 1.8, maxWidth: 750, fontFamily: '"Georgia", serif', textColor: '#e5e7eb', bg: '#14141b', textAlign: 'justify' }, manga: { direction: 'rtl', mode: 'auto', spacing: 16, imageFit: 'screen', preloadCount: 3 } }; let currentType = null; let currentPages = []; let observer = null; // === CAMBIO: Parseo de URL para obtener ID === const parts = window.location.pathname.split('/'); const bookId = parts[4]; let currentChapterId = parts[3]; // Ahora es un ID (string) let provider = parts[2]; let chaptersList = []; // Buffer para guardar el orden de capítulos function loadConfig() { try { const saved = localStorage.getItem('readerConfig'); if (saved) { const parsed = JSON.parse(saved); Object.assign(config.ln, parsed.ln || {}); Object.assign(config.manga, parsed.manga || {}); } } catch (e) { console.error('Error loading config:', e); } updateUIFromConfig(); } function saveConfig() { try { localStorage.setItem('readerConfig', JSON.stringify(config)); } catch (e) { console.error('Error saving config:', e); } } function updateUIFromConfig() { document.getElementById('font-size').value = config.ln.fontSize; document.getElementById('font-size-value').textContent = config.ln.fontSize + 'px'; document.getElementById('line-height').value = config.ln.lineHeight; document.getElementById('line-height-value').textContent = config.ln.lineHeight; document.getElementById('max-width').value = config.ln.maxWidth; document.getElementById('max-width-value').textContent = config.ln.maxWidth + 'px'; document.getElementById('font-family').value = config.ln.fontFamily; document.getElementById('text-color').value = config.ln.textColor; document.getElementById('bg-color').value = config.ln.bg; document.querySelectorAll('[data-align]').forEach(btn => { btn.classList.toggle('active', btn.dataset.align === config.ln.textAlign); }); document.getElementById('display-mode').value = config.manga.mode; document.getElementById('image-fit').value = config.manga.imageFit; document.getElementById('page-spacing').value = config.manga.spacing; document.getElementById('page-spacing-value').textContent = config.manga.spacing + 'px'; document.getElementById('preload-count').value = config.manga.preloadCount; document.querySelectorAll('[data-direction]').forEach(btn => { btn.classList.toggle('active', btn.dataset.direction === config.manga.direction); }); } function applyStyles() { if (currentType === 'ln') { document.documentElement.style.setProperty('--ln-font-size', config.ln.fontSize + 'px'); document.documentElement.style.setProperty('--ln-line-height', config.ln.lineHeight); document.documentElement.style.setProperty('--ln-max-width', config.ln.maxWidth + 'px'); document.documentElement.style.setProperty('--ln-font-family', config.ln.fontFamily); document.documentElement.style.setProperty('--ln-text-color', config.ln.textColor); document.documentElement.style.setProperty('--color-bg-base', config.ln.bg); document.documentElement.style.setProperty('--ln-text-align', config.ln.textAlign); } if (currentType === 'manga') { document.documentElement.style.setProperty('--page-spacing', config.manga.spacing + 'px'); document.documentElement.style.setProperty('--page-max-width', 900 + 'px'); document.documentElement.style.setProperty('--manga-max-width', 1400 + 'px'); const viewportHeight = window.innerHeight - 64 - 32; document.documentElement.style.setProperty('--viewport-height', viewportHeight + 'px'); } } function updateSettingsVisibility() { lnSettings.classList.toggle('hidden', currentType !== 'ln'); mangaSettings.classList.toggle('hidden', currentType !== 'manga'); } // === CAMBIO: Nueva función para traer la lista de capítulos y saber el orden === async function fetchChapterList() { try { // Reusamos el endpoint que lista capítulos const res = await fetch(`/api/book/${bookId}/chapters?source=${source}&provider=${provider}`); const data = await res.json(); // Ordenamos por número para asegurar navegación correcta let list = data.chapters || []; list.sort((a, b) => Number(a.number) - Number(b.number)); // Si hay filtro de idioma en la URL, filtramos la navegación también if (lang !== 'none') { list = list.filter(c => c.language === lang); } chaptersList = list; } catch (e) { console.error("Error fetching chapter list:", e); } } async function loadChapter() { reader.innerHTML = `
Loading chapter...
`; // === CAMBIO: Si no tenemos la lista de capítulos (y no es local), la pedimos === if (provider !== 'local' && chaptersList.length === 0) { await fetchChapterList(); } let newEndpoint; if (provider === 'local') { newEndpoint = `/api/library/${bookId}/units`; } else { // === CAMBIO: Usamos currentChapterId en la URL === newEndpoint = `/api/book/${bookId}/${currentChapterId}/${provider}?source=${source}&lang=${lang}`; } try { const res = await fetch(newEndpoint); const data = await res.json(); const chapterMeta = chaptersList.find( c => String(c.id) === String(currentChapterId) ); if (chapterMeta) { chapterLabel.textContent = `Chapter ${chapterMeta.number} - ${chapterMeta.title}`; document.title = `Chapter ${chapterMeta.number} - ${chapterMeta.title}`; } // Lógica específica para contenido LOCAL if (provider === 'local') { const unitIndex = Number(currentChapterId); // En local el ID suele ser el índice const unit = data.units[unitIndex]; if (!unit) { reader.innerHTML = '
Chapter not found (Local)
'; return; } const manifestRes = await fetch(`/api/library/${unit.id}/manifest`); const manifest = await manifestRes.json(); reader.innerHTML = ''; // Setup navegación manual para local (simple index +/- 1) setupLocalNavigation(unitIndex, data.units.length); if (manifest.type === 'manga') { currentType = 'manga'; updateSettingsVisibility(); applyStyles(); currentPages = manifest.pages; loadManga(currentPages); return; } if (manifest.type === 'ln') { currentType = 'ln'; updateSettingsVisibility(); applyStyles(); const contentRes = await fetch(manifest.url); const html = await contentRes.text(); loadLN(html); return; } } const rawSource = urlParams.get('source') || 'anilist'; const source = rawSource.split('?')[0]; const res2 = await fetch(`/api/book/${bookId}?source=${source}`); const data2 = await res2.json(); fetch("/api/rpc", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ details: data2.title.romaji ?? data2.title, state: `Chapter ${data.title}`, mode: "reading" }) }); if (data.error) { reader.innerHTML = `
Error: ${data.error}
`; return; } if (!chapterMeta) { if (data.title) { chapterLabel.textContent = `Chapter ${data.number ?? ''} - ${data.title}`; document.title = chapterLabel.textContent; } else { chapterLabel.textContent = `Chapter ${data.number ?? currentChapterId}`; document.title = chapterLabel.textContent; } } setupProgressTracking(data, source); // === CAMBIO: Actualizar botones basado en IDs === updateNavigationButtons(); currentType = data.type; updateSettingsVisibility(); applyStyles(); reader.innerHTML = ''; if (data.type === 'manga') { currentPages = data.pages || []; loadManga(currentPages); } else if (data.type === 'ln') { loadLN(data.content); } } catch (error) { console.error(error); reader.innerHTML = `
Error loading chapter: ${error.message}
`; } } // === CAMBIO: Lógica de navegación basada en IDs === function updateNavigationButtons() { if (provider === 'local') return; // Se maneja aparte // Buscamos el índice actual en la lista completa const currentIndex = chaptersList.findIndex(c => String(c.id) === String(currentChapterId)); if (currentIndex === -1) { console.warn("Current chapter not found in list, navigation disabled"); prevBtn.disabled = true; nextBtn.disabled = true; prevBtn.style.opacity = 0.5; nextBtn.style.opacity = 0.5; return; } // Configurar botón ANTERIOR if (currentIndex > 0) { const prevId = chaptersList[currentIndex - 1].id; prevBtn.onclick = () => changeChapter(prevId); prevBtn.disabled = false; prevBtn.style.opacity = 1; } else { prevBtn.onclick = null; prevBtn.disabled = true; prevBtn.style.opacity = 0.5; } // Configurar botón SIGUIENTE if (currentIndex < chaptersList.length - 1) { const nextId = chaptersList[currentIndex + 1].id; nextBtn.onclick = () => changeChapter(nextId); nextBtn.disabled = false; nextBtn.style.opacity = 1; } else { nextBtn.onclick = null; nextBtn.disabled = true; nextBtn.style.opacity = 0.5; } } // Fallback para navegación local (basada en índices) function setupLocalNavigation(currentIndex, totalUnits) { if (currentIndex > 0) { prevBtn.onclick = () => changeChapter(currentIndex - 1); prevBtn.disabled = false; prevBtn.style.opacity = 1; } else { prevBtn.disabled = true; prevBtn.style.opacity = 0.5; } if (currentIndex < totalUnits - 1) { nextBtn.onclick = () => changeChapter(currentIndex + 1); nextBtn.disabled = false; nextBtn.style.opacity = 1; } else { nextBtn.disabled = true; nextBtn.style.opacity = 0.5; } } // === CAMBIO: Función helper para cambiar de capítulo === function changeChapter(newId) { currentChapterId = newId; updateURL(newId); window.scrollTo(0, 0); loadChapter(); } function updateURL(newId) { // La URL ahora contiene el ID en lugar del número/índice const newUrl = `/read/${provider}/${newId}/${bookId}?source=${source}&lang=${lang}`; window.history.pushState({}, '', newUrl); } // --- Resto de funciones UI (Manga/LN loading) sin cambios lógicos mayores --- function loadManga(pages) { if (!pages || pages.length === 0) { reader.innerHTML = '
No pages found
'; return; } const container = document.createElement('div'); container.className = 'manga-container'; let isLongStrip = false; if (config.manga.mode === 'longstrip') { isLongStrip = true; } else if (config.manga.mode === 'auto' && detectLongStrip(pages)) { isLongStrip = true; } const useDouble = config.manga.mode === 'double' || (config.manga.mode === 'auto' && !isLongStrip && shouldUseDoublePage(pages)); if (useDouble) { loadDoublePage(container, pages); } else { loadSinglePage(container, pages); } reader.appendChild(container); setupLazyLoading(); enableMangaPageNavigation(); } function shouldUseDoublePage(pages) { if (pages.length <= 5) return false; const widePages = pages.filter(p => { if (!p.height || !p.width) return false; const ratio = p.width / p.height; return ratio > 1.3; }); if (widePages.length > pages.length * 0.3) return false; return true; } function loadSinglePage(container, pages) { pages.forEach((page, index) => { const img = createImageElement(page, index); container.appendChild(img); }); } function loadDoublePage(container, pages) { let i = 0; while (i < pages.length) { const currentPage = pages[i]; const nextPage = pages[i + 1]; const isWide = currentPage.width && currentPage.height && (currentPage.width / currentPage.height) > 1.1; if (isWide) { const img = createImageElement(currentPage, i); container.appendChild(img); i++; } else { const doubleContainer = document.createElement('div'); doubleContainer.className = 'double-container'; const leftPage = createImageElement(currentPage, i); if (nextPage) { const nextIsWide = nextPage.width && nextPage.height && (nextPage.width / nextPage.height) > 1.3; if (nextIsWide) { const singleImg = createImageElement(currentPage, i); container.appendChild(singleImg); i++; } else { const rightPage = createImageElement(nextPage, i + 1); if (config.manga.direction === 'rtl') { doubleContainer.appendChild(rightPage); doubleContainer.appendChild(leftPage); } else { doubleContainer.appendChild(leftPage); doubleContainer.appendChild(rightPage); } container.appendChild(doubleContainer); i += 2; } } else { const singleImg = createImageElement(currentPage, i); container.appendChild(singleImg); i++; } } } } function createImageElement(page, index) { const img = document.createElement('img'); img.className = 'page-img'; img.dataset.index = index; const url = provider === 'local' ? page.url : buildProxyUrl(page.url, page.headers); const placeholder = "/public/assets/placeholder.svg"; img.onerror = () => { if (img.src !== placeholder) { img.src = placeholder; } }; img.onload = () => { const ratio = img.naturalWidth / img.naturalHeight; if (ratio > 1.3) { const double = img.closest('.double-container'); if (double) { double.replaceWith(img); } } }; if (config.manga.mode === 'longstrip' && index > 0) { img.classList.add('longstrip-fit'); } else { if (config.manga.imageFit === 'width') img.classList.add('fit-width'); else if (config.manga.imageFit === 'height') img.classList.add('fit-height'); else if (config.manga.imageFit === 'screen') img.classList.add('fit-screen'); } if (index < config.manga.preloadCount) { img.src = url; } else { img.dataset.src = url; img.loading = 'lazy'; } img.alt = `Page ${index + 1}`; return img; } function buildProxyUrl(url, headers = {}) { const params = new URLSearchParams({ url }); if (headers.Referer || headers.referer) params.append("referer", headers.Referer || headers.referer); if (headers["User-Agent"] || headers["user-agent"]) params.append("userAgent", headers["User-Agent"] || headers["user-agent"]); if (headers.Origin || headers.origin) params.append("origin", headers.Origin || headers.origin); return `/api/proxy?${params.toString()}`; } function detectLongStrip(pages) { if (!pages || pages.length === 0) return false; const relevant = pages.slice(1); const tall = relevant.filter(p => p.height && p.width && (p.height / p.width) > 2); return tall.length >= 2 || (tall.length / relevant.length) > 0.3; } function setupLazyLoading() { if (observer) observer.disconnect(); observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { img.src = img.dataset.src; delete img.dataset.src; observer.unobserve(img); } } }); }, { rootMargin: '200px' }); document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img)); } function loadLN(html) { const div = document.createElement('div'); div.className = 'ln-content'; div.innerHTML = html; reader.appendChild(div); } // Listeners de configuración document.getElementById('font-size').addEventListener('input', (e) => { config.ln.fontSize = parseInt(e.target.value); document.getElementById('font-size-value').textContent = e.target.value + 'px'; applyStyles(); saveConfig(); }); document.getElementById('line-height').addEventListener('input', (e) => { config.ln.lineHeight = parseFloat(e.target.value); document.getElementById('line-height-value').textContent = e.target.value; applyStyles(); saveConfig(); }); document.getElementById('max-width').addEventListener('input', (e) => { config.ln.maxWidth = parseInt(e.target.value); document.getElementById('max-width-value').textContent = e.target.value + 'px'; applyStyles(); saveConfig(); }); document.getElementById('font-family').addEventListener('change', (e) => { config.ln.fontFamily = e.target.value; applyStyles(); saveConfig(); }); document.getElementById('text-color').addEventListener('change', (e) => { config.ln.textColor = e.target.value; applyStyles(); saveConfig(); }); document.getElementById('bg-color').addEventListener('change', (e) => { config.ln.bg = e.target.value; applyStyles(); saveConfig(); }); document.querySelectorAll('[data-align]').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('[data-align]').forEach(b => b.classList.remove('active')); btn.classList.add('active'); config.ln.textAlign = btn.dataset.align; applyStyles(); saveConfig(); }); }); document.querySelectorAll('[data-preset]').forEach(btn => { btn.addEventListener('click', () => { const preset = btn.dataset.preset; const presets = { dark: { bg: '#14141b', textColor: '#e5e7eb' }, sepia: { bg: '#f4ecd8', textColor: '#5c472d' }, light: { bg: '#fafafa', textColor: '#1f2937' }, amoled: { bg: '#000000', textColor: '#ffffff' } }; if (presets[preset]) { Object.assign(config.ln, presets[preset]); document.getElementById('bg-color').value = config.ln.bg; document.getElementById('text-color').value = config.ln.textColor; applyStyles(); saveConfig(); } }); }); document.getElementById('display-mode').addEventListener('change', (e) => { config.manga.mode = e.target.value; saveConfig(); loadChapter(); }); document.getElementById('image-fit').addEventListener('change', (e) => { config.manga.imageFit = e.target.value; saveConfig(); loadChapter(); }); document.getElementById('page-spacing').addEventListener('input', (e) => { config.manga.spacing = parseInt(e.target.value); document.getElementById('page-spacing-value').textContent = e.target.value + 'px'; applyStyles(); saveConfig(); }); document.getElementById('preload-count').addEventListener('change', (e) => { config.manga.preloadCount = parseInt(e.target.value); saveConfig(); }); document.querySelectorAll('[data-direction]').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('[data-direction]').forEach(b => b.classList.remove('active')); btn.classList.add('active'); config.manga.direction = btn.dataset.direction; saveConfig(); loadChapter(); }); }); // Botón "Atrás" document.getElementById('back-btn').addEventListener('click', () => { if (source === 'anilist' || !source) { window.location.href = `/book/${bookId}`; } else { window.location.href = `/book/${source}/${bookId}`; } }); // Panel de configuración settingsBtn.addEventListener('click', () => { panel.classList.add('open'); overlay.classList.add('active'); }); closePanel.addEventListener('click', closeSettings); overlay.addEventListener('click', closeSettings); function closeSettings() { panel.classList.remove('open'); overlay.classList.remove('active'); } document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && panel.classList.contains('open')) { closeSettings(); } }); function enableMangaPageNavigation() { if (currentType !== 'manga') return; const logicalPages = []; document.querySelectorAll('.manga-container > *').forEach(el => { if (el.classList.contains('double-container') || el.tagName === 'IMG') { logicalPages.push(el); } }); if (logicalPages.length === 0) return; function scrollToLogical(index) { if (index < 0 || index >= logicalPages.length) return; const topBar = document.querySelector('.top-bar'); const offset = topBar ? -topBar.offsetHeight : 0; const y = logicalPages[index].getBoundingClientRect().top + window.pageYOffset + offset; window.scrollTo({ top: y, behavior: 'smooth' }); } function getCurrentLogicalIndex() { let closest = 0; let minDist = Infinity; logicalPages.forEach((el, i) => { const rect = el.getBoundingClientRect(); const dist = Math.abs(rect.top); if (dist < minDist) { minDist = dist; closest = i; } }); return closest; } const rtl = () => config.manga.direction === 'rtl'; document.addEventListener('keydown', (e) => { if (currentType !== 'manga') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; const index = getCurrentLogicalIndex(); if (e.key === 'ArrowLeft') scrollToLogical(rtl() ? index + 1 : index - 1); if (e.key === 'ArrowRight') scrollToLogical(rtl() ? index - 1 : index + 1); }); reader.addEventListener('click', (e) => { if (currentType !== 'manga') return; const bounds = reader.getBoundingClientRect(); const x = e.clientX - bounds.left; const half = bounds.width / 2; const index = getCurrentLogicalIndex(); if (x < half) scrollToLogical(rtl() ? index + 1 : index - 1); else scrollToLogical(rtl() ? index - 1 : index + 1); }); } let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { applyStyles(); }, 250); }); let progressSaved = false; function setupProgressTracking(data, source) { progressSaved = false; async function sendProgress(chapterNumber) { const token = localStorage.getItem('token'); if (!token) return; const body = { entry_id: bookId, source: source, entry_type: data.type === 'manga' ? 'MANGA' : 'NOVEL', status: 'CURRENT', progress: source === 'anilist' ? Math.floor(chapterNumber) : chapterNumber }; try { await fetch('/api/list/entry', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }); } catch (err) { console.error('Error updating progress:', err); } } function checkProgress() { const scrollTop = window.scrollY; const scrollHeight = document.documentElement.scrollHeight - window.innerHeight; const percent = scrollHeight > 0 ? scrollTop / scrollHeight : 0; if (percent >= 0.8 && !progressSaved) { progressSaved = true; // Usamos el número real del capítulo, no el ID const chapterNumber = (typeof data.number !== 'undefined' && data.number !== null) ? data.number : 0; // Fallback si no hay numero sendProgress(chapterNumber); window.removeEventListener('scroll', checkProgress); } } window.removeEventListener('scroll', checkProgress); window.addEventListener('scroll', checkProgress); } // Inicialización if (!bookId || !currentChapterId || !provider) { reader.innerHTML = `
Missing required parameters (bookId, chapterId, provider)
`; } else { loadConfig(); loadChapter(); }