refactor applications to delegate responsibilites
Python CI / test (push) Failing after 4m11s

This commit is contained in:
2025-11-08 19:46:49 +01:00
parent 4811367905
commit fe140ba91f
13 changed files with 1468 additions and 435 deletions
+113 -430
View File
@@ -41,19 +41,19 @@ import os
from PIL import Image
from pyWebLayout.io.readers.epub_reader import read_epub
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
from pyWebLayout.style.page_style import PageStyle
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 pyWebLayout.core.highlight import Highlight, HighlightColor
from .gesture import TouchEvent, GestureType, GestureResponse, ActionType
from .state import OverlayState
from .overlay import OverlayManager
from .managers import DocumentManager, SettingsManager, HighlightCoordinator
from .handlers import GestureRouter
class EbookReader:
@@ -109,22 +109,25 @@ class EbookReader:
inter_block_spacing=inter_block_spacing
)
# State
# Core managers (NEW: Refactored into separate modules)
self.doc_manager = DocumentManager()
self.settings_manager = SettingsManager()
self.highlight_coordinator: Optional[HighlightCoordinator] = None
self.gesture_router = GestureRouter(self)
# Layout manager (initialized after loading)
self.manager: Optional[EreaderLayoutManager] = None
# Legacy compatibility properties
self.blocks: Optional[List[Block]] = None
self.document_id: Optional[str] = None
self.book_title: Optional[str] = None
self.book_author: Optional[str] = None
self.highlight_manager: Optional[HighlightManager] = None
self.highlight_manager = None # Will delegate to highlight_coordinator
# Font scale state
# Font scale state (delegated to settings_manager but kept for compatibility)
self.base_font_scale = 1.0
self.font_scale_step = 0.1 # 10% change per step
# Selection state (for text selection gestures)
self._selection_start: Optional[Tuple[int, int]] = None
self._selection_end: Optional[Tuple[int, int]] = None
self._selected_range: Optional[SelectionRange] = None
self.font_scale_step = 0.1
# Overlay management
self.overlay_manager = OverlayManager(page_size=page_size)
@@ -133,58 +136,47 @@ class EbookReader:
def load_epub(self, epub_path: str) -> bool:
"""
Load an EPUB file into the reader.
Args:
epub_path: Path to the EPUB file
Returns:
True if loaded successfully, False otherwise
"""
try:
# Validate path
if not os.path.exists(epub_path):
raise FileNotFoundError(f"EPUB file not found: {epub_path}")
# Load the EPUB
book = read_epub(epub_path)
# Extract metadata
self.book_title = book.get_title() or "Unknown Title"
self.book_author = book.get_metadata('AUTHOR') or "Unknown Author"
# Create document ID from filename
self.document_id = Path(epub_path).stem
# Extract all blocks from chapters
self.blocks = []
for chapter in book.chapters:
if hasattr(chapter, '_blocks'):
self.blocks.extend(chapter._blocks)
if not self.blocks:
raise ValueError("No content blocks found in EPUB")
# 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
)
# Use DocumentManager to load the EPUB
success = self.doc_manager.load_epub(epub_path)
# 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 EPUB: {e}")
if not success:
return False
# Set compatibility properties
self.book_title = self.doc_manager.title
self.book_author = self.doc_manager.author
self.document_id = self.doc_manager.document_id
self.blocks = self.doc_manager.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 managers that depend on layout manager
self.settings_manager.set_manager(self.manager)
# Initialize highlight coordinator for this document
self.highlight_coordinator = HighlightCoordinator(
document_id=self.document_id,
highlights_dir=self.highlights_dir
)
self.highlight_coordinator.set_layout_manager(self.manager)
self.highlight_manager = self.highlight_coordinator.highlight_manager # Compatibility
return True
def load_html(self, html_string: str, title: str = "HTML Document", author: str = "Unknown", document_id: str = "html_doc") -> bool:
"""
@@ -202,41 +194,41 @@ class EbookReader:
Returns:
True if loaded successfully, False otherwise
"""
try:
# Parse HTML into blocks
blocks = parse_html_string(html_string)
# Use DocumentManager to load HTML
success = self.doc_manager.load_html(html_string, title, author, document_id)
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}")
if not success:
return False
# Set compatibility properties
self.book_title = self.doc_manager.title
self.book_author = self.doc_manager.author
self.document_id = self.doc_manager.document_id
self.blocks = self.doc_manager.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 managers that depend on layout manager
self.settings_manager.set_manager(self.manager)
# Initialize highlight coordinator for this document
self.highlight_coordinator = HighlightCoordinator(
document_id=self.document_id,
highlights_dir=self.highlights_dir
)
self.highlight_coordinator.set_layout_manager(self.manager)
self.highlight_manager = self.highlight_coordinator.highlight_manager # Compatibility
return True
def is_loaded(self) -> bool:
"""Check if a book is currently loaded."""
return self.manager is not None
@@ -466,52 +458,50 @@ class EbookReader:
def set_font_size(self, scale: float) -> Optional[Image.Image]:
"""
Set the font size scale and re-render current page.
Args:
scale: Font scale factor (1.0 = normal, 2.0 = double size, 0.5 = half size)
Returns:
PIL Image of the re-rendered page with new font size
"""
if not self.manager:
return None
try:
self.base_font_scale = max(0.5, min(3.0, scale)) # Clamp between 0.5x and 3.0x
page = self.manager.set_font_scale(self.base_font_scale)
return page.render()
except Exception as e:
print(f"Error setting font size: {e}")
return None
result = self.settings_manager.set_font_size(scale)
if result:
self.base_font_scale = self.settings_manager.font_scale # Sync compatibility property
return result
def increase_font_size(self) -> Optional[Image.Image]:
"""
Increase font size by one step and re-render.
Returns:
PIL Image of the re-rendered page
"""
new_scale = self.base_font_scale + self.font_scale_step
return self.set_font_size(new_scale)
result = self.settings_manager.increase_font_size()
if result:
self.base_font_scale = self.settings_manager.font_scale
return result
def decrease_font_size(self) -> Optional[Image.Image]:
"""
Decrease font size by one step and re-render.
Returns:
PIL Image of the re-rendered page
"""
new_scale = self.base_font_scale - self.font_scale_step
return self.set_font_size(new_scale)
result = self.settings_manager.decrease_font_size()
if result:
self.base_font_scale = self.settings_manager.font_scale
return result
def get_font_size(self) -> float:
"""
Get the current font size scale.
Returns:
Current font scale factor
"""
return self.base_font_scale
return self.settings_manager.get_font_size()
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
"""
@@ -523,28 +513,8 @@ class EbookReader:
Returns:
PIL Image of the re-rendered page
"""
if not self.manager:
return None
return self.settings_manager.set_line_spacing(spacing)
try:
# Calculate delta from current spacing
current_spacing = self.manager.page_style.line_spacing
target_spacing = max(0, spacing)
delta = target_spacing - current_spacing
# Use pyWebLayout's built-in methods to adjust spacing
if delta > 0:
self.manager.increase_line_spacing(abs(delta))
elif delta < 0:
self.manager.decrease_line_spacing(abs(delta))
# Get re-rendered page
page = self.manager.get_current_page()
return page.render()
except Exception as e:
print(f"Error setting line spacing: {e}")
return None
def set_inter_block_spacing(self, spacing: int) -> Optional[Image.Image]:
"""
Set inter-block spacing using pyWebLayout's native support.
@@ -555,27 +525,7 @@ class EbookReader:
Returns:
PIL Image of the re-rendered page
"""
if not self.manager:
return None
try:
# Calculate delta from current spacing
current_spacing = self.manager.page_style.inter_block_spacing
target_spacing = max(0, spacing)
delta = target_spacing - current_spacing
# Use pyWebLayout's built-in methods to adjust spacing
if delta > 0:
self.manager.increase_inter_block_spacing(abs(delta))
elif delta < 0:
self.manager.decrease_inter_block_spacing(abs(delta))
# Get re-rendered page
page = self.manager.get_current_page()
return page.render()
except Exception as e:
print(f"Error setting inter-block spacing: {e}")
return None
return self.settings_manager.set_inter_block_spacing(spacing)
def set_word_spacing(self, spacing: int) -> Optional[Image.Image]:
"""
@@ -587,27 +537,7 @@ class EbookReader:
Returns:
PIL Image of the re-rendered page
"""
if not self.manager:
return None
try:
# Calculate delta from current spacing
current_spacing = self.manager.page_style.word_spacing
target_spacing = max(0, spacing)
delta = target_spacing - current_spacing
# Use pyWebLayout's built-in methods to adjust spacing
if delta > 0:
self.manager.increase_word_spacing(abs(delta))
elif delta < 0:
self.manager.decrease_word_spacing(abs(delta))
# Get re-rendered page
page = self.manager.get_current_page()
return page.render()
except Exception as e:
print(f"Error setting word spacing: {e}")
return None
return self.settings_manager.set_word_spacing(spacing)
def get_position_info(self) -> Dict[str, Any]:
"""
@@ -715,12 +645,7 @@ class EbookReader:
Returns:
Dictionary with all current settings
"""
return {
'font_scale': self.base_font_scale,
'line_spacing': self.page_style.line_spacing if self.manager else 5,
'inter_block_spacing': self.page_style.inter_block_spacing if self.manager else 15,
'word_spacing': self.page_style.word_spacing if self.manager else 0
}
return self.settings_manager.get_current_settings()
def apply_settings(self, settings: Dict[str, Any]) -> bool:
"""
@@ -734,35 +659,11 @@ class EbookReader:
Returns:
True if settings applied successfully, False otherwise
"""
if not self.manager:
return False
try:
# Apply font scale
font_scale = settings.get('font_scale', 1.0)
if font_scale != self.base_font_scale:
self.set_font_size(font_scale)
# Apply line spacing
line_spacing = settings.get('line_spacing', 5)
if line_spacing != self.page_style.line_spacing:
self.set_line_spacing(line_spacing)
# Apply inter-block spacing
inter_block_spacing = settings.get('inter_block_spacing', 15)
if inter_block_spacing != self.page_style.inter_block_spacing:
self.set_inter_block_spacing(inter_block_spacing)
# Apply word spacing
word_spacing = settings.get('word_spacing', 0)
if word_spacing != self.page_style.word_spacing:
self.set_word_spacing(word_spacing)
return True
except Exception as e:
print(f"Error applying settings: {e}")
return False
success = self.settings_manager.apply_settings(settings)
if success:
# Sync compatibility property
self.base_font_scale = self.settings_manager.font_scale
return success
# ===== Gesture Handling =====
# All business logic for touch input is handled here
@@ -780,44 +681,8 @@ class EbookReader:
Returns:
GestureResponse with action and data for UI to process
"""
if not self.is_loaded():
return GestureResponse(ActionType.ERROR, {"message": "No book loaded"})
# 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:
return self._handle_long_press(event.x, event.y)
elif event.gesture == GestureType.SWIPE_LEFT:
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.SWIPE_DOWN:
# Swipe down from top opens settings overlay
return self._handle_swipe_down(event.y)
elif event.gesture == GestureType.PINCH_IN:
return self._handle_zoom_out()
elif event.gesture == GestureType.PINCH_OUT:
return self._handle_zoom_in()
elif event.gesture == GestureType.DRAG_START:
return self._handle_selection_start(event.x, event.y)
elif event.gesture == GestureType.DRAG_MOVE:
return self._handle_selection_move(event.x, event.y)
elif event.gesture == GestureType.DRAG_END:
return self._handle_selection_end(event.x, event.y)
return GestureResponse(ActionType.NONE, {})
# Delegate to gesture router
return self.gesture_router.handle_touch(event)
def query_pixel(self, x: int, y: int) -> Optional[QueryResult]:
"""
@@ -835,184 +700,6 @@ class EbookReader:
page = self.manager.get_current_page()
return page.query_point((x, y))
def _handle_tap(self, x: int, y: int) -> GestureResponse:
"""Handle tap gesture - activates links or selects words"""
page = self.manager.get_current_page()
result = page.query_point((x, y))
if not result or result.object_type == "empty":
return GestureResponse(ActionType.NONE, {})
# If it's a link, navigate
if result.is_interactive and result.link_target:
# Handle different link types
if result.link_target.endswith('.epub'):
# Open new book
success = self.load_epub(result.link_target)
if success:
return GestureResponse(ActionType.BOOK_LOADED, {
"title": self.book_title,
"author": self.book_author,
"path": result.link_target
})
else:
return GestureResponse(ActionType.ERROR, {
"message": f"Failed to load {result.link_target}"
})
else:
# Internal navigation (chapter)
self.jump_to_chapter(result.link_target)
return GestureResponse(ActionType.NAVIGATE, {
"target": result.link_target,
"chapter": self.get_current_chapter_info()
})
# Just a tap on text - select word
if result.text:
return GestureResponse(ActionType.WORD_SELECTED, {
"word": result.text,
"bounds": result.bounds
})
return GestureResponse(ActionType.NONE, {})
def _handle_long_press(self, x: int, y: int) -> GestureResponse:
"""Handle long-press - show definition or menu"""
page = self.manager.get_current_page()
result = page.query_point((x, y))
if result and result.text:
return GestureResponse(ActionType.DEFINE, {
"word": result.text,
"bounds": result.bounds
})
# Long-press on empty - show menu
return GestureResponse(ActionType.SHOW_MENU, {
"options": ["bookmark", "settings", "toc", "search"]
})
def _handle_page_forward(self) -> GestureResponse:
"""Handle swipe left - next page"""
img = self.next_page()
if img:
return GestureResponse(ActionType.PAGE_TURN, {
"direction": "forward",
"progress": self.get_reading_progress(),
"chapter": self.get_current_chapter_info()
})
return GestureResponse(ActionType.AT_END, {})
def _handle_page_back(self) -> GestureResponse:
"""Handle swipe right - previous page"""
img = self.previous_page()
if img:
return GestureResponse(ActionType.PAGE_TURN, {
"direction": "back",
"progress": self.get_reading_progress(),
"chapter": self.get_current_chapter_info()
})
return GestureResponse(ActionType.AT_START, {})
def _handle_zoom_in(self) -> GestureResponse:
"""Handle pinch out - increase font"""
self.increase_font_size()
return GestureResponse(ActionType.ZOOM, {
"direction": "in",
"font_scale": self.base_font_scale
})
def _handle_zoom_out(self) -> GestureResponse:
"""Handle pinch in - decrease font"""
self.decrease_font_size()
return GestureResponse(ActionType.ZOOM, {
"direction": "out",
"font_scale": self.base_font_scale
})
def _handle_selection_start(self, x: int, y: int) -> GestureResponse:
"""Start text selection"""
self._selection_start = (x, y)
self._selection_end = None
self._selected_range = None
return GestureResponse(ActionType.SELECTION_START, {
"start": (x, y)
})
def _handle_selection_move(self, x: int, y: int) -> GestureResponse:
"""Update text selection"""
if not self._selection_start:
return GestureResponse(ActionType.NONE, {})
self._selection_end = (x, y)
# Query range
page = self.manager.get_current_page()
self._selected_range = page.query_range(
self._selection_start,
self._selection_end
)
return GestureResponse(ActionType.SELECTION_UPDATE, {
"start": self._selection_start,
"end": self._selection_end,
"text_count": len(self._selected_range.results),
"bounds": self._selected_range.bounds_list
})
def _handle_selection_end(self, x: int, y: int) -> GestureResponse:
"""End text selection and return selected text"""
if not self._selection_start:
return GestureResponse(ActionType.NONE, {})
self._selection_end = (x, y)
page = self.manager.get_current_page()
self._selected_range = page.query_range(
self._selection_start,
self._selection_end
)
return GestureResponse(ActionType.SELECTION_COMPLETE, {
"text": self._selected_range.text,
"word_count": len(self._selected_range.results),
"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_swipe_down(self, y: int) -> GestureResponse:
"""Handle swipe down gesture - opens settings overlay if from top of screen"""
# Check if swipe started from top 20% of screen
top_threshold = self.page_size[1] * 0.2
if y <= top_threshold:
# Open settings overlay
overlay_image = self.open_settings_overlay()
if overlay_image:
return GestureResponse(ActionType.OVERLAY_OPENED, {
"overlay_type": "settings",
"font_scale": self.base_font_scale,
"line_spacing": self.page_style.line_spacing,
"inter_block_spacing": self.page_style.inter_block_spacing
})
return GestureResponse(ActionType.NONE, {})
def _handle_overlay_tap(self, x: int, y: int) -> GestureResponse:
"""Handle tap when overlay is open - select chapter, adjust settings, or close overlay"""
@@ -1142,10 +829,6 @@ class EbookReader:
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