// /app/static/js/reader.js 'use strict'; document.addEventListener('DOMContentLoaded', () => { const { startPage } = window.readerConfig || { startPage: 1 }; // Elements // #readerContent is the actual scroll container (overflow-y: auto) const readerContent = document.getElementById('readerContent'); const pageJumpSelect = document.getElementById('pageJumpSelect'); const floatingIndicator = document.getElementById('floatingPageIndicator'); const currentPageNum = document.getElementById('currentPageNum'); const readerNav = document.getElementById('readerNav'); const readerHeader = document.querySelector('.reader-header'); const toggleNavBtn = document.getElementById('toggleNavBtn'); const closeNavBtn = document.getElementById('closeNavBtn'); const navThumbs = document.querySelectorAll('.reader-nav-thumb'); const scrollToTopBtn = document.getElementById('scrollToTopBtn'); const scrollToBottomBtn = document.getElementById('scrollToBottomBtn'); const pages = document.querySelectorAll('.reader-page'); const progressBar = document.getElementById('readerProgressBar'); let currentPage = startPage; let isNavOpen = false; let headerHideTimeout = null; // ------------------------- // Scroll to starting page // ------------------------- if (startPage > 1) { const targetPage = document.getElementById('page-' + startPage); if (targetPage) { setTimeout(() => targetPage.scrollIntoView({ behavior: 'auto' }), 100); } } // ------------------------- // Progress bar // ------------------------- function updateProgressBar() { if (!progressBar || !readerContent) return; const scrollTop = readerContent.scrollTop; const scrollHeight = readerContent.scrollHeight - readerContent.clientHeight; const pct = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0; progressBar.style.width = `${Math.min(pct, 100)}%`; } // ------------------------- // Auto-hide header // ------------------------- function showHeader() { if (!readerHeader) return; readerHeader.classList.remove('hidden'); clearTimeout(headerHideTimeout); } function scheduleHideHeader() { clearTimeout(headerHideTimeout); headerHideTimeout = setTimeout(() => { if (readerHeader) readerHeader.classList.add('hidden'); }, 2500); } // ------------------------- // Current page tracking // ------------------------- function updateCurrentPage() { if (!readerContent) return; const scrollTop = readerContent.scrollTop; const viewportCenter = scrollTop + (readerContent.clientHeight / 3); let newCurrentPage = currentPage; pages.forEach(page => { const pageTop = page.offsetTop; const pageBottom = pageTop + page.offsetHeight; if (viewportCenter >= pageTop && viewportCenter < pageBottom) { newCurrentPage = parseInt(page.dataset.page); } }); if (newCurrentPage !== currentPage) { currentPage = newCurrentPage; // Update floating indicator if (currentPageNum) currentPageNum.textContent = currentPage; // Update jump select if (pageJumpSelect) pageJumpSelect.value = currentPage; // Update active nav thumb + scroll it into view navThumbs.forEach(thumb => { const isActive = parseInt(thumb.dataset.page) === currentPage; thumb.classList.toggle('active', isActive); if (isActive) { thumb.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } }); // Update URL without reload const url = new URL(window.location); url.searchParams.set('page', currentPage); history.replaceState({}, '', url); } } // ------------------------- // Scroll handler // ------------------------- let scrollTimeout = null; if (readerContent) { readerContent.addEventListener('scroll', () => { updateProgressBar(); showHeader(); scheduleHideHeader(); if (scrollTimeout) return; scrollTimeout = setTimeout(() => { updateCurrentPage(); scrollTimeout = null; }, 50); }); } // Show header on mouse move document.addEventListener('mousemove', () => { showHeader(); scheduleHideHeader(); }); // ------------------------- // Jump to page // ------------------------- if (pageJumpSelect) { pageJumpSelect.addEventListener('change', (e) => { const pageNum = parseInt(e.target.value); const targetPage = document.getElementById('page-' + pageNum); if (targetPage) targetPage.scrollIntoView({ behavior: 'smooth' }); }); } // ------------------------- // Sidebar toggle // ------------------------- if (toggleNavBtn) { toggleNavBtn.addEventListener('click', () => { isNavOpen = !isNavOpen; readerNav.classList.toggle('open', isNavOpen); }); } if (closeNavBtn) { closeNavBtn.addEventListener('click', () => { isNavOpen = false; readerNav.classList.remove('open'); }); } // Click thumbnail to jump navThumbs.forEach(thumb => { thumb.addEventListener('click', () => { const pageNum = parseInt(thumb.dataset.page); const targetPage = document.getElementById('page-' + pageNum); if (targetPage) targetPage.scrollIntoView({ behavior: 'smooth' }); if (window.innerWidth < 768) { isNavOpen = false; readerNav.classList.remove('open'); } }); }); // ------------------------- // Quick nav buttons // ------------------------- if (scrollToTopBtn) { scrollToTopBtn.addEventListener('click', () => { readerContent.scrollTo({ top: 0, behavior: 'smooth' }); }); } if (scrollToBottomBtn) { scrollToBottomBtn.addEventListener('click', () => { readerContent.scrollTo({ top: readerContent.scrollHeight, behavior: 'smooth' }); }); } // ------------------------- // Keyboard navigation // ------------------------- document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; if (!readerContent) return; showHeader(); switch (e.key) { case 'Home': e.preventDefault(); readerContent.scrollTo({ top: 0, behavior: 'smooth' }); break; case 'End': e.preventDefault(); readerContent.scrollTo({ top: readerContent.scrollHeight, behavior: 'smooth' }); break; case 'PageUp': e.preventDefault(); readerContent.scrollBy({ top: -readerContent.clientHeight * 0.9, behavior: 'smooth' }); break; case 'PageDown': case ' ': e.preventDefault(); readerContent.scrollBy({ top: readerContent.clientHeight * 0.9, behavior: 'smooth' }); break; case 'n': isNavOpen = !isNavOpen; readerNav.classList.toggle('open', isNavOpen); break; } }); // ------------------------- // Initial state // ------------------------- updateCurrentPage(); updateProgressBar(); });