persiitent styles
All checks were successful
Python CI / test (push) Successful in 4m9s

This commit is contained in:
Duncan Tourolle 2025-11-08 19:31:05 +01:00
parent 11dc30ba8d
commit 4811367905
6 changed files with 661 additions and 1 deletions

View File

@ -706,6 +706,64 @@ class EbookReader:
'font_scale': self.base_font_scale
}
# ===== Settings Persistence =====
def get_current_settings(self) -> Dict[str, Any]:
"""
Get current rendering settings.
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
}
def apply_settings(self, settings: Dict[str, Any]) -> bool:
"""
Apply rendering settings from a settings dictionary.
This should be called after loading a book to restore user preferences.
Args:
settings: Dictionary with settings (font_scale, line_spacing, etc.)
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
# ===== Gesture Handling =====
# All business logic for touch input is handled here
@ -1066,6 +1124,16 @@ class EbookReader:
"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
self.close_overlay()
return GestureResponse(ActionType.OVERLAY_CLOSED, {})

View File

@ -126,3 +126,4 @@ class ActionType:
OVERLAY_CLOSED = "overlay_closed"
CHAPTER_SELECTED = "chapter_selected"
SETTING_CHANGED = "setting_changed"
BACK_TO_LIBRARY = "back_to_library"

View File

@ -278,6 +278,12 @@ def generate_settings_overlay(
</p>
</div>
<div style="margin: 20px 0;">
<p style="padding: 15px; margin: 5px 0; background-color: #dc3545; text-align: center; border-radius: 5px;">
<a href="action:back_to_library" style="text-decoration: none; color: white; font-weight: bold; font-size: 14px;"> Back to Library</a>
</p>
</div>
<p style="text-align: center; margin: 15px 0 0 0; padding-top: 12px;
border-top: 2px solid #ccc; color: #888; font-size: 11px;">
Changes apply in real-time Tap outside to close

View File

@ -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

View File

@ -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()

View File

@ -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()