From a6c753085eddeecdb54cbd0aadfde0fe0558ae03 Mon Sep 17 00:00:00 2001 From: lenafx Date: Thu, 27 Nov 2025 02:11:17 +0100 Subject: [PATCH] added reader --- public/book.js | 24 +- public/reader.css | 555 ++++++++++++++++++++++++++++++++++++++++++++++ public/reader.js | 515 ++++++++++++++++++++++++++++++++++++++++++ server.js | 80 ++++++- views/reader.html | 190 ++++++++++++++++ 5 files changed, 1350 insertions(+), 14 deletions(-) create mode 100644 public/reader.css create mode 100644 public/reader.js create mode 100644 views/reader.html diff --git a/public/book.js b/public/book.js index 1fb99f4..3df61c7 100644 --- a/public/book.js +++ b/public/book.js @@ -164,14 +164,17 @@ function renderTable() { pageItems.forEach(ch => { const row = document.createElement('tr'); + row.innerHTML = ` - ${ch.number} - ${ch.title || `Chapter ${ch.number}`} - ${ch.provider} - - - - `; + ${ch.number} + ${ch.title || 'Chapter ' + ch.number} + ${ch.provider} + + + + `; tbody.appendChild(row); }); @@ -202,9 +205,10 @@ function updatePagination() { nextBtn.onclick = () => { currentPage++; renderTable(); }; } -function openReader(chapterId) { - alert("Opening Reader for Chapter ID: " + chapterId); - // window.location.href = `/read/${bookId}/${chapterId}`; +function openReader(bookId, chapterId, provider) { + const c = encodeURIComponent(chapterId); + const p = encodeURIComponent(provider); + window.location.href = `/read/${bookId}/${c}/${p}`; } init(); \ No newline at end of file diff --git a/public/reader.css b/public/reader.css new file mode 100644 index 0000000..006aecd --- /dev/null +++ b/public/reader.css @@ -0,0 +1,555 @@ +:root { + --bg-base: #0a0a0f; + --bg-surface: #14141b; + --bg-elevated: #1c1c26; + --bg-hover: #252530; + --accent: #8b5cf6; + --accent-hover: #7c3aed; + --accent-light: rgba(139, 92, 246, 0.15); + --text-primary: #ffffff; + --text-secondary: #9ca3af; + --text-muted: #6b7280; + --border: rgba(255, 255, 255, 0.08); + --border-focus: rgba(139, 92, 246, 0.4); + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.5); + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-full: 9999px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: var(--bg-base); + color: var(--text-primary); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + overflow-x: hidden; + line-height: 1.6; +} + +.hidden { display: none !important; } + +/* ===== TOP BAR ===== */ +.top-bar { + position: fixed; + top: 0; left: 0; right: 0; + height: 64px; + background: rgba(10, 10, 15, 0.85); + backdrop-filter: blur(20px) saturate(180%); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.5rem; + z-index: 1000; + box-shadow: var(--shadow-sm); +} + +.glass-btn { + background: var(--bg-surface); + border: 1px solid var(--border); + color: var(--text-primary); + padding: 0.625rem 1.25rem; + border-radius: var(--radius-full); + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.glass-btn:hover { + background: var(--bg-hover); + border-color: var(--accent); + transform: translateY(-1px); +} + +.glass-btn:active { + transform: translateY(0); +} + +.chapter-info { + display: flex; + align-items: center; + gap: 1rem; + font-size: 0.95rem; + font-weight: 600; + color: var(--text-primary); +} + +.nav-arrow { + background: var(--bg-surface); + border: 1px solid var(--border); + width: 36px; + height: 36px; + border-radius: 50%; + font-size: 1.25rem; + cursor: pointer; + transition: all 0.2s; + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: center; +} + +.nav-arrow:hover { + background: var(--accent); + border-color: var(--accent); + transform: scale(1.05); +} + +.nav-arrow:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* ===== READER CONTAINER ===== */ +#reader { + margin-top: 64px; + padding: 2rem 1rem; + display: flex; + flex-direction: column; + align-items: center; + min-height: calc(100vh - 64px); + width: 100%; +} + +/* ===== MANGA STYLES ===== */ +.manga-container { + width: 100%; + max-width: var(--manga-max-width, 1200px); + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--page-spacing, 16px); +} + +.page-img { + width: 100%; + max-width: var(--page-max-width, 900px); + height: auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + transition: transform 0.3s ease; + cursor: zoom-in; +} + +.page-img:hover { + transform: scale(1.01); +} + +.page-img.zoomed { + cursor: zoom-out; + max-width: 100%; + position: relative; +} + +.double-container { + display: flex; + gap: var(--page-spacing, 16px); + width: 100%; + max-width: var(--manga-max-width, 1400px); + justify-content: center; +} + +.double-container img { + width: 48%; + max-width: 700px; + height: auto; + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + object-fit: contain; + cursor: zoom-in; + transition: transform 0.3s ease; +} + +.double-container img:hover { + transform: scale(1.02); +} + +/* ===== LIGHT NOVEL STYLES ===== */ +.ln-content { + max-width: var(--ln-max-width, 750px); + width: 100%; + margin: 0 auto; + padding: 3rem 2.5rem; + line-height: var(--ln-line-height, 1.8); + font-size: var(--ln-font-size, 18px); + font-family: var(--ln-font-family, 'Georgia', serif); + color: var(--ln-text-color, #e5e7eb); + background: var(--ln-bg, #14141b); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + text-align: var(--ln-text-align, left); +} + +.ln-content p { + margin-bottom: 1.5em; +} + +.ln-content h1, .ln-content h2, .ln-content h3 { + margin-top: 2em; + margin-bottom: 1em; + font-weight: 700; +} + +/* ===== SETTINGS PANEL ===== */ +.settings-panel { + position: fixed; + right: 0; + top: 64px; + bottom: 0; + width: 400px; + background: var(--bg-surface); + border-left: 1px solid var(--border); + padding: 0; + z-index: 1001; + transform: translateX(100%); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: var(--shadow-lg); + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.settings-panel.open { + transform: translateX(0); +} + +.panel-header { + position: sticky; + top: 0; + background: var(--bg-elevated); + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 1px solid var(--border); + z-index: 10; +} + +.panel-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 700; +} + +.close-btn { + background: var(--bg-surface); + border: 1px solid var(--border); + width: 32px; + height: 32px; + border-radius: 50%; + font-size: 1.25rem; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.close-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.panel-content { + flex: 1; + padding: 1.5rem; + overflow-y: auto; +} + +.settings-group { + margin-bottom: 2rem; +} + +.settings-group h4 { + margin: 0 0 1rem 0; + color: var(--accent); + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; +} + +.control { + margin-bottom: 1.25rem; +} + +.control label { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.625rem; + font-size: 0.875rem; + color: var(--text-secondary); + font-weight: 500; +} + +.control label span { + color: var(--text-primary); + font-weight: 600; + font-family: 'JetBrains Mono', monospace; + font-size: 0.8125rem; +} + +/* Range Inputs */ +input[type="range"] { + width: 100%; + height: 6px; + background: var(--bg-elevated); + border-radius: var(--radius-full); + outline: none; + -webkit-appearance: none; + cursor: pointer; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: var(--accent); + border-radius: 50%; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4); +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.6); +} + +input[type="range"]::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--accent); + border-radius: 50%; + cursor: pointer; + border: none; + transition: all 0.2s; +} + +/* Select & Color Inputs */ +select, input[type="color"], input[type="number"] { + width: 100%; + padding: 0.625rem 0.875rem; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 0.875rem; + transition: all 0.2s; + cursor: pointer; +} + +select:hover, input[type="color"]:hover, input[type="number"]:hover { + border-color: var(--accent); +} + +select:focus, input[type="color"]:focus, input[type="number"]:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-light); +} + +input[type="color"] { + height: 44px; + padding: 0.25rem; + cursor: pointer; +} + +/* Presets */ +.presets { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; +} + +.presets button { + padding: 0.75rem; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; + font-weight: 600; + font-size: 0.875rem; +} + +.presets button:hover { + background: var(--accent); + border-color: var(--accent); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +/* Toggle Switches */ +.toggle-group { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.toggle-btn { + flex: 1; + padding: 0.5rem 1rem; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; + font-size: 0.8125rem; + font-weight: 500; + text-align: center; +} + +.toggle-btn:hover { + border-color: var(--accent); + color: var(--text-primary); +} + +.toggle-btn.active { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +/* Overlay */ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); + z-index: 1000; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; +} + +.overlay.active { + opacity: 1; + pointer-events: all; +} + +/* Loading State */ +.loading-spinner { + display: inline-block; + width: 40px; + height: 40px; + border: 3px solid var(--bg-elevated); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + gap: 1rem; + color: var(--text-secondary); +} + +/* Divider */ +.divider { + height: 1px; + background: var(--border); + margin: 1.5rem 0; +} + +/* Scrollbar */ +.settings-panel::-webkit-scrollbar { + width: 8px; +} + +.settings-panel::-webkit-scrollbar-track { + background: var(--bg-surface); +} + +.settings-panel::-webkit-scrollbar-thumb { + background: var(--bg-elevated); + border-radius: 4px; +} + +.settings-panel::-webkit-scrollbar-thumb:hover { + background: var(--bg-hover); +} + +/* Responsive */ +@media (max-width: 768px) { + .settings-panel { + width: 100%; + max-width: 100%; + } + + .top-bar { + padding: 0 1rem; + } + + .glass-btn { + padding: 0.5rem 1rem; + font-size: 0.8125rem; + } + + .chapter-info { + font-size: 0.875rem; + gap: 0.75rem; + } + + .double-container { + flex-direction: column; + } + + .double-container img { + width: 100%; + max-width: 100%; + } + + .ln-content { + padding: 2rem 1.5rem; + font-size: var(--ln-font-size, 16px); + } +} + +/* Image Fit Modes */ +.fit-width { + width: 100% !important; + height: auto !important; + max-width: 100% !important; +} + +.fit-height { + height: var(--viewport-height, 85vh) !important; + width: auto !important; + max-width: 100% !important; +} + +.fit-screen { + max-height: var(--viewport-height, 85vh) !important; + max-width: 100% !important; + width: auto !important; + height: auto !important; + object-fit: contain !important; +} \ No newline at end of file diff --git a/public/reader.js b/public/reader.js new file mode 100644 index 0000000..ee53c8d --- /dev/null +++ b/public/reader.js @@ -0,0 +1,515 @@ +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', + maxWidth: 900, + quality: 'high', + 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('manga-max-width').value = config.manga.maxWidth; + document.getElementById('manga-max-width-value').textContent = config.manga.maxWidth + 'px'; + 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); + }); + + // Quality buttons + document.querySelectorAll('[data-quality]').forEach(btn => { + btn.classList.toggle('active', btn.dataset.quality === config.manga.quality); + }); +} + +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('--ln-bg', 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', config.manga.maxWidth + 'px'); + document.documentElement.style.setProperty('--manga-max-width', config.manga.maxWidth + 'px'); + + const viewportHeight = window.innerHeight - 64 - 32; // header + padding + 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'; + + const isLongStrip = config.manga.mode === 'longstrip' || + (config.manga.mode === 'auto' && detectLongStrip(pages)); + + const useDouble = config.manga.mode === 'double' || + (config.manga.mode === 'auto' && !isLongStrip && pages.length > 5); + + if (useDouble) { + loadDoublePage(container, pages); + } else { + loadSinglePage(container, pages); + } + + reader.appendChild(container); + setupLazyLoading(); +} + +function loadSinglePage(container, pages) { + pages.forEach((page, index) => { + const img = createImageElement(page.url, index); + container.appendChild(img); + }); +} + +function loadDoublePage(container, pages) { + for (let i = 0; i < pages.length; i += 2) { + const doubleContainer = document.createElement('div'); + doubleContainer.className = 'double-container'; + + const leftPage = createImageElement(pages[i].url, i); + + if (pages[i + 1]) { + const rightPage = createImageElement(pages[i + 1].url, i + 1); + + if (config.manga.direction === 'rtl') { + doubleContainer.appendChild(rightPage); + doubleContainer.appendChild(leftPage); + } else { + doubleContainer.appendChild(leftPage); + doubleContainer.appendChild(rightPage); + } + } else { + doubleContainer.appendChild(leftPage); + } + + container.appendChild(doubleContainer); + } +} + +function createImageElement(url, index) { + const img = document.createElement('img'); + img.className = 'page-img'; + img.dataset.index = index; + + 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'); + } + + // Preload o lazy load + if (index < config.manga.preloadCount) { + img.src = buildProxyUrl(url); + } else { + img.dataset.src = buildProxyUrl(url); + img.loading = 'lazy'; + } + + img.alt = `Page ${index + 1}`; + + return img; +} + +function buildProxyUrl(url) { + return `/api/proxy?url=${encodeURIComponent(url)}&referer=https%3A%2F%2Fmangapark.net`; +} + +function detectLongStrip(pages) { + if (!pages || pages.length === 0) return false; + const tallPages = pages.filter(p => { + if (!p.height || !p.width) return false; + return (p.height / p.width) > 2.5; + }); + return tallPages.length >= Math.min(4, pages.length * 0.5); +} + +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(); +}); + +// 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(); + } + }); +}); + +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('manga-max-width').addEventListener('input', (e) => { + config.manga.maxWidth = parseInt(e.target.value); + document.getElementById('manga-max-width-value').textContent = e.target.value + 'px'; + applyStyles(); + saveConfig(); +}); + +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(); + }); +}); + +// Quality +document.querySelectorAll('[data-quality]').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('[data-quality]').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + config.manga.quality = btn.dataset.quality; + saveConfig(); + }); +}); + +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', () => { + 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(); + } +}); + +document.addEventListener('keydown', (e) => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; + + switch(e.key) { + case 'ArrowLeft': + if (config.manga.direction === 'rtl') { + nextBtn.click(); + } else { + prevBtn.click(); + } + break; + case 'ArrowRight': + if (config.manga.direction === 'rtl') { + prevBtn.click(); + } else { + nextBtn.click(); + } + break; + case 's': + case 'S': + settingsBtn.click(); + break; + } +}); + +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(); +} \ No newline at end of file diff --git a/server.js b/server.js index 58af515..cfa7225 100644 --- a/server.js +++ b/server.js @@ -31,8 +31,8 @@ async function loadExtensions() { try { delete require.cache[require.resolve(filePath)]; const ExtensionClass = require(filePath); - const instance = typeof ExtensionClass === 'function' - ? new ExtensionClass() + const instance = typeof ExtensionClass === 'function' + ? new ExtensionClass() : (ExtensionClass.default ? new ExtensionClass.default() : null); if (instance && (instance.type === "anime-board" || instance.type === "book-board")) { @@ -351,7 +351,7 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => { .map(async ([name, ext]) => { try { console.log(`[${name}] Searching chapters for: ${searchTitle}`); - + // Pass strict search options const matches = await ext.search({ query: searchTitle, @@ -366,7 +366,7 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => { // Use the first match to find chapters const best = matches[0]; const chaps = await ext.findChapters(best.id); - + if (chaps && chaps.length > 0) { console.log(`[${name}] Found ${chaps.length} chapters.`); chaps.forEach(ch => { @@ -398,6 +398,73 @@ fastify.get('/api/book/:id/chapters', async (req, reply) => { return { chapters: sortedChapters }; }); +fastify.get('/api/book/:bookId/:chapter/:provider', async (req, reply) => { + const { bookId, chapter, provider } = req.params; + + const ext = extensions.get(provider); + if (!ext) + return reply.code(404).send({ error: "Provider not found" }); + + let chapterId = decodeURIComponent(chapter); + let chapterTitle = null; + let chapterNumber = null; + + const index = parseInt(chapter); + const chapterList = await fetch( + `http://localhost:3000/api/book/${bookId}/chapters` + ).then(r => r.json()); + + if (!chapterList?.chapters) + return reply.code(404).send({ error: "Chapters not found" }); + + const providerChapters = chapterList.chapters.filter( + c => c.provider === provider + ); + + if (!providerChapters[index]) + return reply.code(404).send({ error: "Chapter index out of range" }); + + const selected = providerChapters[index]; + + chapterId = selected.id; + chapterTitle = selected.title || null; + chapterNumber = selected.number || index; + + + try { + if (ext.mediaType === "manga") { + const pages = await ext.findChapterPages(chapterId); + return reply.send({ + type: "manga", + chapterId, + title: chapterTitle, + number: chapterNumber, + provider, + pages + }); + } + + if (ext.mediaType === "ln") { + const content = await ext.findChapterPages(chapterId); + return reply.send({ + type: "ln", + chapterId, + title: chapterTitle, + number: chapterNumber, + provider, + content + }); + } + + return reply.code(400).send({ error: "Unknown mediaType" }); + + } catch (err) { + console.error(err); + return reply.code(500).send({ error: "Error loading chapter" }); + } +}); + + fastify.get('/api/book/:id', async (req, reply) => { const id = req.params.id; @@ -493,6 +560,11 @@ fastify.get('/api/top-airing', (req, reply) => { return new Promise((resolve) => db.all("SELECT full_data FROM top_airing ORDER BY rank ASC LIMIT 10", [], (err, rows) => resolve({ results: rows ? rows.map(r => JSON.parse(r.full_data)) : [] }))); }); +fastify.get('/read/:id/:chapter/:provider', (req, reply) => { + const stream = fs.createReadStream(path.join(__dirname, 'views', 'reader.html')); + reply.type('text/html').send(stream); +}); + const start = async () => { try { await fastify.listen({ port: 3000, host: '0.0.0.0' }); diff --git a/views/reader.html b/views/reader.html new file mode 100644 index 0000000..de595dc --- /dev/null +++ b/views/reader.html @@ -0,0 +1,190 @@ + + + + + + Reader + + + + + + +
+ + +
+ + Loading... + +
+ + +
+ + + + +
+ +
+
+
+ Loading chapter... +
+
+ + + + \ No newline at end of file