Test appplication for offdevice testing
This commit is contained in:
+428
@@ -0,0 +1,428 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user