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 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; const parts = window.location.pathname.split('/'); const bookId = parts[4]; let chapter = parts[3]; let provider = parts[2]; 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'); } async function loadChapter() { reader.innerHTML = `
Loading chapter...
`; const urlParams = new URLSearchParams(window.location.search); let source = urlParams.get('source'); if (!source) { source = 'anilist'; } let newEndpoint; if (provider === 'local') { newEndpoint = `/api/library/${bookId}/units`; } else { newEndpoint = `/api/book/${bookId}/${chapter}/${provider}?source=${source}`; } try { const res = await fetch(newEndpoint); const data = await res.json(); if (provider === 'local') { const unit = data.units[Number(chapter)]; if (!unit) return; chapterLabel.textContent = unit.name; document.title = unit.name; const manifestRes = await fetch(`/api/library/${unit.id}/manifest`); const manifest = await manifestRes.json(); reader.innerHTML = ''; // ===== MANGA ===== if (manifest.type === 'manga') { currentType = 'manga'; updateSettingsVisibility(); applyStyles(); currentPages = manifest.pages; loadManga(currentPages); return; } // ===== LN ===== if (manifest.type === 'ln') { currentType = 'ln'; updateSettingsVisibility(); applyStyles(); const contentRes = await fetch(manifest.url); const html = await contentRes.text(); loadLN(html); return; } } if (data.title) { chapterLabel.textContent = data.title; document.title = data.title; } else { chapterLabel.textContent = `Chapter ${chapter}`; document.title = `Chapter ${chapter}`; } setupProgressTracking(data, source); 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; } 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) { reader.innerHTML = `
Error loading chapter: ${error.message}
`; } } 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; } }; 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); } 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(); }); }); prevBtn.addEventListener('click', () => { const current = parseInt(chapter); if (current <= 0) return; const newChapter = String(current - 1); updateURL(newChapter); window.scrollTo(0, 0); loadChapter(); }); nextBtn.addEventListener('click', () => { const newChapter = String(parseInt(chapter) + 1); updateURL(newChapter); window.scrollTo(0, 0); loadChapter(); }); function updateURL(newChapter) { chapter = newChapter; const urlParams = new URLSearchParams(window.location.search); let source = urlParams.get('source'); let src; if (source === 'anilist') { src= "?source=anilist" } else { src= `?source=${source}` } const newUrl = `/read/${provider}/${chapter}/${bookId}${src}`; window.history.pushState({}, '', newUrl); } document.getElementById('back-btn').addEventListener('click', () => { const parts = window.location.pathname.split('/'); const mangaId = parts[4]; const urlParams = new URLSearchParams(window.location.search); let source = urlParams.get('source'); if (source === 'anilist') { window.location.href = `/book/${mangaId}`; } else { window.location.href = `/book/${source}/${mangaId}`; } }); 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')) { logicalPages.push(el); } else if (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; const chapterNumber = (typeof data.number !== 'undefined' && data.number !== null) ? data.number : Number(chapter); sendProgress(chapterNumber); window.removeEventListener('scroll', checkProgress); } } window.removeEventListener('scroll', checkProgress); window.addEventListener('scroll', checkProgress); } if (!bookId || !chapter || !provider) { reader.innerHTML = `
Missing required parameters (bookId, chapter, provider)
`; } else { loadConfig(); loadChapter(); }