diff --git a/README.md b/README.md index daf62a1..fa1886f 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ The official recode repo, its private no one should know about this or have the | Task | Status | Notes | | -----|--------| ------ | -| Book Reader | Not Done | N/A | -| Multi book provider loading | Not Done | N/A | +| Book Reader | ✅ | N/A | +| Multi book provider loading | ✅ | N/A | | Better Code Organization | Not Done | N/A | | Mobile View | Not Done | N/A | | Gallery | Not Done | N/A | diff --git a/public/book.js b/public/book.js index 3df61c7..990835e 100644 --- a/public/book.js +++ b/public/book.js @@ -149,12 +149,12 @@ function populateProviderFilter() { function renderTable() { const tbody = document.getElementById('chapters-body'); if (!tbody) return; - + tbody.innerHTML = ''; if (filteredChapters.length === 0) { tbody.innerHTML = 'No chapters match this filter.'; - updatePagination(); // Update to hide buttons + updatePagination(); return; } @@ -162,19 +162,21 @@ function renderTable() { const end = start + itemsPerPage; const pageItems = filteredChapters.slice(start, end); - pageItems.forEach(ch => { + pageItems.forEach((ch, idx) => { + const realIndex = start + idx; + 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); }); @@ -206,6 +208,7 @@ function updatePagination() { } function openReader(bookId, chapterId, provider) { + localStorage.setItem('reader_prev_url', window.location.href); const c = encodeURIComponent(chapterId); const p = encodeURIComponent(provider); window.location.href = `/read/${bookId}/${c}/${p}`; diff --git a/public/reader.css b/public/reader.css index 006aecd..b57c4a6 100644 --- a/public/reader.css +++ b/public/reader.css @@ -141,18 +141,41 @@ body { height: auto; border-radius: var(--radius-md); box-shadow: var(--shadow-lg); - transition: transform 0.3s ease; - cursor: zoom-in; + transition: transform 0.2s ease, box-shadow 0.2s ease; + cursor: pointer; + display: block; } .page-img:hover { - transform: scale(1.01); + box-shadow: 0 24px 56px rgba(0, 0, 0, 0.6); } .page-img.zoomed { + position: fixed; + top: 64px; + left: 0; + right: 0; + bottom: 0; + max-width: 100vw; + max-height: calc(100vh - 64px); + width: auto; + height: auto; + margin: auto; + z-index: 999; + cursor: zoom-out; + border-radius: 0; + object-fit: contain; +} + +.zoom-overlay { + position: fixed; + top: 64px; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.95); + z-index: 998; cursor: zoom-out; - max-width: 100%; - position: relative; } .double-container { @@ -170,12 +193,12 @@ body { border-radius: var(--radius-md); box-shadow: var(--shadow-lg); object-fit: contain; - cursor: zoom-in; - transition: transform 0.3s ease; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; } .double-container img:hover { - transform: scale(1.02); + box-shadow: 0 24px 56px rgba(0, 0, 0, 0.6); } /* ===== LIGHT NOVEL STYLES ===== */ @@ -188,9 +211,6 @@ body { 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); } @@ -208,19 +228,19 @@ body { .settings-panel { position: fixed; right: 0; - top: 64px; + top: 0; 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; + background: var(--bg-surface); + border-left: 1px solid var(--border); + box-shadow: -10px 0 30px rgba(0, 0, 0, 0.6); } .settings-panel.open { @@ -230,13 +250,13 @@ body { .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; + background: #0a0a0f; + border-bottom: 1px solid var(--border); } .panel-header h3 { @@ -246,23 +266,24 @@ body { } .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; + background: var(--bg-elevated); + border: none; + color: var(--text-secondary); } .close-btn:hover { - background: var(--bg-hover); + background: var(--accent); color: var(--text-primary); + transform: rotate(90deg); } .panel-content { @@ -271,23 +292,34 @@ body { overflow-y: auto; } -.settings-group { +.settings-section { margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--border); } -.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; +.settings-section:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.settings-section h4 { + margin: 0 0 1.25rem 0; + color: var(--text-primary); + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.01em; } .control { margin-bottom: 1.25rem; } +.control:last-child { + margin-bottom: 0; +} + .control label { display: flex; justify-content: space-between; @@ -308,24 +340,24 @@ body { /* 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; + height: 8px; + background: var(--bg-hover); } 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); + width: 20px; + height: 20px; + background: var(--accent); } input[type="range"]::-webkit-slider-thumb:hover { @@ -347,13 +379,13 @@ input[type="range"]::-moz-range-thumb { 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; + background: var(--bg-elevated); + border: 1px solid var(--border); } select:hover, input[type="color"]:hover, input[type="number"]:hover { @@ -364,6 +396,7 @@ 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); + transform: translateY(-1px); } input[type="color"] { @@ -380,22 +413,25 @@ input[type="color"] { } .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; + padding: 1rem; + background: var(--bg-elevated); + border: 1px solid var(--border); + font-weight: 700; + letter-spacing: 0.02em; } .presets button:hover { + background: var(--accent); background: var(--accent); border-color: var(--accent); transform: translateY(-2px); - box-shadow: var(--shadow-md); + box-shadow: 0 4px 15px var(--accent-light); } /* Toggle Switches */ @@ -407,16 +443,17 @@ input[type="color"] { .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; + padding: 0.75rem 1rem; + background: var(--bg-elevated); + border: 1px solid var(--border); + color: var(--text-secondary); + font-weight: 600; } .toggle-btn:hover { @@ -427,7 +464,8 @@ input[type="color"] { .toggle-btn.active { background: var(--accent); border-color: var(--accent); - color: white; + color: var(--text-primary); + box-shadow: 0 2px 10px var(--accent-light); } /* Overlay */ @@ -435,7 +473,6 @@ input[type="color"] { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); - backdrop-filter: blur(4px); z-index: 1000; opacity: 0; pointer-events: none; @@ -472,13 +509,6 @@ input[type="color"] { color: var(--text-secondary); } -/* Divider */ -.divider { - height: 1px; - background: var(--border); - margin: 1.5rem 0; -} - /* Scrollbar */ .settings-panel::-webkit-scrollbar { width: 8px; @@ -552,4 +582,11 @@ input[type="color"] { width: auto !important; height: auto !important; object-fit: contain !important; +} + +.page-img.longstrip-fit { + width: 50%; + max-width: 50%; + margin: 0 auto; + display: block; } \ No newline at end of file diff --git a/public/reader.js b/public/reader.js index ee53c8d..d3e01e8 100644 --- a/public/reader.js +++ b/public/reader.js @@ -7,6 +7,8 @@ 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'); @@ -25,8 +27,6 @@ const config = { mode: 'auto', spacing: 16, imageFit: 'screen', - maxWidth: 900, - quality: 'high', preloadCount: 3 } }; @@ -86,8 +86,6 @@ function updateUIFromConfig() { // 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; @@ -96,11 +94,6 @@ function updateUIFromConfig() { 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() { @@ -110,16 +103,16 @@ function applyStyles() { 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('--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', config.manga.maxWidth + 'px'); - document.documentElement.style.setProperty('--manga-max-width', config.manga.maxWidth + '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; // header + padding + const viewportHeight = window.innerHeight - 64 - 32; document.documentElement.style.setProperty('--viewport-height', viewportHeight + 'px'); } } @@ -137,7 +130,6 @@ async function loadChapter() { `; - try { const res = await fetch(`/api/book/${bookId}/${chapter}/${provider}`); const data = await res.json(); @@ -173,7 +165,7 @@ async function loadChapter() { } catch (error) { reader.innerHTML = `
- ❌ Error loading chapter: ${error.message} + Error loading chapter: ${error.message}
`; } @@ -188,11 +180,17 @@ function loadManga(pages) { const container = document.createElement('div'); container.className = 'manga-container'; - const isLongStrip = config.manga.mode === 'longstrip' || - (config.manga.mode === 'auto' && detectLongStrip(pages)); + 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 && pages.length > 5); + (config.manga.mode === 'auto' && !isLongStrip && shouldUseDoublePage(pages)); if (useDouble) { loadDoublePage(container, pages); @@ -202,58 +200,99 @@ function loadManga(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.url, index); + const img = createImageElement(page, 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'; + let i = 0; + while (i < pages.length) { + const currentPage = pages[i]; + const nextPage = pages[i + 1]; - const leftPage = createImageElement(pages[i].url, i); + const isWide = currentPage.width && currentPage.height && + (currentPage.width / currentPage.height) > 1.1; - 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); - } + if (isWide) { + const img = createImageElement(currentPage, i); + container.appendChild(img); + i++; } else { - doubleContainer.appendChild(leftPage); - } + const doubleContainer = document.createElement('div'); + doubleContainer.className = 'double-container'; - container.appendChild(doubleContainer); + 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(url, index) { +function createImageElement(page, 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'); + 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'); } - // Preload o lazy load if (index < config.manga.preloadCount) { - img.src = buildProxyUrl(url); + img.src = url; } else { - img.dataset.src = buildProxyUrl(url); + img.dataset.src = url; img.loading = 'lazy'; } @@ -262,17 +301,24 @@ function createImageElement(url, index) { return img; } -function buildProxyUrl(url) { - return `/api/proxy?url=${encodeURIComponent(url)}&referer=https%3A%2F%2Fmangapark.net`; +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 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); + + 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() { @@ -303,6 +349,7 @@ function loadLN(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'; @@ -375,6 +422,8 @@ document.querySelectorAll('[data-preset]').forEach(btn => { }); }); +// Event Listeners - Manga + document.getElementById('display-mode').addEventListener('change', (e) => { config.manga.mode = e.target.value; saveConfig(); @@ -387,13 +436,6 @@ document.getElementById('image-fit').addEventListener('change', (e) => { 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'; @@ -417,16 +459,7 @@ document.querySelectorAll('[data-direction]').forEach(btn => { }); }); -// 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(); - }); -}); - +// Navigation prevBtn.addEventListener('click', () => { const newChapter = String(parseInt(chapter) - 1); updateURL(newChapter); @@ -448,7 +481,12 @@ function updateURL(newChapter) { } document.getElementById('back-btn').addEventListener('click', () => { - history.back(); + const prev = localStorage.getItem('reader_prev_url'); + if (prev) { + window.location.href = prev; + } else { + history.back(); + } }); settingsBtn.addEventListener('click', () => { @@ -470,30 +508,84 @@ document.addEventListener('keydown', (e) => { } }); -document.addEventListener('keydown', (e) => { - if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; +function enableMangaPageNavigation() { + if (currentType !== 'manga') return; + const logicalPages = []; - 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; + 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', () => { @@ -506,7 +598,7 @@ window.addEventListener('resize', () => { if (!bookId || !chapter || !provider) { reader.innerHTML = `
- Missing required parameters (bookId, chapter, provider) + Missing required parameters (bookId, chapter, provider)
`; } else { diff --git a/views/reader.html b/views/reader.html index de595dc..3b0999d 100644 --- a/views/reader.html +++ b/views/reader.html @@ -12,7 +12,7 @@
@@ -35,142 +35,135 @@
-