refactor all navigation to one screen.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user