""" Main application controller for DReader e-reader application. This module provides the DReaderApplication class which orchestrates: - Library and reading mode transitions - State persistence and recovery - HAL integration for display and input - Event routing and handling The application uses asyncio for non-blocking operations and integrates with a hardware abstraction layer (HAL) for platform independence. """ import asyncio import logging from pathlib import Path from typing import Optional from PIL import Image from .library import LibraryManager from .application import EbookReader from .state import StateManager, EreaderMode, OverlayState, BookState from .gesture import TouchEvent, GestureType, ActionType logger = logging.getLogger(__name__) class AppConfig: """ Configuration for DReaderApplication. Attributes: display_hal: Hardware abstraction layer for display/input library_path: Path to directory containing EPUB files page_size: Tuple of (width, height) for rendered pages bookmarks_dir: Directory for bookmark storage (default: ~/.config/dreader/bookmarks) highlights_dir: Directory for highlights storage (default: ~/.config/dreader/highlights) state_file: Path to state JSON file (default: ~/.config/dreader/state.json) auto_save_interval: Seconds between automatic state saves (default: 60) force_library_mode: If True, always start in library mode (default: False) log_level: Logging level (default: logging.INFO) """ def __init__( self, display_hal, library_path: str, page_size: tuple[int, int] = (800, 1200), bookmarks_dir: Optional[str] = None, highlights_dir: Optional[str] = None, state_file: Optional[str] = None, auto_save_interval: int = 60, force_library_mode: bool = False, log_level: int = logging.INFO ): self.display_hal = display_hal self.library_path = library_path self.page_size = page_size self.force_library_mode = force_library_mode # Set up default config paths config_dir = Path.home() / ".config" / "dreader" config_dir.mkdir(parents=True, exist_ok=True) self.bookmarks_dir = bookmarks_dir or str(config_dir / "bookmarks") self.highlights_dir = highlights_dir or str(config_dir / "highlights") self.state_file = state_file or str(config_dir / "state.json") self.auto_save_interval = auto_save_interval self.log_level = log_level class DReaderApplication: """ Main application controller coordinating library and reading modes. This class orchestrates all major components of the e-reader: - LibraryManager for book browsing - EbookReader for reading books - StateManager for persistence - DisplayHAL for hardware integration Usage: config = AppConfig( display_hal=MyDisplayHAL(), library_path="/path/to/books" ) app = DReaderApplication(config) await app.start() # In event loop: await app.handle_touch(touch_event) await app.shutdown() """ def __init__(self, config: AppConfig): """ Initialize the application with configuration. Args: config: Application configuration """ self.config = config # Set up logging logging.basicConfig(level=config.log_level) logger.info("Initializing DReaderApplication") # State management self.state_manager = StateManager( state_file=config.state_file, auto_save_interval=config.auto_save_interval ) self.state = self.state_manager.load_state() logger.info(f"Loaded state: mode={self.state.mode}, current_book={self.state.current_book}") # Components (lazy-initialized) self.library: Optional[LibraryManager] = None self.reader: Optional[EbookReader] = None # Display abstraction self.display_hal = config.display_hal self.current_image: Optional[Image.Image] = None # Running state self.running = False async def start(self): """ Start the application and display initial screen. This method: 1. Starts automatic state saving 2. Restores previous mode or shows library 3. Displays the initial screen """ logger.info("Starting DReaderApplication") self.running = True # Start auto-save self.state_manager.start_auto_save() logger.info(f"Auto-save started (interval: {self.config.auto_save_interval}s)") # Restore previous mode (or force library mode if configured) force_library = getattr(self.config, 'force_library_mode', False) if force_library: logger.info("Force library mode enabled - starting in library") await self._enter_library_mode() elif self.state.mode == EreaderMode.READING and self.state.current_book: logger.info(f"Resuming reading mode: {self.state.current_book.path}") await self._enter_reading_mode(self.state.current_book.path) else: logger.info("Entering library mode") await self._enter_library_mode() # Display initial screen await self._update_display() logger.info("Application started successfully") async def shutdown(self): """ Gracefully shutdown the application. This method: 1. Saves current reading position 2. Closes active components 3. Stops auto-save and saves final state """ logger.info("Shutting down DReaderApplication") self.running = False # Save current position if reading if self.reader and self.reader.is_loaded(): logger.info("Saving auto-resume position") self.reader.save_position("__auto_resume__") self.reader.close() # Clean up library if self.library: self.library.cleanup() # Stop auto-save and save final state await self.state_manager.stop_auto_save(save_final=True) logger.info("Application shutdown complete") async def handle_touch(self, event: TouchEvent): """ Process touch event based on current mode. Args: event: Touch event from HAL """ logger.info(f"[APP] Received touch event: {event.gesture.value} at ({event.x}, {event.y}), mode={self.state.mode.value}") if self.state.mode == EreaderMode.LIBRARY: logger.info("[APP] Routing to library touch handler") await self._handle_library_touch(event) elif self.state.mode == EreaderMode.READING: logger.info("[APP] Routing to reading touch handler") await self._handle_reading_touch(event) # Update display after handling await self._update_display() async def _enter_library_mode(self): """ Switch to library browsing mode. This method: 1. Saves and closes reader if active 2. Initializes library manager 3. Renders library view 4. Updates state """ logger.info("Entering library mode") # Save and close reader if active if self.reader: if self.reader.is_loaded(): logger.info("Saving reading position before closing") self.reader.save_position("__auto_resume__") self.reader.close() self.reader = None # Initialize library if needed if not self.library: logger.info(f"Initializing library manager: {self.config.library_path}") self.library = LibraryManager( library_path=self.config.library_path, page_size=self.config.page_size, cache_dir=None # Uses default ~/.config/dreader ) # Scan for books (async operation) logger.info("Scanning library for books") books = self.library.scan_library() logger.info(f"Found {len(books)} books") # Render library view logger.info("Rendering library view") self.current_image = self.library.render_library() # Update state self.state_manager.set_mode(EreaderMode.LIBRARY) logger.info("Library mode active") async def _enter_reading_mode(self, book_path: str): """ Switch to reading mode. Args: book_path: Path to EPUB file to open This method: 1. Initializes reader if needed 2. Loads the book 3. Applies saved settings 4. Restores reading position 5. Updates state 6. Renders first/current page """ logger.info(f"Entering reading mode: {book_path}") # Verify book exists if not Path(book_path).exists(): logger.error(f"Book not found: {book_path}") # Return to library await self._enter_library_mode() return # Initialize reader if needed if not self.reader: logger.info("Initializing ebook reader") self.reader = EbookReader( page_size=self.config.page_size, margin=40, background_color=(255, 255, 255), bookmarks_dir=self.config.bookmarks_dir, highlights_dir=self.config.highlights_dir ) # Load book logger.info(f"Loading EPUB: {book_path}") success = self.reader.load_epub(book_path) if not success: logger.error(f"Failed to load EPUB: {book_path}") # Return to library await self._enter_library_mode() return logger.info(f"Loaded: {self.reader.book_title} by {self.reader.book_author}") # Apply saved settings logger.info("Applying saved settings") settings_dict = self.state.settings.to_dict() self.reader.apply_settings(settings_dict) # Restore position logger.info("Restoring reading position") position_loaded = self.reader.load_position("__auto_resume__") if position_loaded: pos_info = self.reader.get_position_info() logger.info(f"Resumed at position: {pos_info}") else: logger.info("No saved position, starting from beginning") # Update state self.state_manager.set_current_book(BookState( path=book_path, title=self.reader.book_title or "Unknown", author=self.reader.book_author or "Unknown" )) self.state_manager.set_mode(EreaderMode.READING) # Render current page logger.info("Rendering current page") self.current_image = self.reader.get_current_page() logger.info("Reading mode active") async def _handle_library_touch(self, event: TouchEvent): """ Handle touch events in library mode. Supports: - TAP: Select a book to read - SWIPE_LEFT: Next page - SWIPE_RIGHT: Previous page Args: event: Touch event """ if event.gesture == GestureType.TAP: logger.debug(f"Library tap at ({event.x}, {event.y})") # Check if a book was selected book_path = self.library.handle_library_tap(event.x, event.y) if book_path: logger.info(f"Book selected: {book_path}") await self._enter_reading_mode(book_path) else: logger.debug("Tap did not hit a book") elif event.gesture == GestureType.SWIPE_LEFT: logger.debug("Library: swipe left (next page)") if self.library.next_page(): logger.info(f"Library: moved to page {self.library.current_page + 1}/{self.library.get_total_pages()}") # Re-render library with new page self.library.create_library_table() self.current_image = self.library.render_library() else: logger.debug("Library: already on last page") elif event.gesture == GestureType.SWIPE_RIGHT: logger.debug("Library: swipe right (previous page)") if self.library.previous_page(): logger.info(f"Library: moved to page {self.library.current_page + 1}/{self.library.get_total_pages()}") # Re-render library with new page self.library.create_library_table() self.current_image = self.library.render_library() else: logger.debug("Library: already on first page") async def _handle_reading_touch(self, event: TouchEvent): """ Handle touch events in reading mode. Args: event: Touch event """ # Delegate to reader's gesture handler logger.info(f"[APP] Calling reader.handle_touch({event.gesture.value})") response = self.reader.handle_touch(event) # response.action is already a string (ActionType enum value), not the enum itself logger.info(f"[APP] Reader response: action={response.action}, data={response.data}") # Handle special actions if response.action == ActionType.BACK_TO_LIBRARY: logger.info("[APP] → Returning to library") await self._enter_library_mode() elif response.action == ActionType.PAGE_TURN: logger.info(f"[APP] → Page turned: {response.data}") self.current_image = self.reader.get_current_page() elif response.action == ActionType.OVERLAY_OPENED: logger.info(f"[APP] → Overlay opened: {self.reader.get_overlay_state()}") self.current_image = self.reader.get_current_page() elif response.action == ActionType.OVERLAY_CLOSED: logger.info("[APP] → Overlay closed") self.current_image = self.reader.get_current_page() elif response.action == ActionType.SETTING_CHANGED: logger.info(f"[APP] → Setting changed: {response.data}") # Update state with new settings settings = self.reader.get_current_settings() self.state_manager.update_settings(settings) self.current_image = self.reader.get_current_page() elif response.action == ActionType.CHAPTER_SELECTED: logger.info(f"[APP] → Chapter selected: {response.data}") self.current_image = self.reader.get_current_page() elif response.action == ActionType.BOOKMARK_SELECTED: logger.info(f"[APP] → Bookmark selected: {response.data}") self.current_image = self.reader.get_current_page() elif response.action == ActionType.NAVIGATE: logger.debug("Navigation action") self.current_image = self.reader.get_current_page() elif response.action == ActionType.ZOOM: logger.info(f"Zoom action: {response.data}") # Font size changed settings = self.reader.get_current_settings() self.state_manager.update_settings(settings) self.current_image = self.reader.get_current_page() elif response.action == ActionType.ERROR: logger.error(f"Error: {response.data}") async def _update_display(self): """ Update the display with current image. This method sends the current image to the HAL for display. """ if self.current_image: logger.debug(f"Updating display: {self.current_image.size}") await self.display_hal.show_image(self.current_image) else: logger.warning("No image to display") def get_current_mode(self) -> EreaderMode: """Get current application mode.""" return self.state.mode def get_overlay_state(self) -> OverlayState: """Get current overlay state (only valid in reading mode).""" if self.reader: return self.reader.get_overlay_state() return OverlayState.NONE def is_running(self) -> bool: """Check if application is running.""" return self.running