library and toc navigation
Python CI / test (push) Successful in 4m30s

This commit is contained in:
2025-11-08 12:20:23 +01:00
parent 60426432a0
commit 284a6e3393
13 changed files with 2365 additions and 219 deletions
+245 -3
View File
@@ -42,7 +42,7 @@ import os
from PIL import Image
from pyWebLayout.io.readers.epub_reader import read_epub
from pyWebLayout.io.gesture import TouchEvent, GestureType, GestureResponse, ActionType
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.abstract.block import Block, HeadingLevel
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager
from pyWebLayout.layout.ereader_layout import RenderingPosition
@@ -51,6 +51,10 @@ from pyWebLayout.concrete.page import Page
from pyWebLayout.core.query import QueryResult, SelectionRange
from pyWebLayout.core.highlight import Highlight, HighlightManager, HighlightColor, create_highlight_from_query_result
from .gesture import TouchEvent, GestureType, GestureResponse, ActionType
from .state import OverlayState
from .overlay import OverlayManager
class EbookReader:
"""
@@ -121,6 +125,10 @@ class EbookReader:
self._selection_start: Optional[Tuple[int, int]] = None
self._selection_end: Optional[Tuple[int, int]] = None
self._selected_range: Optional[SelectionRange] = None
# Overlay management
self.overlay_manager = OverlayManager(page_size=page_size)
self.current_overlay_state = OverlayState.NONE
def load_epub(self, epub_path: str) -> bool:
"""
@@ -178,10 +186,61 @@ class EbookReader:
print(f"Error loading EPUB: {e}")
return False
def load_html(self, html_string: str, title: str = "HTML Document", author: str = "Unknown", document_id: str = "html_doc") -> bool:
"""
Load HTML content directly into the reader.
This is useful for rendering library screens, menus, or other HTML-based UI elements
using the same rendering engine as the ebook reader.
Args:
html_string: HTML content to render
title: Document title (for metadata)
author: Document author (for metadata)
document_id: Unique identifier for this HTML document
Returns:
True if loaded successfully, False otherwise
"""
try:
# Parse HTML into blocks
blocks = parse_html_string(html_string)
if not blocks:
raise ValueError("No content blocks parsed from HTML")
# Set metadata
self.book_title = title
self.book_author = author
self.document_id = document_id
self.blocks = blocks
# Initialize the ereader manager
self.manager = EreaderLayoutManager(
blocks=self.blocks,
page_size=self.page_size,
document_id=self.document_id,
buffer_size=self.buffer_size,
page_style=self.page_style,
bookmarks_dir=self.bookmarks_dir
)
# Initialize highlight manager for this document
self.highlight_manager = HighlightManager(
document_id=self.document_id,
highlights_dir=self.highlights_dir
)
return True
except Exception as e:
print(f"Error loading HTML: {e}")
return False
def is_loaded(self) -> bool:
"""Check if a book is currently loaded."""
return self.manager is not None
def get_current_page(self, include_highlights: bool = True) -> Optional[Image.Image]:
"""
Get the current page as a PIL Image.
@@ -646,7 +705,15 @@ class EbookReader:
if not self.is_loaded():
return GestureResponse(ActionType.ERROR, {"message": "No book loaded"})
# Dispatch based on gesture type
# Handle overlay-specific gestures first
if self.is_overlay_open():
if event.gesture == GestureType.TAP:
return self._handle_overlay_tap(event.x, event.y)
elif event.gesture == GestureType.SWIPE_DOWN:
# Swipe down closes overlay
return self._handle_overlay_close()
# Dispatch based on gesture type for normal reading mode
if event.gesture == GestureType.TAP:
return self._handle_tap(event.x, event.y)
elif event.gesture == GestureType.LONG_PRESS:
@@ -655,6 +722,9 @@ class EbookReader:
return self._handle_page_forward()
elif event.gesture == GestureType.SWIPE_RIGHT:
return self._handle_page_back()
elif event.gesture == GestureType.SWIPE_UP:
# Swipe up from bottom opens TOC overlay
return self._handle_swipe_up(event.y)
elif event.gesture == GestureType.PINCH_IN:
return self._handle_zoom_out()
elif event.gesture == GestureType.PINCH_OUT:
@@ -829,6 +899,77 @@ class EbookReader:
"bounds": self._selected_range.bounds_list
})
def _handle_swipe_up(self, y: int) -> GestureResponse:
"""Handle swipe up gesture - opens TOC overlay if from bottom of screen"""
# Check if swipe started from bottom 20% of screen
bottom_threshold = self.page_size[1] * 0.8
if y >= bottom_threshold:
# Open TOC overlay
overlay_image = self.open_toc_overlay()
if overlay_image:
return GestureResponse(ActionType.OVERLAY_OPENED, {
"overlay_type": "toc",
"chapters": self.get_chapters()
})
return GestureResponse(ActionType.NONE, {})
def _handle_overlay_tap(self, x: int, y: int) -> GestureResponse:
"""Handle tap when overlay is open - select chapter or close overlay"""
# For TOC overlay, use pyWebLayout link query to detect chapter clicks
if self.current_overlay_state == OverlayState.TOC:
# 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 (chapter)
if query_result.get("is_interactive") and query_result.get("link_target"):
link_target = query_result["link_target"]
# Parse "chapter:N" format
if 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
# Not a chapter link, 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, {})
def _handle_overlay_close(self) -> GestureResponse:
"""Handle overlay close gesture (swipe down)"""
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# ===================================================================
# Highlighting API
# ===================================================================
@@ -1037,6 +1178,107 @@ class EbookReader:
return result
# ===================================================================
# Overlay Management API
# ===================================================================
def open_toc_overlay(self) -> Optional[Image.Image]:
"""
Open the table of contents overlay.
Returns:
Composited image with TOC 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
chapters = self.get_chapters()
# Open overlay and get composited image
result = self.overlay_manager.open_toc_overlay(chapters, base_page)
self.current_overlay_state = OverlayState.TOC
return result
def open_settings_overlay(self) -> Optional[Image.Image]:
"""
Open the settings overlay.
Returns:
Composited image with settings 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
# Open overlay and get composited image
result = self.overlay_manager.open_settings_overlay(base_page)
self.current_overlay_state = OverlayState.SETTINGS
return result
def open_bookmarks_overlay(self) -> Optional[Image.Image]:
"""
Open the bookmarks overlay.
Returns:
Composited image with bookmarks 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 bookmarks
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_bookmarks_overlay(bookmarks, base_page)
self.current_overlay_state = OverlayState.BOOKMARKS
return result
def close_overlay(self) -> Optional[Image.Image]:
"""
Close the current overlay and return to reading view.
Returns:
Base page image without overlay, or None if no overlay was open
"""
if self.current_overlay_state == OverlayState.NONE:
return None
result = self.overlay_manager.close_overlay()
self.current_overlay_state = OverlayState.NONE
# Return fresh current page
return self.get_current_page()
def is_overlay_open(self) -> bool:
"""Check if an overlay is currently open."""
return self.current_overlay_state != OverlayState.NONE
def get_overlay_state(self) -> OverlayState:
"""Get current overlay state."""
return self.current_overlay_state
def close(self):
"""
Close the reader and save current position.