+61
-216
@@ -54,6 +54,7 @@ from .state import OverlayState
|
||||
from .overlay import OverlayManager
|
||||
from .managers import DocumentManager, SettingsManager, HighlightCoordinator
|
||||
from .handlers import GestureRouter
|
||||
from .overlays import NavigationOverlay, SettingsOverlay, TOCOverlay
|
||||
|
||||
|
||||
class EbookReader:
|
||||
@@ -129,9 +130,17 @@ class EbookReader:
|
||||
self.base_font_scale = 1.0
|
||||
self.font_scale_step = 0.1
|
||||
|
||||
# Overlay management
|
||||
# Overlay management (legacy - kept for backward compatibility)
|
||||
self.overlay_manager = OverlayManager(page_size=page_size)
|
||||
self.current_overlay_state = OverlayState.NONE
|
||||
|
||||
# Overlay sub-applications (NEW architecture)
|
||||
self._overlay_subapps = {
|
||||
OverlayState.NAVIGATION: NavigationOverlay(self),
|
||||
OverlayState.SETTINGS: SettingsOverlay(self),
|
||||
OverlayState.TOC: TOCOverlay(self),
|
||||
}
|
||||
self._active_overlay = None # Current active overlay sub-application
|
||||
|
||||
def load_epub(self, epub_path: str) -> bool:
|
||||
"""
|
||||
@@ -250,13 +259,13 @@ class EbookReader:
|
||||
return None
|
||||
|
||||
# If an overlay is open, return the cached composited overlay image
|
||||
if self.is_overlay_open() and self.overlay_manager._cached_base_page:
|
||||
# Return the last composited overlay image
|
||||
# The overlay manager keeps this updated when settings change
|
||||
return self.overlay_manager.composite_overlay(
|
||||
self.overlay_manager._cached_base_page,
|
||||
self.overlay_manager._cached_overlay_image
|
||||
)
|
||||
if self.is_overlay_open() and self._active_overlay:
|
||||
# Return the composited overlay from the sub-application
|
||||
if self._active_overlay._cached_base_page and self._active_overlay._cached_overlay_image:
|
||||
return self._active_overlay.composite_overlay(
|
||||
self._active_overlay._cached_base_page,
|
||||
self._active_overlay._cached_overlay_image
|
||||
)
|
||||
|
||||
try:
|
||||
page = self.manager.get_current_page()
|
||||
@@ -702,211 +711,26 @@ class EbookReader:
|
||||
|
||||
|
||||
def _handle_overlay_tap(self, x: int, y: int) -> GestureResponse:
|
||||
"""Handle tap when overlay is open - select chapter, adjust settings, 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)
|
||||
"""
|
||||
Handle tap when overlay is open.
|
||||
|
||||
# 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
|
||||
Delegates to the active overlay sub-application for handling.
|
||||
If the response indicates the overlay should be closed, closes it.
|
||||
"""
|
||||
if not self._active_overlay:
|
||||
# No active overlay, close legacy overlay if any
|
||||
self.close_overlay()
|
||||
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
|
||||
|
||||
# For settings overlay, handle setting adjustments
|
||||
elif self.current_overlay_state == OverlayState.SETTINGS:
|
||||
# Query the overlay to see what was tapped
|
||||
query_result = self.overlay_manager.query_overlay_pixel(x, y)
|
||||
# Delegate to the active overlay sub-application
|
||||
response = self._active_overlay.handle_tap(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 settings control link
|
||||
if query_result.get("is_interactive") and query_result.get("link_target"):
|
||||
link_target = query_result["link_target"]
|
||||
|
||||
# Parse "setting:action" format
|
||||
if link_target.startswith("setting:"):
|
||||
action = link_target.split(":", 1)[1]
|
||||
|
||||
# Apply the setting change
|
||||
if action == "font_increase":
|
||||
self.increase_font_size()
|
||||
elif action == "font_decrease":
|
||||
self.decrease_font_size()
|
||||
elif action == "line_spacing_increase":
|
||||
new_spacing = self.page_style.line_spacing + 2
|
||||
self.set_line_spacing(new_spacing)
|
||||
elif action == "line_spacing_decrease":
|
||||
new_spacing = max(0, self.page_style.line_spacing - 2)
|
||||
self.set_line_spacing(new_spacing)
|
||||
elif action == "block_spacing_increase":
|
||||
new_spacing = self.page_style.inter_block_spacing + 3
|
||||
self.set_inter_block_spacing(new_spacing)
|
||||
elif action == "block_spacing_decrease":
|
||||
new_spacing = max(0, self.page_style.inter_block_spacing - 3)
|
||||
self.set_inter_block_spacing(new_spacing)
|
||||
elif action == "word_spacing_increase":
|
||||
new_spacing = self.page_style.word_spacing + 2
|
||||
self.set_word_spacing(new_spacing)
|
||||
elif action == "word_spacing_decrease":
|
||||
new_spacing = max(0, self.page_style.word_spacing - 2)
|
||||
self.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.manager.get_current_page()
|
||||
updated_page = page.render()
|
||||
|
||||
# Refresh the settings overlay with updated values and page
|
||||
self.overlay_manager.refresh_settings_overlay(
|
||||
updated_base_page=updated_page,
|
||||
font_scale=self.base_font_scale,
|
||||
line_spacing=self.page_style.line_spacing,
|
||||
inter_block_spacing=self.page_style.inter_block_spacing,
|
||||
word_spacing=self.page_style.word_spacing
|
||||
)
|
||||
|
||||
return GestureResponse(ActionType.SETTING_CHANGED, {
|
||||
"action": action,
|
||||
"font_scale": self.base_font_scale,
|
||||
"line_spacing": self.page_style.line_spacing,
|
||||
"inter_block_spacing": self.page_style.inter_block_spacing,
|
||||
"word_spacing": self.page_style.word_spacing
|
||||
})
|
||||
|
||||
# Parse "action:command" format for other actions
|
||||
elif link_target.startswith("action:"):
|
||||
action = link_target.split(":", 1)[1]
|
||||
|
||||
if action == "back_to_library":
|
||||
# Close the overlay first
|
||||
self.close_overlay()
|
||||
# Return a special action for the application to handle
|
||||
return GestureResponse(ActionType.BACK_TO_LIBRARY, {})
|
||||
|
||||
# Not a setting control, close overlay
|
||||
# If the response indicates overlay should be closed, close it
|
||||
if response.action in (ActionType.OVERLAY_CLOSED, ActionType.CHAPTER_SELECTED,
|
||||
ActionType.BOOKMARK_SELECTED):
|
||||
self.close_overlay()
|
||||
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
|
||||
|
||||
# For navigation overlay, handle tab switching, chapter/bookmark selection, and close
|
||||
elif self.current_overlay_state == OverlayState.NAVIGATION:
|
||||
# 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
|
||||
if query_result.get("is_interactive") and query_result.get("link_target"):
|
||||
link_target = query_result["link_target"]
|
||||
|
||||
# Parse "tab:tabname" format for tab switching
|
||||
if link_target.startswith("tab:"):
|
||||
tab_name = link_target.split(":", 1)[1]
|
||||
# Switch to the selected tab
|
||||
self.switch_navigation_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
|
||||
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
|
||||
|
||||
# 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.load_position(bookmark_name)
|
||||
if page:
|
||||
# Close overlay
|
||||
self.close_overlay()
|
||||
|
||||
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":
|
||||
self.close_overlay()
|
||||
return GestureResponse(ActionType.OVERLAY_CLOSED, {})
|
||||
|
||||
# Not an interactive element, 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, {})
|
||||
return response
|
||||
|
||||
|
||||
# ===================================================================
|
||||
@@ -1139,8 +963,12 @@ class EbookReader:
|
||||
# Get chapters
|
||||
chapters = self.get_chapters()
|
||||
|
||||
# Open overlay and get composited image
|
||||
result = self.overlay_manager.open_toc_overlay(chapters, base_page)
|
||||
# Use the TOC sub-application
|
||||
overlay_subapp = self._overlay_subapps[OverlayState.TOC]
|
||||
result = overlay_subapp.open(base_page, chapters=chapters)
|
||||
|
||||
# Update state
|
||||
self._active_overlay = overlay_subapp
|
||||
self.current_overlay_state = OverlayState.TOC
|
||||
|
||||
return result
|
||||
@@ -1166,14 +994,18 @@ class EbookReader:
|
||||
inter_block_spacing = self.page_style.inter_block_spacing
|
||||
word_spacing = self.page_style.word_spacing
|
||||
|
||||
# Open overlay and get composited image
|
||||
result = self.overlay_manager.open_settings_overlay(
|
||||
# Use the Settings sub-application
|
||||
overlay_subapp = self._overlay_subapps[OverlayState.SETTINGS]
|
||||
result = overlay_subapp.open(
|
||||
base_page,
|
||||
font_scale=font_scale,
|
||||
line_spacing=line_spacing,
|
||||
inter_block_spacing=inter_block_spacing,
|
||||
word_spacing=word_spacing
|
||||
)
|
||||
|
||||
# Update state
|
||||
self._active_overlay = overlay_subapp
|
||||
self.current_overlay_state = OverlayState.SETTINGS
|
||||
|
||||
return result
|
||||
@@ -1237,13 +1069,17 @@ class EbookReader:
|
||||
for name in bookmark_names
|
||||
]
|
||||
|
||||
# Open overlay and get composited image
|
||||
result = self.overlay_manager.open_navigation_overlay(
|
||||
# Use the Navigation sub-application
|
||||
overlay_subapp = self._overlay_subapps[OverlayState.NAVIGATION]
|
||||
result = overlay_subapp.open(
|
||||
base_page,
|
||||
chapters=chapters,
|
||||
bookmarks=bookmarks,
|
||||
base_page=base_page,
|
||||
active_tab=active_tab
|
||||
)
|
||||
|
||||
# Update state
|
||||
self._active_overlay = overlay_subapp
|
||||
self.current_overlay_state = OverlayState.NAVIGATION
|
||||
|
||||
return result
|
||||
@@ -1261,8 +1097,12 @@ class EbookReader:
|
||||
if self.current_overlay_state != OverlayState.NAVIGATION:
|
||||
return None
|
||||
|
||||
result = self.overlay_manager.switch_navigation_tab(new_tab)
|
||||
return result if result else self.get_current_page()
|
||||
# Delegate to the Navigation sub-application
|
||||
if isinstance(self._active_overlay, NavigationOverlay):
|
||||
result = self._active_overlay.switch_tab(new_tab)
|
||||
return result if result else self.get_current_page()
|
||||
|
||||
return None
|
||||
|
||||
def close_overlay(self) -> Optional[Image.Image]:
|
||||
"""
|
||||
@@ -1274,7 +1114,12 @@ class EbookReader:
|
||||
if self.current_overlay_state == OverlayState.NONE:
|
||||
return None
|
||||
|
||||
result = self.overlay_manager.close_overlay()
|
||||
# Close the active overlay sub-application
|
||||
if self._active_overlay:
|
||||
self._active_overlay.close()
|
||||
self._active_overlay = None
|
||||
|
||||
# Update state
|
||||
self.current_overlay_state = OverlayState.NONE
|
||||
|
||||
# Return fresh current page
|
||||
|
||||
Reference in New Issue
Block a user