235 lines
8.0 KiB
HTML
235 lines
8.0 KiB
HTML
{% extends "layout.html" %}
|
|
{% block title %}{{ series_name }} - Reader{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="reader-container">
|
|
<!-- Reader Header -->
|
|
<div class="reader-header">
|
|
<div class="reader-header-left">
|
|
<a href="{{ url_for('main.gallery', tag=tag.name) }}" class="reader-back-btn" title="Back to gallery">
|
|
<span class="back-arrow">←</span>
|
|
</a>
|
|
<h1 class="reader-title">{{ series_name }}</h1>
|
|
<span class="reader-page-count">{{ total_pages }} pages</span>
|
|
</div>
|
|
<div class="reader-header-right">
|
|
<div class="reader-jump">
|
|
<label for="pageJumpSelect">Jump to:</label>
|
|
<select id="pageJumpSelect" class="reader-jump-select">
|
|
{% for page in pages %}
|
|
<option value="{{ page.page_number }}" {% if page.page_number == start_page %}selected{% endif %}>
|
|
Page {{ page.page_number }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<button id="toggleNavBtn" class="reader-toggle-nav" title="Toggle navigation panel">
|
|
<span class="nav-icon">☰</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="reader-body">
|
|
<!-- Navigation Sidebar (toggleable) -->
|
|
<aside id="readerNav" class="reader-nav">
|
|
<div class="reader-nav-header">
|
|
<h3>Pages</h3>
|
|
<button id="closeNavBtn" class="reader-nav-close">×</button>
|
|
</div>
|
|
<div class="reader-nav-thumbs">
|
|
{% for page in pages %}
|
|
<div class="reader-nav-thumb" data-page="{{ page.page_number }}">
|
|
<img src="{{ url_for('main.serve_image', filename=(page.image.thumb_path or page.image.filepath) | replace('/images/', '')) }}" alt="Page {{ page.page_number }}" loading="lazy">
|
|
<span class="thumb-page-num">{{ page.page_number }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Reading Area -->
|
|
<main id="readerMain" class="reader-main">
|
|
<div id="readerContent" class="reader-content">
|
|
{% if pages %}
|
|
{% for page in pages %}
|
|
<div class="reader-page" id="page-{{ page.page_number }}" data-page="{{ page.page_number }}">
|
|
<img src="{{ url_for('main.serve_image', filename=page.image.filepath | replace('/images/', '')) }}"
|
|
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 %}
|
|
<div class="reader-empty">
|
|
<p>No pages in this series yet.</p>
|
|
<a href="{{ url_for('main.gallery', tag=tag.name) }}" class="btn primary-btn">
|
|
Go to gallery to add pages
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Floating page indicator -->
|
|
<div id="floatingPageIndicator" class="floating-page-indicator">
|
|
<span id="currentPageNum">1</span> / {{ total_pages }}
|
|
</div>
|
|
|
|
<!-- Quick jump buttons -->
|
|
<div class="reader-quick-nav">
|
|
<button id="scrollToTopBtn" class="reader-quick-btn" title="Go to first page">↑ First</button>
|
|
<button id="scrollToBottomBtn" class="reader-quick-btn" title="Go to last page">Last ↓</button>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</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();
|
|
});
|
|
</script>
|
|
{% endblock %}
|