From 481136790566ba95a70f80d1e42497614abc4204 Mon Sep 17 00:00:00 2001
From: Duncan Tourolle
Changes apply in real-time • Tap outside to close diff --git a/dreader/state.py b/dreader/state.py index 777a20d..eb9c12d 100644 --- a/dreader/state.py +++ b/dreader/state.py @@ -78,10 +78,11 @@ class LibraryState: @dataclass class Settings: - """User settings""" + """User settings for rendering and display""" font_scale: float = 1.0 line_spacing: int = 5 inter_block_spacing: int = 15 + word_spacing: int = 0 # Default word spacing brightness: int = 8 theme: str = "day" @@ -96,6 +97,7 @@ class Settings: font_scale=data.get('font_scale', 1.0), line_spacing=data.get('line_spacing', 5), inter_block_spacing=data.get('inter_block_spacing', 15), + word_spacing=data.get('word_spacing', 0), brightness=data.get('brightness', 8), theme=data.get('theme', 'day') ) @@ -374,6 +376,18 @@ class StateManager: setattr(self.state.settings, key, value) self._dirty = True + def update_settings(self, settings_dict: Dict[str, Any]): + """ + Update multiple settings at once. + + Args: + settings_dict: Dictionary with setting keys and values + """ + for key, value in settings_dict.items(): + if hasattr(self.state.settings, key): + setattr(self.state.settings, key, value) + self._dirty = True + def get_library_state(self) -> LibraryState: """Get library state""" return self.state.library diff --git a/examples/library_reading_integration.py b/examples/library_reading_integration.py new file mode 100755 index 0000000..4759439 --- /dev/null +++ b/examples/library_reading_integration.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +""" +Integration demo: Library → Reading → Settings → Back to Library + +This example demonstrates the complete LIBRARY ↔ READING mode transition workflow: +1. Display a library of EPUB files +2. Select a book by clicking/tapping +3. Open and read the selected book +4. Access settings overlay +5. Return to library from the settings overlay +6. Select another book + +This demonstrates the full user flow for an e-reader application. + +Usage: + python library_reading_integration.py path/to/library/directory +""" + +import sys +import os +from pathlib import Path + +# Add parent directory to path to import dreader +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from dreader.library import LibraryManager +from dreader.application import EbookReader +from dreader.gesture import TouchEvent, GestureType, ActionType + + +def print_separator(): + """Print a visual separator.""" + print("\n" + "="*70 + "\n") + + +def simulate_mode_transition_workflow(library_path: str): + """ + Simulate the complete workflow of library browsing and book reading. + + Args: + library_path: Path to directory containing EPUB files + """ + print_separator() + print("INTEGRATION TEST: LIBRARY ↔ READING MODE TRANSITIONS") + print_separator() + + # =================================================================== + # STEP 1: LIBRARY MODE - Display available books + # =================================================================== + print("STEP 1: LIBRARY MODE - Displaying available books") + print("-" * 70) + + # Initialize library manager + library = LibraryManager( + library_path=library_path, + page_size=(800, 1200) + ) + + # Scan for books + books = library.scan_library() + print(f"✓ Found {len(books)} books in library") + + if len(books) == 0: + print("Error: No EPUB files found in library directory") + print(f"Please add some .epub files to: {library_path}") + sys.exit(1) + + # Display book list + for i, book in enumerate(books): + print(f" [{i}] {book['title']} by {book['author']}") + + # Render library view + print("\nRendering library view...") + library_image = library.render_library() + library_image.save("integration_01_library.png") + print("✓ Saved library view to: integration_01_library.png") + + # =================================================================== + # STEP 2: SIMULATE BOOK SELECTION - User taps on first book + # =================================================================== + print_separator() + print("STEP 2: BOOK SELECTION - Simulating tap on first book") + print("-" * 70) + + # Simulate a tap on the first book row + # Row positions depend on rendering, but first book is typically near top + # We'll tap in the middle of the first book row area + tap_x, tap_y = 400, 150 # Approximate center of first book row + + print(f"Simulating tap at ({tap_x}, {tap_y})...") + selected_book_path = library.handle_library_tap(tap_x, tap_y) + + if not selected_book_path: + print("Warning: Tap didn't hit a book. Selecting first book directly...") + selected_book_path = books[0]['path'] + + print(f"✓ Selected book: {selected_book_path}") + + # =================================================================== + # STEP 3: READING MODE - Open the selected book + # =================================================================== + print_separator() + print("STEP 3: READING MODE - Opening selected book") + print("-" * 70) + + # Create reader + reader = EbookReader( + page_size=(800, 1200), + margin=40, + background_color=(255, 255, 255) + ) + + # Load the EPUB + print(f"Loading: {selected_book_path}") + success = reader.load_epub(selected_book_path) + + if not success: + print("Error: Failed to load EPUB") + sys.exit(1) + + print("✓ Book loaded successfully") + + # Get book info + book_info = reader.get_book_info() + print(f" Title: {book_info['title']}") + print(f" Author: {book_info['author']}") + print(f" Chapters: {book_info['total_chapters']}") + + # Render first page + print("\nRendering first page...") + page_image = reader.get_current_page() + page_image.save("integration_02_reading_page1.png") + print("✓ Saved first page to: integration_02_reading_page1.png") + + # =================================================================== + # STEP 4: PAGE NAVIGATION - Turn some pages + # =================================================================== + print_separator() + print("STEP 4: PAGE NAVIGATION - Simulating page turns") + print("-" * 70) + + # Simulate swipe left (next page) + print("Simulating SWIPE_LEFT (next page)...") + touch_event = TouchEvent(GestureType.SWIPE_LEFT, 600, 600) + response = reader.handle_touch(touch_event) + + if response.action == ActionType.PAGE_TURN: + print(f"✓ Page turned: {response.data}") + page_image = reader.get_current_page() + page_image.save("integration_03_reading_page2.png") + print(" Saved to: integration_03_reading_page2.png") + + # Turn another page + print("\nSimulating another SWIPE_LEFT...") + touch_event = TouchEvent(GestureType.SWIPE_LEFT, 600, 600) + response = reader.handle_touch(touch_event) + + if response.action == ActionType.PAGE_TURN: + print(f"✓ Page turned: {response.data}") + + # =================================================================== + # STEP 5: SETTINGS OVERLAY - Open and adjust settings + # =================================================================== + print_separator() + print("STEP 5: SETTINGS OVERLAY - Opening settings") + print("-" * 70) + + # Open settings overlay + print("Opening settings overlay...") + overlay_image = reader.open_settings_overlay() + + if overlay_image: + overlay_image.save("integration_04_settings_overlay.png") + print("✓ Settings overlay opened") + print(" Saved to: integration_04_settings_overlay.png") + + # Simulate tapping "Increase Font Size" button + print("\nSimulating tap on 'Increase Font Size'...") + # The increase button is typically around y=250-280 in the overlay + tap_x, tap_y = 400, 270 + touch_event = TouchEvent(GestureType.TAP, tap_x, tap_y) + response = reader.handle_touch(touch_event) + + if response.action == ActionType.SETTING_CHANGED: + print(f"✓ Setting changed: {response.data}") + updated_overlay = reader.get_current_page() + updated_overlay.save("integration_05_settings_font_increased.png") + print(" Saved updated overlay to: integration_05_settings_font_increased.png") + + # =================================================================== + # STEP 6: BACK TO LIBRARY - Use the new "Back to Library" button + # =================================================================== + print_separator() + print("STEP 6: BACK TO LIBRARY - Using 'Back to Library' button") + print("-" * 70) + + # The settings overlay is 60% width x 70% height, centered + # For 800x1200: panel is 480x840, offset at (160, 180) + # The "Back to Library" button is near the bottom of the overlay panel + # Let's try scanning for it by querying multiple y-positions + + print("Scanning for 'Back to Library' button...") + found_button = False + + # Scan a wider range with finer granularity + # Settings overlay is 60% x 70% of 800x1200 = 480x840, centered at (160, 180) + # So overlay goes from y=180 to y=1020 + # Button should be near bottom, scan from y=600 to y=1020 + debug_results = [] + for test_y in range(600, 1021, 20): + test_x = 400 # Center of screen + + # Use the overlay manager's query method if there's an overlay open + if hasattr(reader, 'overlay_manager'): + result = reader.overlay_manager.query_overlay_pixel(test_x, test_y) + + if result: + debug_results.append((test_y, result.get("link_target"), result.get("text", "")[:30])) + + if result.get("is_interactive") and result.get("link_target"): + link = result["link_target"] + if link == "action:back_to_library": + print(f"✓ Found button at approximately ({test_x}, {test_y})") + tap_x, tap_y = test_x, test_y + found_button = True + break + + if not found_button and debug_results: + print(f" Debug: Scanned {len(debug_results)} positions, found these links:") + for y, link, text in debug_results[-5:]: # Show last 5 + if link: + print(f" y={y}: link={link}, text='{text}'") + + if not found_button: + print(" Button not found via scan, using estimated position...") + # Fallback: overlay height is 840, centered at y=180 + # Button is near bottom, approximately at panel_y + panel_height - 100 + tap_x, tap_y = 400, 900 + + print(f"Simulating tap at ({tap_x}, {tap_y})...") + touch_event = TouchEvent(GestureType.TAP, tap_x, tap_y) + response = reader.handle_touch(touch_event) + + if response.action == ActionType.BACK_TO_LIBRARY: + print("✓ BACK_TO_LIBRARY action received!") + print(" Application would now:") + print(" 1. Close the current book") + print(" 2. Return to library view") + print(" 3. Save reading position for resume") + + # Save current position for resume + reader.save_position("__auto_resume__") + print("\n ✓ Auto-resume position saved") + + # Close the reader + reader.close() + print(" ✓ Book closed") + + # Re-render library + print("\n Re-rendering library view...") + library_image = library.render_library() + library_image.save("integration_06_back_to_library.png") + print(" ✓ Saved library view to: integration_06_back_to_library.png") + else: + print(f"Unexpected response: {response.action}") + print("Note: The button might be outside the overlay area or coordinates need adjustment") + + # =================================================================== + # STEP 7: SELECT ANOTHER BOOK (if multiple books available) + # =================================================================== + if len(books) > 1: + print_separator() + print("STEP 7: SELECTING ANOTHER BOOK") + print("-" * 70) + + # Select second book + second_book_path = books[1]['path'] + print(f"Selecting second book: {second_book_path}") + + # Create new reader instance + reader2 = EbookReader( + page_size=(800, 1200), + margin=40, + background_color=(255, 255, 255) + ) + + # Load second book + success = reader2.load_epub(second_book_path) + + if success: + book_info = reader2.get_book_info() + print(f"✓ Loaded: {book_info['title']} by {book_info['author']}") + + # Render first page + page_image = reader2.get_current_page() + page_image.save("integration_07_second_book.png") + print(" Saved to: integration_07_second_book.png") + + reader2.close() + + # =================================================================== + # STEP 8: RESUME PREVIOUS BOOK (demonstrate auto-resume) + # =================================================================== + print_separator() + print("STEP 8: AUTO-RESUME - Reopening first book at saved position") + print("-" * 70) + + # Create new reader + reader3 = EbookReader( + page_size=(800, 1200), + margin=40, + background_color=(255, 255, 255) + ) + + # Load the book + print(f"Reloading: {selected_book_path}") + success = reader3.load_epub(selected_book_path) + + if success: + # Load auto-resume position + print("Loading auto-resume position...") + page = reader3.load_position("__auto_resume__") + + if page: + print("✓ Resumed at saved position!") + pos_info = reader3.get_position_info() + print(f" Progress: {pos_info['progress']*100:.1f}%") + + page.save("integration_08_resumed_position.png") + print(" Saved to: integration_08_resumed_position.png") + else: + print("No saved position found (started from beginning)") + + reader3.close() + + # Cleanup + library.cleanup() + + print_separator() + print("✓ INTEGRATION TEST COMPLETE!") + print_separator() + print("\nGenerated demonstration images:") + demo_files = [ + "integration_01_library.png", + "integration_02_reading_page1.png", + "integration_03_reading_page2.png", + "integration_04_settings_overlay.png", + "integration_05_settings_font_increased.png", + "integration_06_back_to_library.png", + "integration_07_second_book.png", + "integration_08_resumed_position.png" + ] + + for filename in demo_files: + if os.path.exists(filename): + print(f" ✓ {filename}") + + print("\nThis demonstrates the complete workflow:") + print(" 1. Library view with book selection") + print(" 2. Opening and reading a book") + print(" 3. Page navigation") + print(" 4. Settings overlay with adjustments") + print(" 5. Back to library transition") + print(" 6. Selecting another book") + print(" 7. Auto-resume functionality") + + +def main(): + """Main entry point.""" + if len(sys.argv) < 2: + print("Usage: python library_reading_integration.py path/to/library/directory") + print("\nThis demo requires a directory containing EPUB files.") + print("\nExample:") + print(" mkdir my_library") + print(" cp tests/data/test.epub my_library/") + print(" cp tests/data/test2.epub my_library/") + print(" python library_reading_integration.py my_library/") + sys.exit(1) + + library_path = sys.argv[1] + + if not os.path.exists(library_path): + print(f"Error: Directory not found: {library_path}") + sys.exit(1) + + if not os.path.isdir(library_path): + print(f"Error: Not a directory: {library_path}") + sys.exit(1) + + try: + simulate_mode_transition_workflow(library_path) + except Exception as e: + print(f"\nError during integration test: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/examples/persistent_settings_example.py b/examples/persistent_settings_example.py new file mode 100755 index 0000000..058654f --- /dev/null +++ b/examples/persistent_settings_example.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Example demonstrating persistent rendering settings. + +This shows how to: +1. Initialize StateManager to load saved settings +2. Apply saved settings to EbookReader +3. Modify settings during reading +4. Save settings automatically for next session + +The settings (font size, line spacing, etc.) will persist between +application sessions, so the user doesn't have to reconfigure each time. +""" + +import sys +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from dreader import EbookReader +from dreader.state import StateManager, Settings + + +def demonstrate_persistent_settings(): + """Show how settings persist across sessions""" + + print("=" * 70) + print("Persistent Settings Example") + print("=" * 70) + + # 1. Initialize state manager (loads saved state from disk) + state_file = Path.home() / ".config" / "dreader" / "state.json" + state_file.parent.mkdir(parents=True, exist_ok=True) + + state_manager = StateManager(state_file=state_file) + state = state_manager.load_state() + + print(f"\nLoaded settings from: {state_file}") + print(f" Font scale: {state.settings.font_scale}") + print(f" Line spacing: {state.settings.line_spacing}px") + print(f" Inter-block spacing: {state.settings.inter_block_spacing}px") + print(f" Word spacing: {state.settings.word_spacing}px") + + # 2. Create reader with saved settings + reader = EbookReader( + page_size=(800, 1000), + line_spacing=state.settings.line_spacing, + inter_block_spacing=state.settings.inter_block_spacing + ) + + # Load a book + epub_dir = Path(__file__).parent.parent / 'tests' / 'data' / 'library-epub' + epubs = list(epub_dir.glob('*.epub')) + + if not epubs: + print("\nError: No test EPUB files found!") + print(f"Looked in: {epub_dir}") + return + + epub_path = epubs[0] + print(f"\nLoading book: {epub_path.name}") + + if not reader.load_epub(str(epub_path)): + print("Failed to load book!") + return + + print(f"Loaded: {reader.book_title} by {reader.book_author}") + + # 3. Apply saved settings to the book + print("\nApplying saved settings to book...") + settings_dict = state.settings.to_dict() + reader.apply_settings(settings_dict) + + # Render initial page + print("\nRendering page with saved settings...") + page = reader.get_current_page() + reader.render_to_file("persistent_settings_before.png") + print("✓ Saved: persistent_settings_before.png") + + # 4. Simulate user changing settings + print("\n" + "=" * 70) + print("User adjusts settings...") + print("=" * 70) + + # Increase font size + print("\n1. Increasing font size...") + reader.increase_font_size() + reader.increase_font_size() + print(f" New font scale: {reader.base_font_scale}") + + # Increase line spacing + print("2. Increasing line spacing...") + new_line_spacing = state.settings.line_spacing + 4 + reader.set_line_spacing(new_line_spacing) + print(f" New line spacing: {new_line_spacing}px") + + # Increase word spacing + print("3. Increasing word spacing...") + new_word_spacing = state.settings.word_spacing + 3 + reader.set_word_spacing(new_word_spacing) + print(f" New word spacing: {new_word_spacing}px") + + # Render page with new settings + print("\nRendering page with new settings...") + page = reader.get_current_page() + reader.render_to_file("persistent_settings_after.png") + print("✓ Saved: persistent_settings_after.png") + + # 5. Save new settings to state + print("\n" + "=" * 70) + print("Saving settings for next session...") + print("=" * 70) + + current_settings = reader.get_current_settings() + state_manager.update_settings(current_settings) + + print(f"\nSettings to be saved:") + print(f" Font scale: {current_settings['font_scale']}") + print(f" Line spacing: {current_settings['line_spacing']}px") + print(f" Inter-block spacing: {current_settings['inter_block_spacing']}px") + print(f" Word spacing: {current_settings['word_spacing']}px") + + # Save state to disk + if state_manager.save_state(): + print(f"\n✓ Settings saved to: {state_file}") + print(" These settings will be used the next time you open a book!") + else: + print("\n✗ Failed to save settings") + + # 6. Demonstrate that settings are saved + print("\n" + "=" * 70) + print("Verification: Reloading state from disk...") + print("=" * 70) + + # Create new state manager to verify persistence + verification_manager = StateManager(state_file=state_file) + verification_state = verification_manager.load_state() + + print(f"\nVerified saved settings:") + print(f" Font scale: {verification_state.settings.font_scale}") + print(f" Line spacing: {verification_state.settings.line_spacing}px") + print(f" Inter-block spacing: {verification_state.settings.inter_block_spacing}px") + print(f" Word spacing: {verification_state.settings.word_spacing}px") + + if (verification_state.settings.font_scale == current_settings['font_scale'] and + verification_state.settings.line_spacing == current_settings['line_spacing'] and + verification_state.settings.word_spacing == current_settings['word_spacing']): + print("\n✓ Settings successfully persisted!") + else: + print("\n✗ Settings mismatch!") + + # Cleanup + reader.close() + + print("\n" + "=" * 70) + print("Demo Complete!") + print("=" * 70) + print("\nKey Points:") + print(" • Settings are automatically loaded from ~/.config/dreader/state.json") + print(" • Use reader.apply_settings() to apply saved settings after loading a book") + print(" • Use reader.get_current_settings() to get current settings") + print(" • Use state_manager.update_settings() to save new settings") + print(" • Settings persist across application restarts") + print("\nGenerated files:") + print(" • persistent_settings_before.png - Page with original settings") + print(" • persistent_settings_after.png - Page with modified settings") + + +if __name__ == '__main__': + demonstrate_persistent_settings()