refinements
This commit is contained in:
parent
a552eb0951
commit
0f9e38eb7c
@ -24,7 +24,6 @@ from dreader.state import (
|
|||||||
OverlayState
|
OverlayState
|
||||||
)
|
)
|
||||||
from dreader.library import LibraryManager
|
from dreader.library import LibraryManager
|
||||||
from dreader.overlay import OverlayManager
|
|
||||||
from dreader.main import DReaderApplication, AppConfig
|
from dreader.main import DReaderApplication, AppConfig
|
||||||
from dreader.hal import DisplayHAL, KeyboardInputHAL, EventLoopHAL
|
from dreader.hal import DisplayHAL, KeyboardInputHAL, EventLoopHAL
|
||||||
|
|
||||||
@ -56,9 +55,6 @@ __all__ = [
|
|||||||
# Library
|
# Library
|
||||||
"LibraryManager",
|
"LibraryManager",
|
||||||
|
|
||||||
# Overlay
|
|
||||||
"OverlayManager",
|
|
||||||
|
|
||||||
# Main application
|
# Main application
|
||||||
"DReaderApplication",
|
"DReaderApplication",
|
||||||
"AppConfig",
|
"AppConfig",
|
||||||
|
|||||||
@ -51,7 +51,6 @@ from pyWebLayout.core.highlight import Highlight, HighlightColor, create_highlig
|
|||||||
|
|
||||||
from .gesture import TouchEvent, GestureType, GestureResponse, ActionType
|
from .gesture import TouchEvent, GestureType, GestureResponse, ActionType
|
||||||
from .state import OverlayState
|
from .state import OverlayState
|
||||||
from .overlay import OverlayManager
|
|
||||||
from .managers import DocumentManager, SettingsManager, HighlightCoordinator
|
from .managers import DocumentManager, SettingsManager, HighlightCoordinator
|
||||||
from .handlers import GestureRouter
|
from .handlers import GestureRouter
|
||||||
from .overlays import NavigationOverlay, SettingsOverlay, TOCOverlay
|
from .overlays import NavigationOverlay, SettingsOverlay, TOCOverlay
|
||||||
@ -130,17 +129,14 @@ class EbookReader:
|
|||||||
self.base_font_scale = 1.0
|
self.base_font_scale = 1.0
|
||||||
self.font_scale_step = 0.1
|
self.font_scale_step = 0.1
|
||||||
|
|
||||||
# Overlay management (legacy - kept for backward compatibility)
|
# Overlay sub-applications
|
||||||
self.overlay_manager = OverlayManager(page_size=page_size)
|
|
||||||
self.current_overlay_state = OverlayState.NONE
|
|
||||||
|
|
||||||
# Overlay sub-applications (NEW architecture)
|
|
||||||
self._overlay_subapps = {
|
self._overlay_subapps = {
|
||||||
OverlayState.NAVIGATION: NavigationOverlay(self),
|
OverlayState.NAVIGATION: NavigationOverlay(self),
|
||||||
OverlayState.SETTINGS: SettingsOverlay(self),
|
OverlayState.SETTINGS: SettingsOverlay(self),
|
||||||
OverlayState.TOC: TOCOverlay(self),
|
OverlayState.TOC: TOCOverlay(self),
|
||||||
}
|
}
|
||||||
self._active_overlay = None # Current active overlay sub-application
|
self._active_overlay = None # Current active overlay sub-application
|
||||||
|
self.current_overlay_state = OverlayState.NONE
|
||||||
|
|
||||||
def load_epub(self, epub_path: str) -> bool:
|
def load_epub(self, epub_path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -1014,29 +1010,12 @@ class EbookReader:
|
|||||||
"""
|
"""
|
||||||
Open the bookmarks overlay.
|
Open the bookmarks overlay.
|
||||||
|
|
||||||
|
This is a convenience method that opens the navigation overlay with the bookmarks tab active.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Composited image with bookmarks overlay on top of current page, or None if no book loaded
|
Composited image with bookmarks overlay on top of current page, or None if no book loaded
|
||||||
"""
|
"""
|
||||||
if not self.is_loaded():
|
return self.open_navigation_overlay(active_tab="bookmarks")
|
||||||
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 open_navigation_overlay(self, active_tab: str = "contents") -> Optional[Image.Image]:
|
def open_navigation_overlay(self, active_tab: str = "contents") -> Optional[Image.Image]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -247,8 +247,12 @@ class GestureRouter:
|
|||||||
return GestureResponse(ActionType.NONE, {})
|
return GestureResponse(ActionType.NONE, {})
|
||||||
|
|
||||||
def _handle_swipe_down(self, y: int) -> GestureResponse:
|
def _handle_swipe_down(self, y: int) -> GestureResponse:
|
||||||
"""Handle swipe down gesture - opens Settings overlay"""
|
"""Handle swipe down gesture - opens Settings overlay (only from top 20% of screen)"""
|
||||||
# Open settings overlay from anywhere on screen
|
# Only open settings overlay if swipe starts from top 20% of screen
|
||||||
|
top_threshold = self.reader.page_size[1] * 0.2
|
||||||
|
if y > top_threshold:
|
||||||
|
return GestureResponse(ActionType.NONE, {})
|
||||||
|
|
||||||
overlay_image = self.reader.open_settings_overlay()
|
overlay_image = self.reader.open_settings_overlay()
|
||||||
if overlay_image:
|
if overlay_image:
|
||||||
return GestureResponse(ActionType.OVERLAY_OPENED, {
|
return GestureResponse(ActionType.OVERLAY_OPENED, {
|
||||||
|
|||||||
@ -1,563 +0,0 @@
|
|||||||
"""
|
|
||||||
Overlay management for dreader application.
|
|
||||||
|
|
||||||
Handles rendering and compositing of overlay screens (TOC, Settings, Bookmarks)
|
|
||||||
on top of the base reading page.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Optional, List, Dict, Any, Tuple
|
|
||||||
from pathlib import Path
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from .state import OverlayState
|
|
||||||
from .html_generator import (
|
|
||||||
generate_toc_overlay,
|
|
||||||
generate_settings_overlay,
|
|
||||||
generate_bookmarks_overlay,
|
|
||||||
generate_navigation_overlay
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OverlayManager:
|
|
||||||
"""
|
|
||||||
Manages overlay rendering and interaction.
|
|
||||||
|
|
||||||
Handles:
|
|
||||||
- Generating overlay HTML
|
|
||||||
- Rendering HTML to images using pyWebLayout
|
|
||||||
- Compositing overlays on top of base pages
|
|
||||||
- Tracking current overlay state
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, page_size: Tuple[int, int] = (800, 1200)):
|
|
||||||
"""
|
|
||||||
Initialize overlay manager.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
page_size: Size of the page/overlay (width, height)
|
|
||||||
"""
|
|
||||||
self.page_size = page_size
|
|
||||||
self.current_overlay = OverlayState.NONE
|
|
||||||
self._cached_base_page: Optional[Image.Image] = None
|
|
||||||
self._cached_overlay_image: Optional[Image.Image] = None
|
|
||||||
self._overlay_reader = None # Will be EbookReader instance for rendering overlays
|
|
||||||
self._overlay_panel_offset: Tuple[int, int] = (0, 0) # Panel position on screen
|
|
||||||
|
|
||||||
def render_html_to_image(self, html: str, size: Optional[Tuple[int, int]] = None) -> Image.Image:
|
|
||||||
"""
|
|
||||||
Render HTML content to a PIL Image using pyWebLayout.
|
|
||||||
|
|
||||||
This creates a temporary EbookReader instance to render the HTML,
|
|
||||||
then extracts the rendered page as an image.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
html: HTML string to render
|
|
||||||
size: Optional (width, height) for rendering size. Defaults to self.page_size
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PIL Image of the rendered HTML
|
|
||||||
"""
|
|
||||||
# Import here to avoid circular dependency
|
|
||||||
from .application import EbookReader
|
|
||||||
|
|
||||||
render_size = size if size else self.page_size
|
|
||||||
|
|
||||||
# Create a temporary reader for rendering this HTML
|
|
||||||
temp_reader = EbookReader(
|
|
||||||
page_size=render_size,
|
|
||||||
margin=15,
|
|
||||||
background_color=(255, 255, 255)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load the HTML content
|
|
||||||
success = temp_reader.load_html(
|
|
||||||
html_string=html,
|
|
||||||
title="Overlay",
|
|
||||||
author="",
|
|
||||||
document_id="temp_overlay"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
raise ValueError("Failed to load HTML for overlay rendering")
|
|
||||||
|
|
||||||
# Get the rendered page
|
|
||||||
image = temp_reader.get_current_page()
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
temp_reader.close()
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
def composite_overlay(self, base_image: Image.Image, overlay_panel: Image.Image) -> Image.Image:
|
|
||||||
"""
|
|
||||||
Composite overlay panel on top of base image with darkened background.
|
|
||||||
|
|
||||||
Creates a popup effect by:
|
|
||||||
1. Darkening the base image (multiply by 0.5)
|
|
||||||
2. Placing the overlay panel (60% size) centered on top
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_image: Base page image (reading page)
|
|
||||||
overlay_panel: Rendered overlay panel (TOC, settings, etc.)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Composited PIL Image with popup overlay effect
|
|
||||||
"""
|
|
||||||
from PIL import ImageDraw, ImageEnhance
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# Convert base image to RGB
|
|
||||||
result = base_image.convert('RGB').copy()
|
|
||||||
|
|
||||||
# Lighten the background slightly (70% brightness for e-ink visibility)
|
|
||||||
enhancer = ImageEnhance.Brightness(result)
|
|
||||||
result = enhancer.enhance(0.7)
|
|
||||||
|
|
||||||
# Convert overlay panel to RGB
|
|
||||||
if overlay_panel.mode != 'RGB':
|
|
||||||
overlay_panel = overlay_panel.convert('RGB')
|
|
||||||
|
|
||||||
# Calculate centered position for the panel
|
|
||||||
panel_x = int((self.page_size[0] - overlay_panel.width) / 2)
|
|
||||||
panel_y = int((self.page_size[1] - overlay_panel.height) / 2)
|
|
||||||
|
|
||||||
# Add a thick black border around the panel for e-ink clarity
|
|
||||||
draw = ImageDraw.Draw(result)
|
|
||||||
border_width = 3
|
|
||||||
draw.rectangle(
|
|
||||||
[panel_x - border_width, panel_y - border_width,
|
|
||||||
panel_x + overlay_panel.width + border_width, panel_y + overlay_panel.height + border_width],
|
|
||||||
outline=(0, 0, 0),
|
|
||||||
width=border_width
|
|
||||||
)
|
|
||||||
|
|
||||||
# Paste the panel onto the dimmed background
|
|
||||||
result.paste(overlay_panel, (panel_x, panel_y))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def open_toc_overlay(self, chapters: List[Tuple[str, int]], base_page: Image.Image) -> Image.Image:
|
|
||||||
"""
|
|
||||||
Open the table of contents overlay.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
chapters: List of (chapter_title, chapter_index) tuples
|
|
||||||
base_page: Current reading page to show underneath
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Composited image with TOC overlay on top
|
|
||||||
"""
|
|
||||||
# Import here to avoid circular dependency
|
|
||||||
from .application import EbookReader
|
|
||||||
|
|
||||||
# Calculate panel size (60% of screen)
|
|
||||||
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 TOC HTML with clickable links
|
|
||||||
html = generate_toc_overlay(chapter_data, 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="Table of Contents",
|
|
||||||
author="",
|
|
||||||
document_id="toc_overlay"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
raise ValueError("Failed to load TOC 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.TOC
|
|
||||||
|
|
||||||
# Composite and return
|
|
||||||
return self.composite_overlay(base_page, overlay_panel)
|
|
||||||
|
|
||||||
def open_settings_overlay(
|
|
||||||
self,
|
|
||||||
base_page: Image.Image,
|
|
||||||
font_scale: float = 1.0,
|
|
||||||
line_spacing: int = 5,
|
|
||||||
inter_block_spacing: int = 15,
|
|
||||||
word_spacing: int = 0
|
|
||||||
) -> Image.Image:
|
|
||||||
"""
|
|
||||||
Open the settings overlay with current settings values.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_page: Current reading page to show underneath
|
|
||||||
font_scale: Current font scale
|
|
||||||
line_spacing: Current line spacing
|
|
||||||
inter_block_spacing: Current inter-block spacing
|
|
||||||
word_spacing: Current word spacing
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Composited image with settings overlay on top
|
|
||||||
"""
|
|
||||||
# Import here to avoid circular dependency
|
|
||||||
from .application import EbookReader
|
|
||||||
|
|
||||||
# Calculate panel size (60% of screen)
|
|
||||||
panel_width = int(self.page_size[0] * 0.6)
|
|
||||||
panel_height = int(self.page_size[1] * 0.7)
|
|
||||||
|
|
||||||
# Generate settings HTML with current values
|
|
||||||
html = generate_settings_overlay(
|
|
||||||
font_scale=font_scale,
|
|
||||||
line_spacing=line_spacing,
|
|
||||||
inter_block_spacing=inter_block_spacing,
|
|
||||||
word_spacing=word_spacing,
|
|
||||||
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="Settings",
|
|
||||||
author="",
|
|
||||||
document_id="settings_overlay"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
raise ValueError("Failed to load settings 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.SETTINGS
|
|
||||||
|
|
||||||
# Composite and return
|
|
||||||
return self.composite_overlay(base_page, overlay_panel)
|
|
||||||
|
|
||||||
def refresh_settings_overlay(
|
|
||||||
self,
|
|
||||||
updated_base_page: Image.Image,
|
|
||||||
font_scale: float,
|
|
||||||
line_spacing: int,
|
|
||||||
inter_block_spacing: int,
|
|
||||||
word_spacing: int = 0
|
|
||||||
) -> Image.Image:
|
|
||||||
"""
|
|
||||||
Refresh the settings overlay with updated values and background page.
|
|
||||||
|
|
||||||
This is used for live preview when settings change - it updates both
|
|
||||||
the background page (with new settings applied) and the overlay panel
|
|
||||||
(with new values displayed).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
updated_base_page: Updated reading page with new settings applied
|
|
||||||
font_scale: Updated font scale
|
|
||||||
line_spacing: Updated line spacing
|
|
||||||
inter_block_spacing: Updated inter-block spacing
|
|
||||||
word_spacing: Updated word spacing
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Composited image with updated settings overlay
|
|
||||||
"""
|
|
||||||
# Import here to avoid circular dependency
|
|
||||||
from .application import EbookReader
|
|
||||||
|
|
||||||
# Calculate panel size (60% of screen)
|
|
||||||
panel_width = int(self.page_size[0] * 0.6)
|
|
||||||
panel_height = int(self.page_size[1] * 0.7)
|
|
||||||
|
|
||||||
# Generate updated settings HTML
|
|
||||||
html = generate_settings_overlay(
|
|
||||||
font_scale=font_scale,
|
|
||||||
line_spacing=line_spacing,
|
|
||||||
inter_block_spacing=inter_block_spacing,
|
|
||||||
word_spacing=word_spacing,
|
|
||||||
page_size=(panel_width, panel_height)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Recreate overlay reader with updated HTML
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
success = self._overlay_reader.load_html(
|
|
||||||
html_string=html,
|
|
||||||
title="Settings",
|
|
||||||
author="",
|
|
||||||
document_id="settings_overlay"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
raise ValueError("Failed to load updated settings overlay HTML")
|
|
||||||
|
|
||||||
# Get the updated rendered panel
|
|
||||||
overlay_panel = self._overlay_reader.get_current_page()
|
|
||||||
|
|
||||||
# Update caches
|
|
||||||
self._cached_base_page = updated_base_page.copy()
|
|
||||||
self._cached_overlay_image = overlay_panel
|
|
||||||
|
|
||||||
# Composite and return
|
|
||||||
return self.composite_overlay(updated_base_page, overlay_panel)
|
|
||||||
|
|
||||||
def open_bookmarks_overlay(self, bookmarks: List[Dict[str, Any]], base_page: Image.Image) -> Image.Image:
|
|
||||||
"""
|
|
||||||
Open the bookmarks overlay.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bookmarks: List of bookmark dictionaries with 'name' and 'position' keys
|
|
||||||
base_page: Current reading page to show underneath
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Composited image with bookmarks overlay on top
|
|
||||||
"""
|
|
||||||
# Generate bookmarks HTML
|
|
||||||
html = generate_bookmarks_overlay(bookmarks)
|
|
||||||
|
|
||||||
# Render HTML to image
|
|
||||||
overlay_image = self.render_html_to_image(html)
|
|
||||||
|
|
||||||
# Cache for later use
|
|
||||||
self._cached_base_page = base_page.copy()
|
|
||||||
self._cached_overlay_image = overlay_image
|
|
||||||
self.current_overlay = OverlayState.BOOKMARKS
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Base page image (without overlay), or None if no overlay was open
|
|
||||||
"""
|
|
||||||
if self.current_overlay == OverlayState.NONE:
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.current_overlay = OverlayState.NONE
|
|
||||||
base_page = self._cached_base_page
|
|
||||||
|
|
||||||
# Clear caches
|
|
||||||
self._cached_base_page = None
|
|
||||||
self._cached_overlay_image = None
|
|
||||||
self._overlay_panel_offset = (0, 0)
|
|
||||||
|
|
||||||
# Close overlay reader
|
|
||||||
if self._overlay_reader:
|
|
||||||
self._overlay_reader.close()
|
|
||||||
self._overlay_reader = None
|
|
||||||
|
|
||||||
return base_page
|
|
||||||
|
|
||||||
def is_overlay_open(self) -> bool:
|
|
||||||
"""Check if an overlay is currently open."""
|
|
||||||
return self.current_overlay != OverlayState.NONE
|
|
||||||
|
|
||||||
def get_current_overlay_type(self) -> OverlayState:
|
|
||||||
"""Get the type of currently open overlay."""
|
|
||||||
return self.current_overlay
|
|
||||||
|
|
||||||
def query_overlay_pixel(self, x: int, y: int) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Query a pixel in the current overlay to detect interactions.
|
|
||||||
|
|
||||||
Uses pyWebLayout's query_point() to detect which element was tapped,
|
|
||||||
including link targets and data attributes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x, y: Pixel coordinates to query (in screen space)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with query result data (text, link_target, is_interactive),
|
|
||||||
or None if no overlay open or query failed
|
|
||||||
"""
|
|
||||||
if not self.is_overlay_open() or not self._overlay_reader:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Translate screen coordinates to overlay panel coordinates
|
|
||||||
panel_x, panel_y = self._overlay_panel_offset
|
|
||||||
overlay_x = x - panel_x
|
|
||||||
overlay_y = y - panel_y
|
|
||||||
|
|
||||||
# Check if coordinates are within the overlay panel
|
|
||||||
if overlay_x < 0 or overlay_y < 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Get the current page from the overlay reader
|
|
||||||
if not self._overlay_reader.manager:
|
|
||||||
return None
|
|
||||||
|
|
||||||
current_page = self._overlay_reader.manager.get_current_page()
|
|
||||||
if not current_page:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Query the point
|
|
||||||
result = current_page.query_point((overlay_x, overlay_y))
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Extract relevant data from QueryResult
|
|
||||||
return {
|
|
||||||
"text": result.text,
|
|
||||||
"link_target": result.link_target,
|
|
||||||
"is_interactive": result.is_interactive,
|
|
||||||
"bounds": result.bounds,
|
|
||||||
"object_type": result.object_type
|
|
||||||
}
|
|
||||||
@ -29,12 +29,18 @@ class TestSettingsOverlay(unittest.TestCase):
|
|||||||
|
|
||||||
self.reader = EbookReader(page_size=(800, 1200))
|
self.reader = EbookReader(page_size=(800, 1200))
|
||||||
|
|
||||||
# Load a test EPUB - use any available EPUB in test data
|
# Load a test EPUB - use a larger EPUB for spacing tests
|
||||||
epub_dir = Path(__file__).parent / 'data' / 'library-epub'
|
epub_dir = Path(__file__).parent / 'data' / 'library-epub'
|
||||||
epubs = list(epub_dir.glob('*.epub'))
|
epubs = list(epub_dir.glob('*.epub'))
|
||||||
if not epubs:
|
if not epubs:
|
||||||
self.skipTest("No test EPUB files available")
|
self.skipTest("No test EPUB files available")
|
||||||
|
|
||||||
|
# Prefer larger EPUBs for better testing of spacing changes
|
||||||
|
# Skip minimal-test.epub as it has too little content
|
||||||
|
epubs = [e for e in epubs if 'minimal' not in e.name]
|
||||||
|
if not epubs:
|
||||||
|
epubs = list(epub_dir.glob('*.epub'))
|
||||||
|
|
||||||
test_epub = epubs[0]
|
test_epub = epubs[0]
|
||||||
|
|
||||||
# Debug logging
|
# Debug logging
|
||||||
@ -175,9 +181,12 @@ class TestSettingsOverlay(unittest.TestCase):
|
|||||||
self.reader.open_settings_overlay()
|
self.reader.open_settings_overlay()
|
||||||
initial_font_scale = self.reader.base_font_scale
|
initial_font_scale = self.reader.base_font_scale
|
||||||
|
|
||||||
# Get overlay reader to query button positions
|
# Get overlay reader to query button positions from the active overlay sub-application
|
||||||
overlay_manager = self.reader.overlay_manager
|
overlay_subapp = self.reader._active_overlay
|
||||||
overlay_reader = overlay_manager._overlay_reader
|
if not overlay_subapp:
|
||||||
|
self.skipTest("No active overlay sub-application")
|
||||||
|
|
||||||
|
overlay_reader = overlay_subapp._overlay_reader
|
||||||
|
|
||||||
if not overlay_reader or not overlay_reader.manager:
|
if not overlay_reader or not overlay_reader.manager:
|
||||||
self.skipTest("Overlay reader not available for querying")
|
self.skipTest("Overlay reader not available for querying")
|
||||||
@ -302,15 +311,17 @@ class TestSettingsOverlay(unittest.TestCase):
|
|||||||
# Open overlay
|
# Open overlay
|
||||||
self.reader.open_settings_overlay()
|
self.reader.open_settings_overlay()
|
||||||
|
|
||||||
# Access refresh method through overlay manager
|
# Access refresh method through active overlay sub-application
|
||||||
overlay_manager = self.reader.overlay_manager
|
overlay_subapp = self.reader._active_overlay
|
||||||
|
if not overlay_subapp:
|
||||||
|
self.skipTest("No active overlay sub-application")
|
||||||
|
|
||||||
# Change a setting programmatically
|
# Change a setting programmatically
|
||||||
self.reader.increase_font_size()
|
self.reader.increase_font_size()
|
||||||
new_page = self.reader.get_current_page(include_highlights=False)
|
new_page = self.reader.get_current_page(include_highlights=False)
|
||||||
|
|
||||||
# Refresh overlay
|
# Refresh overlay
|
||||||
refreshed_image = overlay_manager.refresh_settings_overlay(
|
refreshed_image = overlay_subapp.refresh(
|
||||||
updated_base_page=new_page,
|
updated_base_page=new_page,
|
||||||
font_scale=self.reader.base_font_scale,
|
font_scale=self.reader.base_font_scale,
|
||||||
line_spacing=self.reader.page_style.line_spacing,
|
line_spacing=self.reader.page_style.line_spacing,
|
||||||
|
|||||||
@ -44,9 +44,14 @@ class TestTOCOverlay(unittest.TestCase):
|
|||||||
self.reader.close()
|
self.reader.close()
|
||||||
|
|
||||||
def test_overlay_manager_initialization(self):
|
def test_overlay_manager_initialization(self):
|
||||||
"""Test that overlay manager is properly initialized"""
|
"""Test that overlay sub-applications are properly initialized"""
|
||||||
self.assertIsNotNone(self.reader.overlay_manager)
|
# Check that overlay sub-applications exist
|
||||||
self.assertEqual(self.reader.overlay_manager.page_size, (800, 1200))
|
self.assertIsNotNone(self.reader._overlay_subapps)
|
||||||
|
self.assertIn(OverlayState.TOC, self.reader._overlay_subapps)
|
||||||
|
self.assertIn(OverlayState.SETTINGS, self.reader._overlay_subapps)
|
||||||
|
self.assertIn(OverlayState.NAVIGATION, self.reader._overlay_subapps)
|
||||||
|
|
||||||
|
# Initially no overlay should be active
|
||||||
self.assertFalse(self.reader.is_overlay_open())
|
self.assertFalse(self.reader.is_overlay_open())
|
||||||
self.assertEqual(self.reader.get_overlay_state(), OverlayState.NONE)
|
self.assertEqual(self.reader.get_overlay_state(), OverlayState.NONE)
|
||||||
|
|
||||||
@ -298,13 +303,12 @@ class TestOverlayRendering(unittest.TestCase):
|
|||||||
self.assertIsNotNone(html)
|
self.assertIsNotNone(html)
|
||||||
self.assertIn("Table of Contents", html)
|
self.assertIn("Table of Contents", html)
|
||||||
|
|
||||||
# Render HTML to image using overlay manager
|
# Open the TOC overlay which internally renders HTML to image
|
||||||
overlay_manager = self.reader.overlay_manager
|
overlay_image = self.reader.open_toc_overlay()
|
||||||
image = overlay_manager.render_html_to_image(html)
|
|
||||||
|
|
||||||
# Should produce valid image
|
# Should produce valid image
|
||||||
self.assertIsNotNone(image)
|
self.assertIsNotNone(overlay_image)
|
||||||
self.assertEqual(image.size, (800, 1200))
|
self.assertEqual(overlay_image.size, (800, 1200))
|
||||||
|
|
||||||
|
|
||||||
class TestTOCPagination(unittest.TestCase):
|
class TestTOCPagination(unittest.TestCase):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user