+245
-3
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user