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 prevUrl = localStorage.getItem('reader_prev_url'); 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[2]; let chapter = parts[3]; let provider = parts[4]; 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() { // Light Novel 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; // Text alignment buttons document.querySelectorAll('[data-align]').forEach(btn => { btn.classList.toggle('active', btn.dataset.align === config.ln.textAlign); }); // Manga 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; // Direction buttons 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('--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...
`; try { const res = await fetch(`/api/book/${bookId}/${chapter}/${provider}`); const data = await res.json(); if (data.title) { chapterLabel.textContent = data.title; document.title = data.title; } else { chapterLabel.textContent = `Chapter ${chapter}`; document.title = `Chapter ${chapter}`; } 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 = buildProxyUrl(page.url, page.headers); 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) params.append('referer', headers.referer); if (headers['user-agent']) params.append('ua', headers['user-agent']); if (headers.cookie) params.append('cookie', headers.cookie); 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); } // Event Listeners - Light Novel 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(); }); // Text alignment 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(); }); }); // Presets 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(); } }); }); // Event Listeners - Manga 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(); }); // Direction 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(); }); }); // Navigation prevBtn.addEventListener('click', () => { const newChapter = String(parseInt(chapter) - 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 newUrl = `/reader/${bookId}/${chapter}/${provider}`; window.history.pushState({}, '', newUrl); } document.getElementById('back-btn').addEventListener('click', () => { const prev = localStorage.getItem('reader_prev_url'); if (prev) { window.location.href = prev; } else { history.back(); } }); 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); }); if (!bookId || !chapter || !provider) { reader.innerHTML = `
Missing required parameters (bookId, chapter, provider)
`; } else { loadConfig(); loadChapter(); }