feat: add progress bar, auto-hide header, active thumb tracking to series reader

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 08:59:29 -04:00
parent 8599ffb37c
commit 1e96208533
3 changed files with 268 additions and 155 deletions
+7 -147
View File
@@ -2,6 +2,8 @@
{% block title %}{{ series_name }} - Reader{% endblock %}
{% block content %}
<!-- Reading progress bar -->
<div id="readerProgressBar" class="reader-progress-bar"></div>
<div class="reader-container">
<!-- Reader Header -->
<div class="reader-header">
@@ -56,7 +58,6 @@
alt="Page {{ page.page_number }}"
loading="lazy"
data-image-id="{{ page.image.id }}">
<div class="page-indicator">{{ page.page_number }} / {{ total_pages }}</div>
</div>
{% endfor %}
{% else %}
@@ -84,151 +85,10 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
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 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 startPage = {{ start_page }};
let currentPage = startPage;
let isNavOpen = false;
// Scroll to starting page on load
if (startPage > 1) {
const targetPage = document.getElementById('page-' + startPage);
if (targetPage) {
setTimeout(() => {
targetPage.scrollIntoView({ behavior: 'auto' });
}, 100);
}
}
// Track current page while scrolling
function updateCurrentPage() {
const scrollTop = readerContent.scrollTop;
const viewportHeight = readerContent.clientHeight;
const viewportCenter = scrollTop + (viewportHeight / 3);
let newCurrentPage = 1;
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;
currentPageNum.textContent = currentPage;
pageJumpSelect.value = currentPage;
// Update nav thumb highlight
navThumbs.forEach(thumb => {
thumb.classList.toggle('active', parseInt(thumb.dataset.page) === currentPage);
});
// Update URL without reload
const url = new URL(window.location);
url.searchParams.set('page', currentPage);
history.replaceState({}, '', url);
}
}
// Throttled scroll handler
let scrollTimeout;
readerContent.addEventListener('scroll', () => {
if (scrollTimeout) return;
scrollTimeout = setTimeout(() => {
updateCurrentPage();
scrollTimeout = null;
}, 50);
});
// Jump to page from select
pageJumpSelect.addEventListener('change', (e) => {
const pageNum = parseInt(e.target.value);
const targetPage = document.getElementById('page-' + pageNum);
if (targetPage) {
targetPage.scrollIntoView({ behavior: 'smooth' });
}
});
// Toggle navigation sidebar
toggleNavBtn.addEventListener('click', () => {
isNavOpen = !isNavOpen;
readerNav.classList.toggle('open', isNavOpen);
});
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' });
}
// Close nav on mobile
if (window.innerWidth < 768) {
isNavOpen = false;
readerNav.classList.remove('open');
}
});
});
// Quick nav buttons
scrollToTopBtn.addEventListener('click', () => {
readerContent.scrollTo({ top: 0, behavior: 'smooth' });
});
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;
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();
});
window.readerConfig = {
startPage: {{ start_page }},
totalPages: {{ total_pages }}
};
</script>
<script src="{{ url_for('static', filename='js/reader.js') }}"></script>
{% endblock %}