paginate long tocs
This commit is contained in:
parent
01e79dfa4b
commit
a552eb0951
@ -294,7 +294,12 @@ def generate_settings_overlay(
|
|||||||
return html
|
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.
|
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
|
- index: Chapter index
|
||||||
- title: Chapter title
|
- title: Chapter title
|
||||||
page_size: Page dimensions (width, height) for sizing the overlay
|
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:
|
Returns:
|
||||||
HTML string for TOC overlay (60% popup with transparent background)
|
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
|
# Build chapter list items with clickable links for pyWebLayout query
|
||||||
chapter_items = []
|
chapter_items = []
|
||||||
for i, chapter in enumerate(chapters):
|
for i, chapter in enumerate(toc_paginated):
|
||||||
title = chapter["title"]
|
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
|
# 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
|
# 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:
|
if len(title) <= 2:
|
||||||
# Add extra padding spaces inside the link to make it easier to click
|
# 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(
|
chapter_items.append(
|
||||||
f'<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; '
|
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>'
|
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
|
# Render simple white panel - compositing will be done by OverlayManager
|
||||||
html = f'''
|
html = f'''
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -345,10 +381,12 @@ def generate_toc_overlay(chapters: List[Dict], page_size: tuple = (800, 1200)) -
|
|||||||
{len(chapters)} chapters
|
{len(chapters)} chapters
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="max-height: 600px; overflow-y: auto;">
|
<div style="min-height: 400px;">
|
||||||
{"".join(chapter_items)}
|
{"".join(chapter_items)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{toc_pagination}
|
||||||
|
|
||||||
<p style="text-align: center; margin: 15px 0 0 0; padding-top: 12px;
|
<p style="text-align: center; margin: 15px 0 0 0; padding-top: 12px;
|
||||||
border-top: 2px solid #ccc; color: #888; font-size: 11px;">
|
border-top: 2px solid #ccc; color: #888; font-size: 11px;">
|
||||||
Tap a chapter to navigate • Tap outside to close
|
Tap a chapter to navigate • Tap outside to close
|
||||||
@ -508,13 +546,17 @@ def generate_navigation_overlay(
|
|||||||
chapters: List[Dict],
|
chapters: List[Dict],
|
||||||
bookmarks: List[Dict],
|
bookmarks: List[Dict],
|
||||||
active_tab: str = "contents",
|
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:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate HTML for the unified navigation overlay with Contents and Bookmarks tabs.
|
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).
|
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:
|
Args:
|
||||||
chapters: List of chapter dictionaries with keys:
|
chapters: List of chapter dictionaries with keys:
|
||||||
@ -525,17 +567,28 @@ def generate_navigation_overlay(
|
|||||||
- position: Position info (optional)
|
- position: Position info (optional)
|
||||||
active_tab: Which tab to show ("contents" or "bookmarks")
|
active_tab: Which tab to show ("contents" or "bookmarks")
|
||||||
page_size: Page dimensions (width, height) for sizing the overlay
|
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:
|
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
|
# Build chapter list items with clickable links
|
||||||
chapter_items = []
|
chapter_items = []
|
||||||
for i, chapter in enumerate(chapters):
|
for i, chapter in enumerate(toc_paginated):
|
||||||
title = chapter["title"]
|
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:
|
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(
|
chapter_items.append(
|
||||||
f'<p style="margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #000;">'
|
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>'
|
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
|
# Build bookmark list items with clickable links
|
||||||
bookmark_items = []
|
bookmark_items = []
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks_paginated:
|
||||||
name = bookmark['name']
|
name = bookmark['name']
|
||||||
position_text = bookmark.get('position', 'Saved position')
|
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>'
|
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>'
|
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'''
|
html = f'''
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@ -600,9 +699,10 @@ def generate_navigation_overlay(
|
|||||||
border-bottom: 2px solid #ccc; font-size: 13px;">
|
border-bottom: 2px solid #ccc; font-size: 13px;">
|
||||||
{len(chapters)} chapters
|
{len(chapters)} chapters
|
||||||
</p>
|
</p>
|
||||||
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
|
<div style="min-height: 400px;">
|
||||||
{chapters_html}
|
{chapters_html}
|
||||||
</div>
|
</div>
|
||||||
|
{toc_pagination}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bookmarks Tab Content -->
|
<!-- Bookmarks Tab Content -->
|
||||||
@ -614,9 +714,10 @@ def generate_navigation_overlay(
|
|||||||
border-bottom: 2px solid #ccc; font-size: 13px;">
|
border-bottom: 2px solid #ccc; font-size: 13px;">
|
||||||
{len(bookmarks)} saved
|
{len(bookmarks)} saved
|
||||||
</p>
|
</p>
|
||||||
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
|
<div style="min-height: 400px;">
|
||||||
{bookmarks_html}
|
{bookmarks_html}
|
||||||
</div>
|
</div>
|
||||||
|
{bookmarks_pagination}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Close Button (bottom right) -->
|
<!-- Close Button (bottom right) -->
|
||||||
|
|||||||
@ -37,6 +37,11 @@ class NavigationOverlay(OverlaySubApplication):
|
|||||||
self._cached_chapters: List[Tuple[str, int]] = []
|
self._cached_chapters: List[Tuple[str, int]] = []
|
||||||
self._cached_bookmarks: List[Dict[str, Any]] = []
|
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:
|
def get_overlay_type(self) -> OverlayState:
|
||||||
"""Return NAVIGATION overlay type."""
|
"""Return NAVIGATION overlay type."""
|
||||||
return OverlayState.NAVIGATION
|
return OverlayState.NAVIGATION
|
||||||
@ -63,6 +68,10 @@ class NavigationOverlay(OverlaySubApplication):
|
|||||||
self._cached_bookmarks = bookmarks
|
self._cached_bookmarks = bookmarks
|
||||||
self._active_tab = active_tab
|
self._active_tab = active_tab
|
||||||
|
|
||||||
|
# Reset pagination when opening
|
||||||
|
self._toc_page = 0
|
||||||
|
self._bookmarks_page = 0
|
||||||
|
|
||||||
# Calculate panel size (60% width, 70% height)
|
# Calculate panel size (60% width, 70% height)
|
||||||
panel_size = self._calculate_panel_size(0.6, 0.7)
|
panel_size = self._calculate_panel_size(0.6, 0.7)
|
||||||
|
|
||||||
@ -77,7 +86,10 @@ class NavigationOverlay(OverlaySubApplication):
|
|||||||
chapters=chapter_data,
|
chapters=chapter_data,
|
||||||
bookmarks=bookmarks,
|
bookmarks=bookmarks,
|
||||||
active_tab=active_tab,
|
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
|
# Render HTML to image
|
||||||
@ -180,6 +192,16 @@ class NavigationOverlay(OverlaySubApplication):
|
|||||||
logger.info(f"[NAV_OVERLAY] Close button clicked")
|
logger.info(f"[NAV_OVERLAY] Close button clicked")
|
||||||
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
|
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
|
# Tap inside overlay but not on interactive element - keep overlay open
|
||||||
logger.info(f"[NAV_OVERLAY] Tap on non-interactive area inside overlay, ignoring")
|
logger.info(f"[NAV_OVERLAY] Tap on non-interactive area inside overlay, ignoring")
|
||||||
return GestureResponse(ActionType.NONE, {})
|
return GestureResponse(ActionType.NONE, {})
|
||||||
@ -225,7 +247,10 @@ class NavigationOverlay(OverlaySubApplication):
|
|||||||
chapters=chapter_data,
|
chapters=chapter_data,
|
||||||
bookmarks=self._cached_bookmarks,
|
bookmarks=self._cached_bookmarks,
|
||||||
active_tab=new_tab,
|
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
|
# Render HTML to image
|
||||||
@ -236,3 +261,51 @@ class NavigationOverlay(OverlaySubApplication):
|
|||||||
|
|
||||||
# Composite and return
|
# Composite and return
|
||||||
return self.composite_overlay(self._cached_base_page, overlay_panel)
|
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
|
# Handle gesture
|
||||||
response = self.reader.handle_touch(event)
|
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.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())
|
self.assertTrue(self.reader.is_overlay_open())
|
||||||
|
|
||||||
def test_swipe_up_from_middle_does_not_open_toc(self):
|
def test_swipe_up_from_middle_opens_navigation(self):
|
||||||
"""Test that swipe up from middle of screen does NOT open TOC"""
|
"""Test that swipe up from anywhere opens navigation overlay"""
|
||||||
# Create swipe up event from middle of screen (y=600, which is < 80% of 1200)
|
# Create swipe up event from middle of screen
|
||||||
event = TouchEvent(
|
event = TouchEvent(
|
||||||
gesture=GestureType.SWIPE_UP,
|
gesture=GestureType.SWIPE_UP,
|
||||||
x=400,
|
x=400,
|
||||||
@ -111,9 +111,10 @@ class TestTOCOverlay(unittest.TestCase):
|
|||||||
# Handle gesture
|
# Handle gesture
|
||||||
response = self.reader.handle_touch(event)
|
response = self.reader.handle_touch(event)
|
||||||
|
|
||||||
# Should not open overlay
|
# Should open navigation overlay from anywhere
|
||||||
self.assertEqual(response.action, ActionType.NONE)
|
self.assertEqual(response.action, ActionType.OVERLAY_OPENED)
|
||||||
self.assertFalse(self.reader.is_overlay_open())
|
self.assertIn(response.data['overlay_type'], ['toc', 'navigation'])
|
||||||
|
self.assertTrue(self.reader.is_overlay_open())
|
||||||
|
|
||||||
def test_swipe_down_closes_overlay(self):
|
def test_swipe_down_closes_overlay(self):
|
||||||
"""Test that swipe down closes the overlay"""
|
"""Test that swipe down closes the overlay"""
|
||||||
@ -306,5 +307,146 @@ class TestOverlayRendering(unittest.TestCase):
|
|||||||
self.assertEqual(image.size, (800, 1200))
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user