refactor all navigation to one screen.

This commit is contained in:
2025-11-08 23:50:49 +01:00
parent 5d3e7fae7b
commit 7518bcf835
7 changed files with 778 additions and 3 deletions
+137
View File
@@ -825,6 +825,85 @@ class EbookReader:
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# For navigation overlay, handle tab switching, chapter/bookmark selection, and close
elif self.current_overlay_state == OverlayState.NAVIGATION:
# Query the overlay to see what was tapped
query_result = self.overlay_manager.query_overlay_pixel(x, y)
# If query failed (tap outside overlay), close it
if not query_result:
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# Check if tapped on a link
if query_result.get("is_interactive") and query_result.get("link_target"):
link_target = query_result["link_target"]
# Parse "tab:tabname" format for tab switching
if link_target.startswith("tab:"):
tab_name = link_target.split(":", 1)[1]
# Switch to the selected tab
self.switch_navigation_tab(tab_name)
return GestureResponse(ActionType.TAB_SWITCHED, {
"tab": tab_name
})
# Parse "chapter:N" format for chapter navigation
elif link_target.startswith("chapter:"):
try:
chapter_idx = int(link_target.split(":")[1])
# Get chapter title for response
chapters = self.get_chapters()
chapter_title = None
for title, idx in chapters:
if idx == chapter_idx:
chapter_title = title
break
# Jump to selected chapter
self.jump_to_chapter(chapter_idx)
# Close overlay
self.close_overlay()
return GestureResponse(ActionType.CHAPTER_SELECTED, {
"chapter_index": chapter_idx,
"chapter_title": chapter_title or f"Chapter {chapter_idx}"
})
except (ValueError, IndexError):
pass
# Parse "bookmark:name" format for bookmark navigation
elif link_target.startswith("bookmark:"):
bookmark_name = link_target.split(":", 1)[1]
# Load the bookmark position
page = self.load_position(bookmark_name)
if page:
# Close overlay
self.close_overlay()
return GestureResponse(ActionType.BOOKMARK_SELECTED, {
"bookmark_name": bookmark_name
})
else:
# Failed to load bookmark
return GestureResponse(ActionType.ERROR, {
"message": f"Failed to load bookmark: {bookmark_name}"
})
# Parse "action:close" format for close button
elif link_target.startswith("action:"):
action = link_target.split(":", 1)[1]
if action == "close":
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# Not an interactive element, close overlay
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# For other overlays, just close on any tap for now
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
@@ -1127,6 +1206,64 @@ class EbookReader:
return result
def open_navigation_overlay(self, active_tab: str = "contents") -> Optional[Image.Image]:
"""
Open the unified navigation overlay with Contents and Bookmarks tabs.
This is the new unified overlay that replaces separate TOC and Bookmarks overlays.
It provides a tabbed interface for switching between table of contents and bookmarks.
Args:
active_tab: Which tab to show initially ("contents" or "bookmarks")
Returns:
Composited image with navigation overlay on top of current page, or None if no book loaded
"""
if not self.is_loaded():
return None
# Get current page as base
base_page = self.get_current_page(include_highlights=False)
if not base_page:
return None
# Get chapters for Contents tab
chapters = self.get_chapters()
# Get bookmarks for Bookmarks tab
bookmark_names = self.list_saved_positions()
bookmarks = [
{"name": name, "position": f"Saved position"}
for name in bookmark_names
]
# Open overlay and get composited image
result = self.overlay_manager.open_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
base_page=base_page,
active_tab=active_tab
)
self.current_overlay_state = OverlayState.NAVIGATION
return result
def switch_navigation_tab(self, new_tab: str) -> Optional[Image.Image]:
"""
Switch between tabs in the navigation overlay.
Args:
new_tab: Tab to switch to ("contents" or "bookmarks")
Returns:
Updated image with new tab active, or None if navigation overlay is not open
"""
if self.current_overlay_state != OverlayState.NAVIGATION:
return None
result = self.overlay_manager.switch_navigation_tab(new_tab)
return result if result else self.get_current_page()
def close_overlay(self) -> Optional[Image.Image]:
"""
Close the current overlay and return to reading view.
+2
View File
@@ -125,5 +125,7 @@ class ActionType:
OVERLAY_OPENED = "overlay_opened"
OVERLAY_CLOSED = "overlay_closed"
CHAPTER_SELECTED = "chapter_selected"
BOOKMARK_SELECTED = "bookmark_selected"
TAB_SWITCHED = "tab_switched"
SETTING_CHANGED = "setting_changed"
BACK_TO_LIBRARY = "back_to_library"
+133
View File
@@ -502,3 +502,136 @@ def generate_bookmarks_overlay(bookmarks: List[Dict]) -> str:
</html>
'''
return html
def generate_navigation_overlay(
chapters: List[Dict],
bookmarks: List[Dict],
active_tab: str = "contents",
page_size: tuple = (800, 1200)
) -> 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.
Tabs are clickable links that switch between contents (tab:contents) and bookmarks (tab:bookmarks).
Args:
chapters: List of chapter dictionaries with keys:
- index: Chapter index
- title: Chapter title
bookmarks: List of bookmark dictionaries with keys:
- name: Bookmark name
- position: Position info (optional)
active_tab: Which tab to show ("contents" or "bookmarks")
page_size: Page dimensions (width, height) for sizing the overlay
Returns:
HTML string for navigation overlay with tab switching
"""
# Build chapter list items with clickable links
chapter_items = []
for i, chapter in enumerate(chapters):
title = chapter["title"]
link_text = f'{i+1}. {title}'
if len(title) <= 2:
link_text = f'{i+1}. {title} ' # Extra spaces for padding
chapter_items.append(
f'<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; '
f'border-left: 3px solid #000;">'
f'<a href="chapter:{chapter["index"]}" style="text-decoration: none; color: #000;">'
f'{link_text}</a></p>'
)
# Build bookmark list items with clickable links
bookmark_items = []
for bookmark in bookmarks:
name = bookmark['name']
position_text = bookmark.get('position', 'Saved position')
bookmark_items.append(
f'<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; '
f'border-left: 3px solid #000;">'
f'<a href="bookmark:{name}" style="text-decoration: none; color: #000; display: block;">'
f'<span style="font-weight: bold; display: block;">{name}</span>'
f'<span style="font-size: 11px; color: #666;">{position_text}</span>'
f'</a></p>'
)
# Determine which content to show
contents_display = "block" if active_tab == "contents" else "none"
bookmarks_display = "block" if active_tab == "bookmarks" else "none"
# Style active tab
contents_tab_style = "background-color: #000; color: #fff;" if active_tab == "contents" else "background-color: #f0f0f0; color: #000;"
bookmarks_tab_style = "background-color: #000; color: #fff;" if active_tab == "bookmarks" else "background-color: #f0f0f0; color: #000;"
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>'
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Navigation</title>
</head>
<body style="background-color: white; margin: 0; padding: 0; font-family: Arial, sans-serif;">
<!-- Tab Bar -->
<div style="display: flex; border-bottom: 2px solid #ccc; background-color: #f8f8f8;">
<a href="tab:contents"
style="flex: 1; padding: 15px; text-align: center; font-weight: bold;
text-decoration: none; border-right: 1px solid #ccc; {contents_tab_style}">
Contents
</a>
<a href="tab:bookmarks"
style="flex: 1; padding: 15px; text-align: center; font-weight: bold;
text-decoration: none; {bookmarks_tab_style}">
Bookmarks
</a>
</div>
<!-- Contents Tab Content -->
<div id="contents-tab" style="padding: 25px; display: {contents_display};">
<h2 style="color: #000; margin: 0 0 15px 0; font-size: 20px; text-align: center;">
Table of Contents
</h2>
<p style="text-align: center; color: #666; margin: 0 0 15px 0; padding-bottom: 12px;
border-bottom: 2px solid #ccc; font-size: 13px;">
{len(chapters)} chapters
</p>
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
{chapters_html}
</div>
</div>
<!-- Bookmarks Tab Content -->
<div id="bookmarks-tab" style="padding: 25px; display: {bookmarks_display};">
<h2 style="color: #000; margin: 0 0 15px 0; font-size: 20px; text-align: center;">
Bookmarks
</h2>
<p style="text-align: center; color: #666; margin: 0 0 15px 0; padding-bottom: 12px;
border-bottom: 2px solid #ccc; font-size: 13px;">
{len(bookmarks)} saved
</p>
<div style="overflow-y: auto; max-height: calc(100vh - 200px);">
{bookmarks_html}
</div>
</div>
<!-- Close Button (bottom right) -->
<div style="position: fixed; bottom: 20px; right: 20px;">
<a href="action:close"
style="display: inline-block; padding: 12px 24px; background-color: #dc3545;
color: white; text-decoration: none; border-radius: 4px; font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
Close
</a>
</div>
</body>
</html>
'''
return html
+111 -1
View File
@@ -14,7 +14,8 @@ from .state import OverlayState
from .html_generator import (
generate_toc_overlay,
generate_settings_overlay,
generate_bookmarks_overlay
generate_bookmarks_overlay,
generate_navigation_overlay
)
@@ -370,6 +371,115 @@ class OverlayManager:
# Composite and return
return self.composite_overlay(base_page, overlay_image)
def open_navigation_overlay(
self,
chapters: List[Tuple[str, int]],
bookmarks: List[Dict],
base_page: Image.Image,
active_tab: str = "contents"
) -> Image.Image:
"""
Open the unified navigation overlay with Contents and Bookmarks tabs.
This replaces the separate TOC and Bookmarks overlays with a single
overlay that has tabs for switching between contents and bookmarks.
Args:
chapters: List of (chapter_title, chapter_index) tuples
bookmarks: List of bookmark dictionaries with 'name' and optional 'position'
base_page: Current reading page to show underneath
active_tab: Which tab to show ("contents" or "bookmarks")
Returns:
Composited image with navigation overlay on top
"""
# Import here to avoid circular dependency
from .application import EbookReader
# Calculate panel size (60% of screen width, 70% height)
panel_width = int(self.page_size[0] * 0.6)
panel_height = int(self.page_size[1] * 0.7)
# Convert chapters to format expected by HTML generator
chapter_data = [
{"index": idx, "title": title}
for title, idx in chapters
]
# Generate navigation HTML with tabs
html = generate_navigation_overlay(
chapters=chapter_data,
bookmarks=bookmarks,
active_tab=active_tab,
page_size=(panel_width, panel_height)
)
# Create reader for overlay and keep it alive for querying
if self._overlay_reader:
self._overlay_reader.close()
self._overlay_reader = EbookReader(
page_size=(panel_width, panel_height),
margin=15,
background_color=(255, 255, 255)
)
# Load the HTML content
success = self._overlay_reader.load_html(
html_string=html,
title="Navigation",
author="",
document_id="navigation_overlay"
)
if not success:
raise ValueError("Failed to load navigation overlay HTML")
# Get the rendered page
overlay_panel = self._overlay_reader.get_current_page()
# Calculate and store panel position for coordinate translation
panel_x = int((self.page_size[0] - panel_width) / 2)
panel_y = int((self.page_size[1] - panel_height) / 2)
self._overlay_panel_offset = (panel_x, panel_y)
# Cache for later use
self._cached_base_page = base_page.copy()
self._cached_overlay_image = overlay_panel
self.current_overlay = OverlayState.NAVIGATION
# Store active tab for tab switching
self._active_nav_tab = active_tab
self._cached_chapters = chapters
self._cached_bookmarks = bookmarks
# Composite and return
return self.composite_overlay(base_page, overlay_panel)
def switch_navigation_tab(self, new_tab: str) -> Optional[Image.Image]:
"""
Switch between tabs in the navigation overlay.
Args:
new_tab: Tab to switch to ("contents" or "bookmarks")
Returns:
Updated composited image with new tab active, or None if not in navigation overlay
"""
if self.current_overlay != OverlayState.NAVIGATION:
return None
# Re-open navigation overlay with new active tab
if hasattr(self, '_cached_chapters') and hasattr(self, '_cached_bookmarks'):
return self.open_navigation_overlay(
chapters=self._cached_chapters,
bookmarks=self._cached_bookmarks,
base_page=self._cached_base_page,
active_tab=new_tab
)
return None
def close_overlay(self) -> Optional[Image.Image]:
"""
Close the current overlay and return to base page.
+3 -2
View File
@@ -27,9 +27,10 @@ class EreaderMode(Enum):
class OverlayState(Enum):
"""Overlay states within READING mode"""
NONE = "none"
TOC = "toc"
TOC = "toc" # Deprecated: use NAVIGATION instead
SETTINGS = "settings"
BOOKMARKS = "bookmarks"
BOOKMARKS = "bookmarks" # Deprecated: use NAVIGATION instead
NAVIGATION = "navigation" # Unified overlay for TOC and Bookmarks
@dataclass