Update coverage badges [skip ci]

This commit is contained in:
Gitea Action
2025-11-23 14:10:14 +00:00
commit f83411929c
107 changed files with 23106 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
"""
Overlay sub-applications for dreader.
Each overlay is a self-contained sub-application that handles its own:
- HTML generation
- Rendering logic
- Gesture handling
- State management
"""
from .base import OverlaySubApplication
from .navigation import NavigationOverlay
from .settings import SettingsOverlay
from .toc import TOCOverlay
__all__ = [
'OverlaySubApplication',
'NavigationOverlay',
'SettingsOverlay',
'TOCOverlay',
]
+359
View File
@@ -0,0 +1,359 @@
"""
Base class for overlay sub-applications.
This provides a common interface for all overlay types (TOC, Settings, Navigation, etc.)
Each overlay is a self-contained sub-application that handles its own rendering and gestures.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Optional, Dict, Any, Tuple
from PIL import Image
from ..gesture import GestureResponse, ActionType
from ..state import OverlayState
if TYPE_CHECKING:
from ..application import EbookReader
class OverlaySubApplication(ABC):
"""
Base class for overlay sub-applications.
Each overlay type extends this class and implements:
- open(): Generate HTML, render, and return composited image
- handle_tap(): Process tap gestures within the overlay
- close(): Clean up and return base page
- get_overlay_type(): Return the OverlayState enum value
The base class provides:
- Common rendering infrastructure (HTML to image conversion)
- Coordinate translation (screen to overlay panel)
- Query pixel support (detecting interactive elements)
- Compositing (darkened background + centered panel)
"""
def __init__(self, reader: 'EbookReader'):
"""
Initialize overlay sub-application.
Args:
reader: Reference to parent EbookReader instance
"""
self.reader = reader
self.page_size = reader.page_size
# Overlay rendering state
self._overlay_reader: Optional['EbookReader'] = None
self._cached_base_page: Optional[Image.Image] = None
self._cached_overlay_image: Optional[Image.Image] = None
self._overlay_panel_offset: Tuple[int, int] = (0, 0)
self._panel_size: Tuple[int, int] = (0, 0)
@abstractmethod
def get_overlay_type(self) -> OverlayState:
"""
Get the overlay type identifier.
Returns:
OverlayState enum value for this overlay
"""
pass
@abstractmethod
def open(self, base_page: Image.Image, **kwargs) -> Image.Image:
"""
Open the overlay and return composited image.
Args:
base_page: Current reading page to show underneath
**kwargs: Overlay-specific parameters
Returns:
Composited image with overlay on top of base page
"""
pass
@abstractmethod
def handle_tap(self, x: int, y: int) -> GestureResponse:
"""
Handle tap gesture within the overlay.
Args:
x, y: Screen coordinates of tap
Returns:
GestureResponse indicating what action to take
"""
pass
def close(self) -> Optional[Image.Image]:
"""
Close the overlay and clean up resources.
Returns:
Base page image (without overlay), or None if not open
"""
base_page = self._cached_base_page
# Clear caches
self._cached_base_page = None
self._cached_overlay_image = None
self._overlay_panel_offset = (0, 0)
self._panel_size = (0, 0)
# Close overlay reader
if self._overlay_reader:
self._overlay_reader.close()
self._overlay_reader = None
return base_page
# ===================================================================
# Common Infrastructure Methods
# ===================================================================
def render_html_to_image(self, html: str, panel_size: Tuple[int, int]) -> Image.Image:
"""
Render HTML to image using a temporary EbookReader.
Args:
html: HTML content to render
panel_size: Size for the overlay panel (width, height)
Returns:
Rendered PIL Image of the HTML
"""
# Import here to avoid circular dependency
from ..application import EbookReader
# Create or reuse overlay reader
if self._overlay_reader:
self._overlay_reader.close()
self._overlay_reader = EbookReader(
page_size=panel_size,
margin=15,
background_color=(255, 255, 255)
)
# Load the HTML content
success = self._overlay_reader.load_html(
html_string=html,
title=f"{self.get_overlay_type().name} Overlay",
author="",
document_id=f"{self.get_overlay_type().name.lower()}_overlay"
)
if not success:
raise ValueError(f"Failed to load {self.get_overlay_type().name} overlay HTML")
# Get the rendered page
return self._overlay_reader.get_current_page()
def composite_overlay(self, base_page: Image.Image, overlay_panel: Image.Image) -> Image.Image:
"""
Composite overlay panel on top of base page with darkened background.
Creates popup effect by:
1. Darkening the base image (70% brightness for e-ink visibility)
2. Placing the overlay panel centered on top with a border
Args:
base_page: Base reading page
overlay_panel: Rendered overlay panel
Returns:
Composited PIL Image with popup effect
"""
from PIL import ImageDraw, ImageEnhance
import os
# Convert base image to RGB
result = base_page.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')
# DEBUG: Draw bounding boxes on interactive elements if debug mode enabled
debug_mode = os.environ.get('DREADER_DEBUG_OVERLAY', '0') == '1'
if debug_mode:
overlay_panel = self._draw_debug_bounding_boxes(overlay_panel.copy())
# 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)
# Store panel position and size for coordinate translation
self._overlay_panel_offset = (panel_x, panel_y)
self._panel_size = (overlay_panel.width, overlay_panel.height)
# 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 query_overlay_pixel(self, x: int, y: int) -> Optional[Dict[str, Any]]:
"""
Query a pixel in the overlay to detect interactive elements.
Uses pyWebLayout's query_point() to detect tapped elements,
including link targets and data attributes.
Args:
x, y: Screen coordinates to query
Returns:
Dictionary with query result (text, link_target, is_interactive),
or None if query failed or coordinates outside overlay
"""
if 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
panel_width, panel_height = self._panel_size
if overlay_x >= panel_width or overlay_y >= panel_height:
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))
import logging
logger = logging.getLogger(__name__)
logger.info(f"[OVERLAY_BASE] query_point({overlay_x}, {overlay_y}) returned: {result}")
if result:
logger.info(f"[OVERLAY_BASE] text={result.text}, link_target={result.link_target}, is_interactive={result.is_interactive}")
logger.info(f"[OVERLAY_BASE] bounds={result.bounds}, object_type={result.object_type}")
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
}
def _calculate_panel_size(self, width_ratio: float = 0.6, height_ratio: float = 0.7) -> Tuple[int, int]:
"""
Calculate overlay panel size as a percentage of screen size.
Args:
width_ratio: Panel width as ratio of screen width (default 60%)
height_ratio: Panel height as ratio of screen height (default 70%)
Returns:
Tuple of (panel_width, panel_height) in pixels
"""
panel_width = int(self.page_size[0] * width_ratio)
panel_height = int(self.page_size[1] * height_ratio)
return (panel_width, panel_height)
def _draw_debug_bounding_boxes(self, overlay_panel: Image.Image) -> Image.Image:
"""
Draw bounding boxes around all interactive elements for debugging.
This scans the overlay panel and draws red rectangles around all
clickable elements to help visualize where users need to click.
Args:
overlay_panel: Overlay panel image to annotate
Returns:
Annotated overlay panel with bounding boxes
"""
from PIL import ImageDraw, ImageFont
import logging
logger = logging.getLogger(__name__)
if not self._overlay_reader or not self._overlay_reader.manager:
logger.warning("[DEBUG] No overlay reader available for debug visualization")
return overlay_panel
page = self._overlay_reader.manager.get_current_page()
if not page:
logger.warning("[DEBUG] No page available for debug visualization")
return overlay_panel
# Scan for all interactive elements
panel_width, panel_height = overlay_panel.size
link_regions = {} # link_target -> (min_x, min_y, max_x, max_y)
logger.info(f"[DEBUG] Scanning {panel_width}x{panel_height} overlay for interactive elements...")
# Scan with fine granularity to find all interactive pixels
for y in range(0, panel_height, 2):
for x in range(0, panel_width, 2):
result = page.query_point((x, y))
if result and result.link_target:
if result.link_target not in link_regions:
link_regions[result.link_target] = [x, y, x, y]
else:
# Expand bounding box
link_regions[result.link_target][0] = min(link_regions[result.link_target][0], x)
link_regions[result.link_target][1] = min(link_regions[result.link_target][1], y)
link_regions[result.link_target][2] = max(link_regions[result.link_target][2], x)
link_regions[result.link_target][3] = max(link_regions[result.link_target][3], y)
# Draw bounding boxes
draw = ImageDraw.Draw(overlay_panel)
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)
except:
font = ImageFont.load_default()
logger.info(f"[DEBUG] Found {len(link_regions)} interactive regions")
for link_target, (min_x, min_y, max_x, max_y) in link_regions.items():
# Draw red bounding box
draw.rectangle(
[min_x, min_y, max_x, max_y],
outline=(255, 0, 0),
width=2
)
# Draw label
label = link_target[:20] # Truncate if too long
draw.text((min_x + 2, min_y - 12), label, fill=(255, 0, 0), font=font)
logger.info(f"[DEBUG] {link_target}: ({min_x}, {min_y}) to ({max_x}, {max_y})")
return overlay_panel
+311
View File
@@ -0,0 +1,311 @@
"""
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]] = []
# Pagination state
self._toc_page: int = 0 # Current page in TOC
self._toc_items_per_page: int = 10 # Items per page
self._bookmarks_page: int = 0 # Current page in bookmarks
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
# Reset pagination when opening
self._toc_page = 0
self._bookmarks_page = 0
# 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,
toc_page=self._toc_page,
toc_items_per_page=self._toc_items_per_page,
bookmarks_page=self._bookmarks_page
)
# 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, {})
# Parse "page:direction" format for pagination
elif link_target.startswith("page:"):
direction = link_target.split(":", 1)[1]
logger.info(f"[NAV_OVERLAY] Pagination button clicked: {direction}")
self._handle_pagination(direction)
return GestureResponse(ActionType.PAGE_CHANGED, {
"direction": direction,
"tab": self._active_tab
})
# 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,
toc_page=self._toc_page,
toc_items_per_page=self._toc_items_per_page,
bookmarks_page=self._bookmarks_page
)
# 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)
def _handle_pagination(self, direction: str) -> Optional[Image.Image]:
"""
Handle pagination within the active tab.
Args:
direction: Either "next" or "prev"
Returns:
Updated composited image with new page, or None if invalid
"""
import logging
logger = logging.getLogger(__name__)
if self._active_tab == "contents":
# Calculate total pages
total_items = len(self._cached_chapters)
total_pages = (total_items + self._toc_items_per_page - 1) // self._toc_items_per_page
# Update page number
if direction == "next" and self._toc_page < total_pages - 1:
self._toc_page += 1
logger.info(f"[NAV_OVERLAY] TOC page -> {self._toc_page + 1}/{total_pages}")
elif direction == "prev" and self._toc_page > 0:
self._toc_page -= 1
logger.info(f"[NAV_OVERLAY] TOC page -> {self._toc_page + 1}/{total_pages}")
else:
logger.info(f"[NAV_OVERLAY] Can't paginate {direction} from page {self._toc_page + 1}/{total_pages}")
return None
elif self._active_tab == "bookmarks":
# Calculate total pages
total_items = len(self._cached_bookmarks)
total_pages = (total_items + self._toc_items_per_page - 1) // self._toc_items_per_page
# Update page number
if direction == "next" and self._bookmarks_page < total_pages - 1:
self._bookmarks_page += 1
logger.info(f"[NAV_OVERLAY] Bookmarks page -> {self._bookmarks_page + 1}/{total_pages}")
elif direction == "prev" and self._bookmarks_page > 0:
self._bookmarks_page -= 1
logger.info(f"[NAV_OVERLAY] Bookmarks page -> {self._bookmarks_page + 1}/{total_pages}")
else:
logger.info(f"[NAV_OVERLAY] Can't paginate {direction} from page {self._bookmarks_page + 1}/{total_pages}")
return None
# Regenerate the overlay with new page
return self._switch_tab(self._active_tab)
+251
View File
@@ -0,0 +1,251 @@
"""
Settings overlay sub-application.
Provides interactive controls for adjusting reading settings with live preview.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from PIL import Image
from .base import OverlaySubApplication
from ..gesture import GestureResponse, ActionType
from ..state import OverlayState
from ..html_generator import generate_settings_overlay
if TYPE_CHECKING:
from ..application import EbookReader
class SettingsOverlay(OverlaySubApplication):
"""
Settings overlay with live preview.
Features:
- Font size adjustment (increase/decrease)
- Line spacing adjustment
- Inter-block spacing adjustment
- Word spacing adjustment
- Live preview of changes on base page
- Back to library button
"""
def get_overlay_type(self) -> OverlayState:
"""Return SETTINGS overlay type."""
return OverlayState.SETTINGS
def open(self, base_page: Image.Image, **kwargs) -> Image.Image:
"""
Open the settings overlay.
Args:
base_page: Current reading page to show underneath
font_scale: Current font scale
line_spacing: Current line spacing in pixels
inter_block_spacing: Current inter-block spacing in pixels
word_spacing: Current word spacing in pixels
font_family: Current font family name (e.g., "SERIF", "SANS", "MONOSPACE", or None)
Returns:
Composited image with settings overlay
"""
font_scale = kwargs.get('font_scale', 1.0)
line_spacing = kwargs.get('line_spacing', 5)
inter_block_spacing = kwargs.get('inter_block_spacing', 15)
word_spacing = kwargs.get('word_spacing', 0)
font_family = kwargs.get('font_family', 'Default')
# Calculate panel size (60% width, 70% height)
panel_size = self._calculate_panel_size(0.6, 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,
font_family=font_family,
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 settings overlay.
Detects:
- Setting adjustment controls (setting:action)
- Back to library button (action:back_to_library)
- 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"[SETTINGS_OVERLAY] Handling tap at ({x}, {y})")
logger.info(f"[SETTINGS_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"[SETTINGS_OVERLAY] Query result: {query_result}")
# If query failed (tap outside overlay panel), close it
if query_result is None:
logger.info(f"[SETTINGS_OVERLAY] Tap outside overlay panel, closing")
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
# Check if tapped on a settings control link
if query_result.get("is_interactive") and query_result.get("link_target"):
link_target = query_result["link_target"]
logger.info(f"[SETTINGS_OVERLAY] Found interactive link: {link_target}")
# Parse "setting:action" format
if link_target.startswith("setting:"):
action = link_target.split(":", 1)[1]
logger.info(f"[SETTINGS_OVERLAY] Applying setting change: {action}")
return self._apply_setting_change(action)
# Parse "action:command" format for other actions
elif link_target.startswith("action:"):
action = link_target.split(":", 1)[1]
if action == "back_to_library":
logger.info(f"[SETTINGS_OVERLAY] Back to library clicked")
return GestureResponse(ActionType.BACK_TO_LIBRARY, {})
# Tap inside overlay but not on interactive element - keep overlay open
logger.info(f"[SETTINGS_OVERLAY] Tap on non-interactive area inside overlay, ignoring")
return GestureResponse(ActionType.NONE, {})
def refresh(self, updated_base_page: Image.Image,
font_scale: float,
line_spacing: int,
inter_block_spacing: int,
word_spacing: int = 0,
font_family: str = "Default") -> 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
font_family: Updated font family
Returns:
Composited image with updated settings overlay
"""
# Calculate panel size (60% width, 70% height)
panel_size = self._calculate_panel_size(0.6, 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,
font_family=font_family,
page_size=panel_size
)
# Render HTML to image
overlay_panel = self.render_html_to_image(html, panel_size)
# 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 _apply_setting_change(self, action: str) -> GestureResponse:
"""
Apply a setting change and refresh the overlay.
Args:
action: Setting action (e.g., "font_increase", "line_spacing_decrease", "font_family_serif")
Returns:
GestureResponse with SETTING_CHANGED action
"""
from pyWebLayout.style.fonts import BundledFont
# Apply the setting change via reader
if action == "font_increase":
self.reader.increase_font_size()
elif action == "font_decrease":
self.reader.decrease_font_size()
elif action == "font_family_default":
self.reader.set_font_family(None)
elif action == "font_family_serif":
self.reader.set_font_family(BundledFont.SERIF)
elif action == "font_family_sans":
self.reader.set_font_family(BundledFont.SANS)
elif action == "font_family_monospace":
self.reader.set_font_family(BundledFont.MONOSPACE)
elif action == "line_spacing_increase":
new_spacing = self.reader.page_style.line_spacing + 2
self.reader.set_line_spacing(new_spacing)
elif action == "line_spacing_decrease":
new_spacing = max(0, self.reader.page_style.line_spacing - 2)
self.reader.set_line_spacing(new_spacing)
elif action == "block_spacing_increase":
new_spacing = self.reader.page_style.inter_block_spacing + 3
self.reader.set_inter_block_spacing(new_spacing)
elif action == "block_spacing_decrease":
new_spacing = max(0, self.reader.page_style.inter_block_spacing - 3)
self.reader.set_inter_block_spacing(new_spacing)
elif action == "word_spacing_increase":
new_spacing = self.reader.page_style.word_spacing + 2
self.reader.set_word_spacing(new_spacing)
elif action == "word_spacing_decrease":
new_spacing = max(0, self.reader.page_style.word_spacing - 2)
self.reader.set_word_spacing(new_spacing)
# Re-render the base page with new settings applied
# Must get directly from manager, not get_current_page() which returns overlay
page = self.reader.manager.get_current_page()
updated_page = page.render()
# Get font family for display
font_family = self.reader.get_font_family()
font_family_name = font_family.name if font_family else "Default"
# Refresh the settings overlay with updated values and page
self.refresh(
updated_base_page=updated_page,
font_scale=self.reader.base_font_scale,
line_spacing=self.reader.page_style.line_spacing,
inter_block_spacing=self.reader.page_style.inter_block_spacing,
word_spacing=self.reader.page_style.word_spacing,
font_family=font_family_name
)
return GestureResponse(ActionType.SETTING_CHANGED, {
"action": action,
"font_scale": self.reader.base_font_scale,
"font_family": font_family_name,
"line_spacing": self.reader.page_style.line_spacing,
"inter_block_spacing": self.reader.page_style.inter_block_spacing,
"word_spacing": self.reader.page_style.word_spacing
})
+128
View File
@@ -0,0 +1,128 @@
"""
Table of Contents overlay sub-application.
Simple TOC overlay (deprecated in favor of NavigationOverlay).
Kept for backward compatibility.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, List, Tuple
from PIL import Image
from .base import OverlaySubApplication
from ..gesture import GestureResponse, ActionType
from ..state import OverlayState
from ..html_generator import generate_toc_overlay
if TYPE_CHECKING:
from ..application import EbookReader
class TOCOverlay(OverlaySubApplication):
"""
Simple Table of Contents overlay.
NOTE: This is deprecated in favor of NavigationOverlay which provides
a unified interface for both TOC and bookmarks. Kept for backward compatibility.
Features:
- List of chapters with clickable links
- Chapter navigation
"""
def __init__(self, reader: 'EbookReader'):
"""Initialize TOC overlay."""
super().__init__(reader)
self._cached_chapters: List[Tuple[str, int]] = []
def get_overlay_type(self) -> OverlayState:
"""Return TOC overlay type."""
return OverlayState.TOC
def open(self, base_page: Image.Image, **kwargs) -> Image.Image:
"""
Open the TOC overlay.
Args:
base_page: Current reading page to show underneath
chapters: List of (chapter_title, chapter_index) tuples
Returns:
Composited image with TOC overlay
"""
chapters = kwargs.get('chapters', [])
# Store for later use
self._cached_chapters = chapters
# 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 TOC HTML with clickable links
html = generate_toc_overlay(chapter_data, 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 TOC overlay.
Detects:
- Chapter selection (chapter:N)
- Tap outside overlay (closes)
Args:
x, y: Screen coordinates of tap
Returns:
GestureResponse with appropriate action
"""
# Query the overlay to see what was tapped
query_result = self.query_overlay_pixel(x, y)
# If query failed (tap outside overlay), close it
if not query_result:
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
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
# Not a chapter link, close overlay
return GestureResponse(ActionType.OVERLAY_CLOSED, {})