This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
imagerepo/app/static/js/reader.js
T

225 lines
6.9 KiB
JavaScript

// /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();
});