239 lines
8.2 KiB
Python
239 lines
8.2 KiB
Python
"""
|
|
Navigation overlay sub-application.
|
|
|
|
Provides tabbed interface for Contents (TOC) and Bookmarks.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING, List, Tuple, Dict, Any, Optional
|
|
from PIL import Image
|
|
|
|
from .base import OverlaySubApplication
|
|
from ..gesture import GestureResponse, ActionType
|
|
from ..state import OverlayState
|
|
from ..html_generator import generate_navigation_overlay
|
|
|
|
if TYPE_CHECKING:
|
|
from ..application import EbookReader
|
|
|
|
|
|
class NavigationOverlay(OverlaySubApplication):
|
|
"""
|
|
Unified navigation overlay with Contents and Bookmarks tabs.
|
|
|
|
Features:
|
|
- Tab switching between Contents and Bookmarks
|
|
- Chapter navigation via clickable links
|
|
- Bookmark navigation
|
|
- Close button
|
|
"""
|
|
|
|
def __init__(self, reader: 'EbookReader'):
|
|
"""Initialize navigation overlay."""
|
|
super().__init__(reader)
|
|
|
|
# Tab state
|
|
self._active_tab: str = "contents"
|
|
self._cached_chapters: List[Tuple[str, int]] = []
|
|
self._cached_bookmarks: List[Dict[str, Any]] = []
|
|
|
|
def get_overlay_type(self) -> OverlayState:
|
|
"""Return NAVIGATION overlay type."""
|
|
return OverlayState.NAVIGATION
|
|
|
|
def open(self, base_page: Image.Image, **kwargs) -> Image.Image:
|
|
"""
|
|
Open the navigation overlay.
|
|
|
|
Args:
|
|
base_page: Current reading page to show underneath
|
|
chapters: List of (chapter_title, chapter_index) tuples
|
|
bookmarks: List of bookmark dicts with 'name' and optional 'position'
|
|
active_tab: Which tab to show initially ("contents" or "bookmarks")
|
|
|
|
Returns:
|
|
Composited image with navigation overlay
|
|
"""
|
|
chapters = kwargs.get('chapters', [])
|
|
bookmarks = kwargs.get('bookmarks', [])
|
|
active_tab = kwargs.get('active_tab', 'contents')
|
|
|
|
# Store for later use (tab switching)
|
|
self._cached_chapters = chapters
|
|
self._cached_bookmarks = bookmarks
|
|
self._active_tab = active_tab
|
|
|
|
# Calculate panel size (60% width, 70% height)
|
|
panel_size = self._calculate_panel_size(0.6, 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_size
|
|
)
|
|
|
|
# Render HTML to image
|
|
overlay_panel = self.render_html_to_image(html, panel_size)
|
|
|
|
# Cache for later use
|
|
self._cached_base_page = base_page.copy()
|
|
self._cached_overlay_image = overlay_panel
|
|
|
|
# Composite and return
|
|
return self.composite_overlay(base_page, overlay_panel)
|
|
|
|
def handle_tap(self, x: int, y: int) -> GestureResponse:
|
|
"""
|
|
Handle tap within navigation overlay.
|
|
|
|
Detects:
|
|
- Tab switching (tab:contents, tab:bookmarks)
|
|
- Chapter selection (chapter:N)
|
|
- Bookmark selection (bookmark:name)
|
|
- Close button (action:close)
|
|
- Tap outside overlay (closes)
|
|
|
|
Args:
|
|
x, y: Screen coordinates of tap
|
|
|
|
Returns:
|
|
GestureResponse with appropriate action
|
|
"""
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"[NAV_OVERLAY] Handling tap at ({x}, {y})")
|
|
logger.info(f"[NAV_OVERLAY] Panel offset: {self._overlay_panel_offset}, Panel size: {self._panel_size}")
|
|
|
|
# Query the overlay to see what was tapped
|
|
query_result = self.query_overlay_pixel(x, y)
|
|
|
|
logger.info(f"[NAV_OVERLAY] Query result: {query_result}")
|
|
|
|
# If query failed (tap outside overlay panel), close it
|
|
if query_result is None:
|
|
logger.info(f"[NAV_OVERLAY] Tap outside overlay panel, closing")
|
|
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"]
|
|
logger.info(f"[NAV_OVERLAY] Found interactive link: {link_target}")
|
|
|
|
# Parse "tab:tabname" format for tab switching
|
|
if link_target.startswith("tab:"):
|
|
tab_name = link_target.split(":", 1)[1]
|
|
self._switch_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
|
|
chapter_title = None
|
|
for title, idx in self._cached_chapters:
|
|
if idx == chapter_idx:
|
|
chapter_title = title
|
|
break
|
|
|
|
# Jump to selected chapter
|
|
self.reader.jump_to_chapter(chapter_idx)
|
|
|
|
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.reader.load_position(bookmark_name)
|
|
if page:
|
|
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":
|
|
logger.info(f"[NAV_OVERLAY] Close button clicked")
|
|
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
|
|
|
|
# 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, {})
|
|
|
|
def switch_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
|
|
"""
|
|
return self._switch_tab(new_tab)
|
|
|
|
def _switch_tab(self, new_tab: str) -> Optional[Image.Image]:
|
|
"""
|
|
Internal tab switching implementation.
|
|
|
|
Args:
|
|
new_tab: Tab to switch to
|
|
|
|
Returns:
|
|
Updated composited image with new tab active
|
|
"""
|
|
if not self._cached_base_page:
|
|
return None
|
|
|
|
self._active_tab = new_tab
|
|
|
|
# Regenerate overlay with new active tab
|
|
panel_size = self._calculate_panel_size(0.6, 0.7)
|
|
|
|
# Convert chapters to format expected by HTML generator
|
|
chapter_data = [
|
|
{"index": idx, "title": title}
|
|
for title, idx in self._cached_chapters
|
|
]
|
|
|
|
# Generate navigation HTML with new active tab
|
|
html = generate_navigation_overlay(
|
|
chapters=chapter_data,
|
|
bookmarks=self._cached_bookmarks,
|
|
active_tab=new_tab,
|
|
page_size=panel_size
|
|
)
|
|
|
|
# Render HTML to image
|
|
overlay_panel = self.render_html_to_image(html, panel_size)
|
|
|
|
# Update cache
|
|
self._cached_overlay_image = overlay_panel
|
|
|
|
# Composite and return
|
|
return self.composite_overlay(self._cached_base_page, overlay_panel)
|