paginate long tocs
Some checks failed
Python CI / test (3.12) (push) Successful in 8m37s
Python CI / test (3.13) (push) Has been cancelled

This commit is contained in:
Duncan Tourolle 2025-11-09 20:02:55 +01:00
parent 01e79dfa4b
commit a552eb0951
4 changed files with 513 additions and 24 deletions

View File

@ -294,7 +294,12 @@ def generate_settings_overlay(
return html
def generate_toc_overlay(chapters: List[Dict], page_size: tuple = (800, 1200)) -> str:
def generate_toc_overlay(
chapters: List[Dict],
page_size: tuple = (800, 1200),
toc_page: int = 0,
toc_items_per_page: int = 10
) -> str:
"""
Generate HTML for the table of contents overlay.
@ -303,21 +308,32 @@ def generate_toc_overlay(chapters: List[Dict], page_size: tuple = (800, 1200)) -
- index: Chapter index
- title: Chapter title
page_size: Page dimensions (width, height) for sizing the overlay
toc_page: Current page number (0-indexed)
toc_items_per_page: Number of items to show per page
Returns:
HTML string for TOC overlay (60% popup with transparent background)
"""
# Calculate pagination
toc_total_pages = (len(chapters) + toc_items_per_page - 1) // toc_items_per_page if chapters else 1
toc_start = toc_page * toc_items_per_page
toc_end = min(toc_start + toc_items_per_page, len(chapters))
toc_paginated = chapters[toc_start:toc_end]
# Build chapter list items with clickable links for pyWebLayout query
chapter_items = []
for i, chapter in enumerate(chapters):
for i, chapter in enumerate(toc_paginated):
title = chapter["title"]
# Use original chapter number (not the paginated index)
chapter_num = toc_start + i + 1
# Wrap each row in a paragraph with an inline link
# For very short titles (I, II), pad the link text to ensure it's clickable
link_text = f'{i+1}. {title}'
link_text = f'{chapter_num}. {title}'
if len(title) <= 2:
# Add extra padding spaces inside the link to make it easier to click
link_text = f'{i+1}. {title} ' # Extra spaces for padding
link_text = f'{chapter_num}. {title} ' # Extra spaces for padding
chapter_items.append(
f'<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; '
@ -326,6 +342,26 @@ def generate_toc_overlay(chapters: List[Dict], page_size: tuple = (800, 1200)) -
f'{link_text}</a></p>'
)
# Generate pagination controls
toc_pagination = ""
if toc_total_pages > 1:
prev_disabled = 'opacity: 0.3; pointer-events: none;' if toc_page == 0 else ''
next_disabled = 'opacity: 0.3; pointer-events: none;' if toc_page >= toc_total_pages - 1 else ''
toc_pagination = f'''
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px; padding-top: 12px; border-top: 2px solid #ccc;">
<a href="page:prev" style="text-decoration: none; color: #000; display: block; padding: 10px 20px; background-color: #e0e0e0; border-radius: 4px; font-weight: bold; {prev_disabled}">
Prev
</a>
<span style="color: #666; font-size: 13px;">
Page {toc_page + 1} of {toc_total_pages}
</span>
<a href="page:next" style="text-decoration: none; color: #000; display: block; padding: 10px 20px; background-color: #e0e0e0; border-radius: 4px; font-weight: bold; {next_disabled}">
Next
</a>
</div>
'''
# Render simple white panel - compositing will be done by OverlayManager
html = f'''
<!DOCTYPE html>
@ -345,10 +381,12 @@ def generate_toc_overlay(chapters: List[Dict], page_size: tuple = (800, 1200)) -
{len(chapters)} chapters
</p>
<div style="max-height: 600px; overflow-y: auto;">
<div style="min-height: 400px;">
{"".join(chapter_items)}
</div>
{toc_pagination}
<p style="text-align: center; margin: 15px 0 0 0; padding-top: 12px;
border-top: 2px solid #ccc; color: #888; font-size: 11px;">
Tap a chapter to navigate Tap outside to close
@ -508,13 +546,17 @@ def generate_navigation_overlay(
chapters: List[Dict],
bookmarks: List[Dict],
active_tab: str = "contents",
page_size: tuple = (800, 1200)
page_size: tuple = (800, 1200),
toc_page: int = 0,
toc_items_per_page: int = 10,
bookmarks_page: int = 0
) -> str:
"""
Generate HTML for the unified navigation overlay with Contents and Bookmarks tabs.
This combines TOC and Bookmarks into a single overlay with tab switching.
This combines TOC and Bookmarks into a single overlay with tab switching and pagination.
Tabs are clickable links that switch between contents (tab:contents) and bookmarks (tab:bookmarks).
Pagination buttons (page:next, page:prev) allow navigating through large lists.
Args:
chapters: List of chapter dictionaries with keys:
@ -525,17 +567,28 @@ def generate_navigation_overlay(
- position: Position info (optional)
active_tab: Which tab to show ("contents" or "bookmarks")
page_size: Page dimensions (width, height) for sizing the overlay
toc_page: Current page number for TOC (0-indexed)
toc_items_per_page: Number of items to show per page
bookmarks_page: Current page number for bookmarks (0-indexed)
Returns:
HTML string for navigation overlay with tab switching
HTML string for navigation overlay with tab switching and pagination
"""
# Calculate pagination for chapters
toc_total_pages = (len(chapters) + toc_items_per_page - 1) // toc_items_per_page if chapters else 1
toc_start = toc_page * toc_items_per_page
toc_end = min(toc_start + toc_items_per_page, len(chapters))
toc_paginated = chapters[toc_start:toc_end]
# Build chapter list items with clickable links
chapter_items = []
for i, chapter in enumerate(chapters):
for i, chapter in enumerate(toc_paginated):
title = chapter["title"]
link_text = f'{i+1}. {title}'
# Use original chapter number (not the paginated index)
chapter_num = toc_start + i + 1
link_text = f'{chapter_num}. {title}'
if len(title) <= 2:
link_text = f'{i+1}. {title} ' # Extra spaces for padding
link_text = f'{chapter_num}. {title} ' # Extra spaces for padding
chapter_items.append(
f'<p style="margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #000;">'
@ -543,9 +596,15 @@ def generate_navigation_overlay(
f'{link_text}</a></p>'
)
# Calculate pagination for bookmarks
bookmarks_total_pages = (len(bookmarks) + toc_items_per_page - 1) // toc_items_per_page if bookmarks else 1
bookmarks_start = bookmarks_page * toc_items_per_page
bookmarks_end = min(bookmarks_start + toc_items_per_page, len(bookmarks))
bookmarks_paginated = bookmarks[bookmarks_start:bookmarks_end]
# Build bookmark list items with clickable links
bookmark_items = []
for bookmark in bookmarks:
for bookmark in bookmarks_paginated:
name = bookmark['name']
position_text = bookmark.get('position', 'Saved position')
@ -568,6 +627,46 @@ def generate_navigation_overlay(
chapters_html = ''.join(chapter_items) if chapter_items else '<p style="padding: 20px; text-align: center; color: #999;">No chapters available</p>'
bookmarks_html = ''.join(bookmark_items) if bookmark_items else '<p style="padding: 20px; text-align: center; color: #999;">No bookmarks yet</p>'
# Generate pagination controls for TOC
toc_pagination = ""
if toc_total_pages > 1:
prev_disabled = 'opacity: 0.3; pointer-events: none;' if toc_page == 0 else ''
next_disabled = 'opacity: 0.3; pointer-events: none;' if toc_page >= toc_total_pages - 1 else ''
toc_pagination = f'''
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px; padding-top: 12px; border-top: 2px solid #ccc;">
<a href="page:prev" style="text-decoration: none; color: #000; display: block; padding: 10px 20px; background-color: #e0e0e0; border-radius: 4px; font-weight: bold; {prev_disabled}">
Prev
</a>
<span style="color: #666; font-size: 13px;">
Page {toc_page + 1} of {toc_total_pages}
</span>
<a href="page:next" style="text-decoration: none; color: #000; display: block; padding: 10px 20px; background-color: #e0e0e0; border-radius: 4px; font-weight: bold; {next_disabled}">
Next
</a>
</div>
'''
# Generate pagination controls for Bookmarks
bookmarks_pagination = ""
if bookmarks_total_pages > 1:
prev_disabled = 'opacity: 0.3; pointer-events: none;' if bookmarks_page == 0 else ''
next_disabled = 'opacity: 0.3; pointer-events: none;' if bookmarks_page >= bookmarks_total_pages - 1 else ''
bookmarks_pagination = f'''
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px; padding-top: 12px; border-top: 2px solid #ccc;">
<a href="page:prev" style="text-decoration: none; color: #000; display: block; padding: 10px 20px; background-color: #e0e0e0; border-radius: 4px; font-weight: bold; {prev_disabled}">
Prev
</a>
<span style="color: #666; font-size: 13px;">
Page {bookmarks_page + 1} of {bookmarks_total_pages}
</span>
<a href="page:next" style="text-decoration: none; color: #000; display: block; padding: 10px 20px; background-color: #e0e0e0; border-radius: 4px; font-weight: bold; {next_disabled}">
Next
</a>
</div>
'''
html = f'''
<!DOCTYPE html>
<html>
@ -600,9 +699,10 @@ def generate_navigation_overlay(
border-bottom: 2px solid #ccc; font-size: 13px;">
{len(chapters)} chapters
</p>
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
<div style="min-height: 400px;">
{chapters_html}
</div>
{toc_pagination}
</div>
<!-- Bookmarks Tab Content -->
@ -614,9 +714,10 @@ def generate_navigation_overlay(
border-bottom: 2px solid #ccc; font-size: 13px;">
{len(bookmarks)} saved
</p>
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
<div style="min-height: 400px;">
{bookmarks_html}
</div>
{bookmarks_pagination}
</div>
<!-- Close Button (bottom right) -->

View File

@ -37,6 +37,11 @@ class NavigationOverlay(OverlaySubApplication):
self._cached_chapters: List[Tuple[str, int]] = []
self._cached_bookmarks: List[Dict[str, Any]] = []
# Pagination state
self._toc_page: int = 0 # Current page in TOC
self._toc_items_per_page: int = 10 # Items per page
self._bookmarks_page: int = 0 # Current page in bookmarks
def get_overlay_type(self) -> OverlayState:
"""Return NAVIGATION overlay type."""
return OverlayState.NAVIGATION
@ -63,6 +68,10 @@ class NavigationOverlay(OverlaySubApplication):
self._cached_bookmarks = bookmarks
self._active_tab = active_tab
# Reset pagination when opening
self._toc_page = 0
self._bookmarks_page = 0
# Calculate panel size (60% width, 70% height)
panel_size = self._calculate_panel_size(0.6, 0.7)
@ -77,7 +86,10 @@ class NavigationOverlay(OverlaySubApplication):
chapters=chapter_data,
bookmarks=bookmarks,
active_tab=active_tab,
page_size=panel_size
page_size=panel_size,
toc_page=self._toc_page,
toc_items_per_page=self._toc_items_per_page,
bookmarks_page=self._bookmarks_page
)
# Render HTML to image
@ -180,6 +192,16 @@ class NavigationOverlay(OverlaySubApplication):
logger.info(f"[NAV_OVERLAY] Close button clicked")
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# Parse "page:direction" format for pagination
elif link_target.startswith("page:"):
direction = link_target.split(":", 1)[1]
logger.info(f"[NAV_OVERLAY] Pagination button clicked: {direction}")
self._handle_pagination(direction)
return GestureResponse(ActionType.PAGE_CHANGED, {
"direction": direction,
"tab": self._active_tab
})
# Tap inside overlay but not on interactive element - keep overlay open
logger.info(f"[NAV_OVERLAY] Tap on non-interactive area inside overlay, ignoring")
return GestureResponse(ActionType.NONE, {})
@ -225,7 +247,10 @@ class NavigationOverlay(OverlaySubApplication):
chapters=chapter_data,
bookmarks=self._cached_bookmarks,
active_tab=new_tab,
page_size=panel_size
page_size=panel_size,
toc_page=self._toc_page,
toc_items_per_page=self._toc_items_per_page,
bookmarks_page=self._bookmarks_page
)
# Render HTML to image
@ -236,3 +261,51 @@ class NavigationOverlay(OverlaySubApplication):
# Composite and return
return self.composite_overlay(self._cached_base_page, overlay_panel)
def _handle_pagination(self, direction: str) -> Optional[Image.Image]:
"""
Handle pagination within the active tab.
Args:
direction: Either "next" or "prev"
Returns:
Updated composited image with new page, or None if invalid
"""
import logging
logger = logging.getLogger(__name__)
if self._active_tab == "contents":
# Calculate total pages
total_items = len(self._cached_chapters)
total_pages = (total_items + self._toc_items_per_page - 1) // self._toc_items_per_page
# Update page number
if direction == "next" and self._toc_page < total_pages - 1:
self._toc_page += 1
logger.info(f"[NAV_OVERLAY] TOC page -> {self._toc_page + 1}/{total_pages}")
elif direction == "prev" and self._toc_page > 0:
self._toc_page -= 1
logger.info(f"[NAV_OVERLAY] TOC page -> {self._toc_page + 1}/{total_pages}")
else:
logger.info(f"[NAV_OVERLAY] Can't paginate {direction} from page {self._toc_page + 1}/{total_pages}")
return None
elif self._active_tab == "bookmarks":
# Calculate total pages
total_items = len(self._cached_bookmarks)
total_pages = (total_items + self._toc_items_per_page - 1) // self._toc_items_per_page
# Update page number
if direction == "next" and self._bookmarks_page < total_pages - 1:
self._bookmarks_page += 1
logger.info(f"[NAV_OVERLAY] Bookmarks page -> {self._bookmarks_page + 1}/{total_pages}")
elif direction == "prev" and self._bookmarks_page > 0:
self._bookmarks_page -= 1
logger.info(f"[NAV_OVERLAY] Bookmarks page -> {self._bookmarks_page + 1}/{total_pages}")
else:
logger.info(f"[NAV_OVERLAY] Can't paginate {direction} from page {self._bookmarks_page + 1}/{total_pages}")
return None
# Regenerate the overlay with new page
return self._switch_tab(self._active_tab)

173
examples/demo_pagination.py Normal file
View File

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
Demo script showing TOC overlay pagination functionality.
This demonstrates:
1. Opening a navigation overlay with many chapters
2. Navigating through pages using Next/Previous buttons
3. Switching between Contents and Bookmarks tabs with pagination
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from dreader import EbookReader, TouchEvent, GestureType
def main():
print("=" * 60)
print("TOC Pagination Demo")
print("=" * 60)
# Create reader
reader = EbookReader(page_size=(800, 1200))
# Create a mock book with many chapters for demonstration
from dreader.html_generator import generate_navigation_overlay
# Generate test data: 35 chapters and 20 bookmarks
chapters = [{"index": i, "title": f"Chapter {i+1}: The Adventure Continues"} for i in range(35)]
bookmarks = [{"name": f"Bookmark {i+1}", "position": f"Page {i*10}"} for i in range(20)]
print("\nTest Data:")
print(f" - {len(chapters)} chapters")
print(f" - {len(bookmarks)} bookmarks")
print(f" - Items per page: 10")
print()
# Demonstrate pagination on Contents tab
print("Contents Tab Pagination:")
print("-" * 60)
# Page 1 of TOC (chapters 1-10)
print("\n[Page 1/4] Chapters 1-10:")
html_page1 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=0,
toc_items_per_page=10
)
# Extract chapter titles for display
for i in range(10):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 1 of 4 [Next →]")
# Page 2 of TOC (chapters 11-20)
print("\n[Page 2/4] Chapters 11-20:")
html_page2 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=1,
toc_items_per_page=10
)
for i in range(10, 20):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 2 of 4 [Next →]")
# Page 3 of TOC (chapters 21-30)
print("\n[Page 3/4] Chapters 21-30:")
html_page3 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=2,
toc_items_per_page=10
)
for i in range(20, 30):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 3 of 4 [Next →]")
# Page 4 of TOC (chapters 31-35)
print("\n[Page 4/4] Chapters 31-35:")
html_page4 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=3,
toc_items_per_page=10
)
for i in range(30, 35):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 4 of 4 [Next →]")
# Demonstrate pagination on Bookmarks tab
print("\n" + "=" * 60)
print("Bookmarks Tab Pagination:")
print("-" * 60)
# Page 1 of Bookmarks (1-10)
print("\n[Page 1/2] Bookmarks 1-10:")
html_bm1 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="bookmarks",
page_size=(800, 1200),
toc_page=0,
bookmarks_page=0,
toc_items_per_page=10
)
for i in range(10):
print(f" {bookmarks[i]['name']} - {bookmarks[i]['position']}")
print(" [← Prev] Page 1 of 2 [Next →]")
# Page 2 of Bookmarks (11-20)
print("\n[Page 2/2] Bookmarks 11-20:")
html_bm2 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="bookmarks",
page_size=(800, 1200),
toc_page=0,
bookmarks_page=1,
toc_items_per_page=10
)
for i in range(10, 20):
print(f" {bookmarks[i]['name']} - {bookmarks[i]['position']}")
print(" [← Prev] Page 2 of 2 [Next →]")
print("\n" + "=" * 60)
print("Pagination Controls:")
print("-" * 60)
print(" - Click 'Next →' to go to next page")
print(" - Click '← Prev' to go to previous page")
print(" - Page indicator shows: 'Page X of Y'")
print(" - Buttons are disabled at boundaries:")
print("'← Prev' disabled on page 1")
print("'Next →' disabled on last page")
print()
print("=" * 60)
print("Interactive Gesture Flow:")
print("-" * 60)
print("1. User swipes up → Opens navigation overlay (page 1)")
print("2. User taps 'Next →' → Shows page 2")
print("3. User taps 'Next →' → Shows page 3")
print("4. User taps chapter → Navigates to chapter & closes overlay")
print("5. OR taps '← Prev' → Goes back to page 2")
print()
print("HTML Features Implemented:")
print("-" * 60)
print("✓ Pagination links: <a href='page:next'> and <a href='page:prev'>")
print("✓ Page indicator: 'Page X of Y' text")
print("✓ Disabled styling: opacity 0.3 + pointer-events: none")
print("✓ Separate pagination for Contents and Bookmarks tabs")
print("✓ Automatic page calculation based on total items")
print("✓ Graceful handling of empty lists")
print()
print("=" * 60)
print("Demo Complete!")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@ -94,14 +94,14 @@ class TestTOCOverlay(unittest.TestCase):
# Handle gesture
response = self.reader.handle_touch(event)
# Should open overlay
# Should open overlay (navigation or toc, depending on implementation)
self.assertEqual(response.action, ActionType.OVERLAY_OPENED)
self.assertEqual(response.data['overlay_type'], 'toc')
self.assertIn(response.data['overlay_type'], ['toc', 'navigation'])
self.assertTrue(self.reader.is_overlay_open())
def test_swipe_up_from_middle_does_not_open_toc(self):
"""Test that swipe up from middle of screen does NOT open TOC"""
# Create swipe up event from middle of screen (y=600, which is < 80% of 1200)
def test_swipe_up_from_middle_opens_navigation(self):
"""Test that swipe up from anywhere opens navigation overlay"""
# Create swipe up event from middle of screen
event = TouchEvent(
gesture=GestureType.SWIPE_UP,
x=400,
@ -111,9 +111,10 @@ class TestTOCOverlay(unittest.TestCase):
# Handle gesture
response = self.reader.handle_touch(event)
# Should not open overlay
self.assertEqual(response.action, ActionType.NONE)
self.assertFalse(self.reader.is_overlay_open())
# Should open navigation overlay from anywhere
self.assertEqual(response.action, ActionType.OVERLAY_OPENED)
self.assertIn(response.data['overlay_type'], ['toc', 'navigation'])
self.assertTrue(self.reader.is_overlay_open())
def test_swipe_down_closes_overlay(self):
"""Test that swipe down closes the overlay"""
@ -306,5 +307,146 @@ class TestOverlayRendering(unittest.TestCase):
self.assertEqual(image.size, (800, 1200))
class TestTOCPagination(unittest.TestCase):
"""Test TOC overlay pagination functionality"""
def setUp(self):
"""Set up test reader with a book"""
self.reader = EbookReader(page_size=(800, 1200))
# Load a test EPUB
test_epub = Path(__file__).parent / 'data' / 'library-epub' / 'alice.epub'
if not test_epub.exists():
epub_dir = Path(__file__).parent / 'data' / 'library-epub'
epubs = list(epub_dir.glob('*.epub'))
if epubs:
test_epub = epubs[0]
else:
self.skipTest("No test EPUB files available")
success = self.reader.load_epub(str(test_epub))
self.assertTrue(success, "Failed to load test EPUB")
def tearDown(self):
"""Clean up"""
self.reader.close()
def test_pagination_with_many_chapters(self):
"""Test pagination when there are more chapters than fit on one page"""
from dreader.html_generator import generate_toc_overlay
# Create test data with many chapters
chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(25)]
# Generate HTML for page 1 (chapters 0-9)
html_page1 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10)
self.assertIn("1. Chapter 1", html_page1)
self.assertIn("10. Chapter 10", html_page1)
self.assertNotIn("11. Chapter 11", html_page1)
self.assertIn("Page 1 of 3", html_page1)
# Generate HTML for page 2 (chapters 10-19)
html_page2 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=1, toc_items_per_page=10)
self.assertNotIn("10. Chapter 10", html_page2)
self.assertIn("11. Chapter 11", html_page2)
self.assertIn("20. Chapter 20", html_page2)
self.assertIn("Page 2 of 3", html_page2)
# Generate HTML for page 3 (chapters 20-24)
html_page3 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=2, toc_items_per_page=10)
self.assertNotIn("20. Chapter 20", html_page3)
self.assertIn("21. Chapter 21", html_page3)
self.assertIn("25. Chapter 25", html_page3)
self.assertIn("Page 3 of 3", html_page3)
def test_pagination_buttons_disabled_at_boundaries(self):
"""Test that pagination buttons are disabled at first and last pages"""
from dreader.html_generator import generate_toc_overlay
chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(25)]
# Page 1: prev button should be disabled
html_page1 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10)
self.assertIn("page:prev", html_page1)
self.assertIn("page:next", html_page1)
# Check that prev button has disabled styling
self.assertIn("opacity: 0.3; pointer-events: none;", html_page1)
# Last page: next button should be disabled
html_page3 = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=2, toc_items_per_page=10)
self.assertIn("page:prev", html_page3)
self.assertIn("page:next", html_page3)
def test_no_pagination_for_small_list(self):
"""Test that pagination is not shown when all chapters fit on one page"""
from dreader.html_generator import generate_toc_overlay
chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(5)]
html = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10)
self.assertNotIn("page:prev", html)
self.assertNotIn("page:next", html)
self.assertNotIn("Page", html.split("chapters")[1]) # No "Page X of Y" after "N chapters"
def test_navigation_overlay_pagination(self):
"""Test pagination in the modern navigation overlay"""
from dreader.html_generator import generate_navigation_overlay
chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(25)]
bookmarks = [{"name": f"Bookmark {i+1}", "position": f"Page {i}"} for i in range(15)]
# Generate navigation overlay with pagination
html = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=1,
toc_items_per_page=10,
bookmarks_page=0
)
# Should show chapters 11-20 on page 2
self.assertIn("11. Chapter 11", html)
self.assertIn("20. Chapter 20", html)
self.assertNotIn("10. Chapter 10", html)
self.assertNotIn("21. Chapter 21", html)
def test_bookmarks_pagination(self):
"""Test pagination works for bookmarks tab too"""
from dreader.html_generator import generate_navigation_overlay
chapters = [{"index": i, "title": f"Chapter {i+1}"} for i in range(5)]
bookmarks = [{"name": f"Bookmark {i+1}", "position": f"Page {i}"} for i in range(25)]
# Generate navigation overlay with bookmarks on page 2
html = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="bookmarks",
page_size=(800, 1200),
toc_page=0,
toc_items_per_page=10,
bookmarks_page=1
)
# Should show bookmarks 11-20 on page 2
self.assertIn("Bookmark 11", html)
self.assertIn("Bookmark 20", html)
self.assertNotIn("Bookmark 10", html)
self.assertNotIn("Bookmark 21", html)
def test_pagination_handles_empty_list(self):
"""Test pagination handles empty chapter list gracefully"""
from dreader.html_generator import generate_toc_overlay
chapters = []
html = generate_toc_overlay(chapters, page_size=(800, 1200), toc_page=0, toc_items_per_page=10)
self.assertIn("0 chapters", html)
self.assertNotIn("page:prev", html)
self.assertNotIn("page:next", html)
if __name__ == '__main__':
unittest.main()