paginate long tocs
This commit is contained in:
parent
01e79dfa4b
commit
a552eb0951
@ -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) -->
|
||||
|
||||
@ -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
173
examples/demo_pagination.py
Normal 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()
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user