Updated demo and fixed bug in spacing
All checks were successful
Python CI / test (push) Successful in 4m10s
All checks were successful
Python CI / test (push) Successful in 4m10s
This commit is contained in:
parent
284a6e3393
commit
1993b0bf48
BIN
docs/images/settings_overlay_demo.gif
Normal file
BIN
docs/images/settings_overlay_demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@ -245,15 +245,27 @@ class EbookReader:
|
||||
"""
|
||||
Get the current page as a PIL Image.
|
||||
|
||||
If an overlay is currently open, returns the composited overlay image.
|
||||
Otherwise returns the base reading page.
|
||||
|
||||
Args:
|
||||
include_highlights: Whether to overlay highlights on the page
|
||||
include_highlights: Whether to overlay highlights on the page (only applies to base page)
|
||||
|
||||
Returns:
|
||||
PIL Image of the current page, or None if no book is loaded
|
||||
PIL Image of the current page (or overlay), or None if no book is loaded
|
||||
"""
|
||||
if not self.manager:
|
||||
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
|
||||
)
|
||||
|
||||
try:
|
||||
page = self.manager.get_current_page()
|
||||
img = page.render()
|
||||
@ -503,42 +515,30 @@ class EbookReader:
|
||||
|
||||
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||
"""
|
||||
Set line spacing and re-render current page.
|
||||
|
||||
Set line spacing using pyWebLayout's native support.
|
||||
|
||||
Args:
|
||||
spacing: Line spacing in pixels
|
||||
|
||||
|
||||
Returns:
|
||||
PIL Image of the re-rendered page
|
||||
"""
|
||||
if not self.manager:
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
# Update page style
|
||||
self.page_style.line_spacing = max(0, spacing)
|
||||
|
||||
# Need to recreate the manager with new page style
|
||||
current_pos = self.manager.current_position
|
||||
current_font_scale = self.base_font_scale
|
||||
self.manager.shutdown()
|
||||
|
||||
self.manager = EreaderLayoutManager(
|
||||
blocks=self.blocks,
|
||||
page_size=self.page_size,
|
||||
document_id=self.document_id,
|
||||
buffer_size=self.buffer_size,
|
||||
page_style=self.page_style,
|
||||
bookmarks_dir=self.bookmarks_dir
|
||||
)
|
||||
|
||||
# Restore position
|
||||
self.manager.current_position = current_pos
|
||||
|
||||
# Restore font scale using the method (not direct assignment)
|
||||
if current_font_scale != 1.0:
|
||||
self.manager.set_font_scale(current_font_scale)
|
||||
|
||||
# Calculate delta from current spacing
|
||||
current_spacing = self.manager.page_style.line_spacing
|
||||
target_spacing = max(0, spacing)
|
||||
delta = target_spacing - current_spacing
|
||||
|
||||
# Use pyWebLayout's built-in methods to adjust spacing
|
||||
if delta > 0:
|
||||
self.manager.increase_line_spacing(abs(delta))
|
||||
elif delta < 0:
|
||||
self.manager.decrease_line_spacing(abs(delta))
|
||||
|
||||
# Get re-rendered page
|
||||
page = self.manager.get_current_page()
|
||||
return page.render()
|
||||
except Exception as e:
|
||||
@ -547,48 +547,68 @@ class EbookReader:
|
||||
|
||||
def set_inter_block_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||
"""
|
||||
Set spacing between blocks (paragraphs, headings, etc.) and re-render.
|
||||
|
||||
Set inter-block spacing using pyWebLayout's native support.
|
||||
|
||||
Args:
|
||||
spacing: Inter-block spacing in pixels
|
||||
|
||||
|
||||
Returns:
|
||||
PIL Image of the re-rendered page
|
||||
"""
|
||||
if not self.manager:
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
# Update page style
|
||||
self.page_style.inter_block_spacing = max(0, spacing)
|
||||
|
||||
# Need to recreate the manager with new page style
|
||||
current_pos = self.manager.current_position
|
||||
current_font_scale = self.base_font_scale
|
||||
self.manager.shutdown()
|
||||
|
||||
self.manager = EreaderLayoutManager(
|
||||
blocks=self.blocks,
|
||||
page_size=self.page_size,
|
||||
document_id=self.document_id,
|
||||
buffer_size=self.buffer_size,
|
||||
page_style=self.page_style,
|
||||
bookmarks_dir=self.bookmarks_dir
|
||||
)
|
||||
|
||||
# Restore position
|
||||
self.manager.current_position = current_pos
|
||||
|
||||
# Restore font scale using the method (not direct assignment)
|
||||
if current_font_scale != 1.0:
|
||||
self.manager.set_font_scale(current_font_scale)
|
||||
|
||||
# Calculate delta from current spacing
|
||||
current_spacing = self.manager.page_style.inter_block_spacing
|
||||
target_spacing = max(0, spacing)
|
||||
delta = target_spacing - current_spacing
|
||||
|
||||
# Use pyWebLayout's built-in methods to adjust spacing
|
||||
if delta > 0:
|
||||
self.manager.increase_inter_block_spacing(abs(delta))
|
||||
elif delta < 0:
|
||||
self.manager.decrease_inter_block_spacing(abs(delta))
|
||||
|
||||
# Get re-rendered page
|
||||
page = self.manager.get_current_page()
|
||||
return page.render()
|
||||
except Exception as e:
|
||||
print(f"Error setting inter-block spacing: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def set_word_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||
"""
|
||||
Set word spacing using pyWebLayout's native support.
|
||||
|
||||
Args:
|
||||
spacing: Word spacing in pixels
|
||||
|
||||
Returns:
|
||||
PIL Image of the re-rendered page
|
||||
"""
|
||||
if not self.manager:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Calculate delta from current spacing
|
||||
current_spacing = self.manager.page_style.word_spacing
|
||||
target_spacing = max(0, spacing)
|
||||
delta = target_spacing - current_spacing
|
||||
|
||||
# Use pyWebLayout's built-in methods to adjust spacing
|
||||
if delta > 0:
|
||||
self.manager.increase_word_spacing(abs(delta))
|
||||
elif delta < 0:
|
||||
self.manager.decrease_word_spacing(abs(delta))
|
||||
|
||||
# Get re-rendered page
|
||||
page = self.manager.get_current_page()
|
||||
return page.render()
|
||||
except Exception as e:
|
||||
print(f"Error setting word spacing: {e}")
|
||||
return None
|
||||
|
||||
def get_position_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get detailed information about the current position.
|
||||
@ -725,6 +745,9 @@ class EbookReader:
|
||||
elif event.gesture == GestureType.SWIPE_UP:
|
||||
# Swipe up from bottom opens TOC overlay
|
||||
return self._handle_swipe_up(event.y)
|
||||
elif event.gesture == GestureType.SWIPE_DOWN:
|
||||
# Swipe down from top opens settings overlay
|
||||
return self._handle_swipe_down(event.y)
|
||||
elif event.gesture == GestureType.PINCH_IN:
|
||||
return self._handle_zoom_out()
|
||||
elif event.gesture == GestureType.PINCH_OUT:
|
||||
@ -915,8 +938,26 @@ class EbookReader:
|
||||
|
||||
return GestureResponse(ActionType.NONE, {})
|
||||
|
||||
def _handle_swipe_down(self, y: int) -> GestureResponse:
|
||||
"""Handle swipe down gesture - opens settings overlay if from top of screen"""
|
||||
# Check if swipe started from top 20% of screen
|
||||
top_threshold = self.page_size[1] * 0.2
|
||||
|
||||
if y <= top_threshold:
|
||||
# Open settings overlay
|
||||
overlay_image = self.open_settings_overlay()
|
||||
if overlay_image:
|
||||
return GestureResponse(ActionType.OVERLAY_OPENED, {
|
||||
"overlay_type": "settings",
|
||||
"font_scale": self.base_font_scale,
|
||||
"line_spacing": self.page_style.line_spacing,
|
||||
"inter_block_spacing": self.page_style.inter_block_spacing
|
||||
})
|
||||
|
||||
return GestureResponse(ActionType.NONE, {})
|
||||
|
||||
def _handle_overlay_tap(self, x: int, y: int) -> GestureResponse:
|
||||
"""Handle tap when overlay is open - select chapter or close overlay"""
|
||||
"""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
|
||||
@ -961,6 +1002,74 @@ class EbookReader:
|
||||
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)
|
||||
|
||||
# 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
|
||||
})
|
||||
|
||||
# Not a setting control, 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, {})
|
||||
@ -1208,7 +1317,7 @@ class EbookReader:
|
||||
|
||||
def open_settings_overlay(self) -> Optional[Image.Image]:
|
||||
"""
|
||||
Open the settings overlay.
|
||||
Open the settings overlay with current settings values.
|
||||
|
||||
Returns:
|
||||
Composited image with settings overlay on top of current page, or None if no book loaded
|
||||
@ -1221,8 +1330,20 @@ class EbookReader:
|
||||
if not base_page:
|
||||
return None
|
||||
|
||||
# Get current settings
|
||||
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
|
||||
|
||||
# Open overlay and get composited image
|
||||
result = self.overlay_manager.open_settings_overlay(base_page)
|
||||
result = self.overlay_manager.open_settings_overlay(
|
||||
base_page,
|
||||
font_scale=font_scale,
|
||||
line_spacing=line_spacing,
|
||||
inter_block_spacing=inter_block_spacing,
|
||||
word_spacing=word_spacing
|
||||
)
|
||||
self.current_overlay_state = OverlayState.SETTINGS
|
||||
|
||||
return result
|
||||
|
||||
@ -125,3 +125,4 @@ class ActionType:
|
||||
OVERLAY_OPENED = "overlay_opened"
|
||||
OVERLAY_CLOSED = "overlay_closed"
|
||||
CHAPTER_SELECTED = "chapter_selected"
|
||||
SETTING_CHANGED = "setting_changed"
|
||||
|
||||
@ -192,131 +192,96 @@ def generate_reader_html(book_title: str, book_author: str, page_image_data: str
|
||||
return html
|
||||
|
||||
|
||||
def generate_settings_overlay() -> str:
|
||||
def generate_settings_overlay(
|
||||
font_scale: float = 1.0,
|
||||
line_spacing: int = 5,
|
||||
inter_block_spacing: int = 15,
|
||||
word_spacing: int = 0,
|
||||
page_size: tuple = (800, 1200)
|
||||
) -> str:
|
||||
"""
|
||||
Generate HTML for the settings overlay.
|
||||
Generate HTML for the settings overlay with current values.
|
||||
|
||||
Uses simple paragraphs with links, similar to TOC overlay,
|
||||
since pyWebLayout doesn't support HTML tables.
|
||||
|
||||
Args:
|
||||
font_scale: Current font scale (e.g., 1.0 = 100%, 1.2 = 120%)
|
||||
line_spacing: Current line spacing in pixels
|
||||
inter_block_spacing: Current inter-block spacing in pixels
|
||||
word_spacing: Current word spacing in pixels
|
||||
page_size: Page dimensions (width, height) for sizing the overlay
|
||||
|
||||
Returns:
|
||||
HTML string for settings overlay
|
||||
HTML string for settings overlay with clickable controls
|
||||
"""
|
||||
html = '''
|
||||
# Format current values for display
|
||||
font_percent = int(font_scale * 100)
|
||||
|
||||
html = f'''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.overlay-panel {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||
padding: 20px;
|
||||
min-width: 400px;
|
||||
}
|
||||
.overlay-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
.overlay-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.close-button {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-button:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
.settings-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.settings-table td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.setting-label {
|
||||
font-weight: bold;
|
||||
width: 40%;
|
||||
}
|
||||
.setting-control {
|
||||
width: 60%;
|
||||
text-align: right;
|
||||
}
|
||||
.control-button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.control-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="overlay-panel">
|
||||
<div class="overlay-header">
|
||||
<span class="overlay-title">Settings</span>
|
||||
<button class="close-button" id="btn-close">Close</button>
|
||||
</div>
|
||||
<body style="background-color: white; margin: 0; padding: 25px; font-family: Arial, sans-serif;">
|
||||
|
||||
<table class="settings-table">
|
||||
<tr>
|
||||
<td class="setting-label">Font Size</td>
|
||||
<td class="setting-control">
|
||||
<button class="control-button" id="btn-font-decrease">A-</button>
|
||||
<button class="control-button" id="btn-font-increase">A+</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="setting-label">Line Spacing</td>
|
||||
<td class="setting-control">
|
||||
<button class="control-button" id="btn-spacing-decrease">-</button>
|
||||
<button class="control-button" id="btn-spacing-increase">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="setting-label">Brightness</td>
|
||||
<td class="setting-control">
|
||||
<button class="control-button" id="btn-brightness-decrease">-</button>
|
||||
<button class="control-button" id="btn-brightness-increase">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="setting-label">WiFi</td>
|
||||
<td class="setting-control">
|
||||
<button class="control-button" id="btn-wifi">Configure</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h1 style="color: #000; margin: 0 0 8px 0; font-size: 24px; text-align: center; font-weight: bold;">
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<p style="text-align: center; color: #666; margin: 0 0 15px 0; padding-bottom: 12px;
|
||||
border-bottom: 2px solid #ccc; font-size: 13px;">
|
||||
Adjust reading preferences
|
||||
</p>
|
||||
|
||||
<div style="margin: 15px 0;">
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #007bff;">
|
||||
<b>Font Size: {font_percent}%</b>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:font_decrease" style="text-decoration: none; color: #000;">Decrease [ - ]</a>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:font_increase" style="text-decoration: none; color: #000;">Increase [ + ]</a>
|
||||
</p>
|
||||
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #28a745;">
|
||||
<b>Line Spacing: {line_spacing}px</b>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:line_spacing_decrease" style="text-decoration: none; color: #000;">Decrease [ - ]</a>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:line_spacing_increase" style="text-decoration: none; color: #000;">Increase [ + ]</a>
|
||||
</p>
|
||||
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #17a2b8;">
|
||||
<b>Paragraph Spacing: {inter_block_spacing}px</b>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:block_spacing_decrease" style="text-decoration: none; color: #000;">Decrease [ - ]</a>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:block_spacing_increase" style="text-decoration: none; color: #000;">Increase [ + ]</a>
|
||||
</p>
|
||||
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #ffc107;">
|
||||
<b>Word Spacing: {word_spacing}px</b>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:word_spacing_decrease" style="text-decoration: none; color: #000;">Decrease [ - ]</a>
|
||||
</p>
|
||||
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0;">
|
||||
<a href="setting:word_spacing_increase" style="text-decoration: none; color: #000;">Increase [ + ]</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
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
@ -200,29 +200,150 @@ class OverlayManager:
|
||||
# Composite and return
|
||||
return self.composite_overlay(base_page, overlay_panel)
|
||||
|
||||
def open_settings_overlay(self, base_page: Image.Image) -> Image.Image:
|
||||
def open_settings_overlay(
|
||||
self,
|
||||
base_page: Image.Image,
|
||||
font_scale: float = 1.0,
|
||||
line_spacing: int = 5,
|
||||
inter_block_spacing: int = 15,
|
||||
word_spacing: int = 0
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Open the settings overlay.
|
||||
Open the settings overlay with current settings values.
|
||||
|
||||
Args:
|
||||
base_page: Current reading page to show underneath
|
||||
font_scale: Current font scale
|
||||
line_spacing: Current line spacing
|
||||
inter_block_spacing: Current inter-block spacing
|
||||
word_spacing: Current word spacing
|
||||
|
||||
Returns:
|
||||
Composited image with settings overlay on top
|
||||
"""
|
||||
# Generate settings HTML
|
||||
html = generate_settings_overlay()
|
||||
# Import here to avoid circular dependency
|
||||
from .application import EbookReader
|
||||
|
||||
# Render HTML to image
|
||||
overlay_image = self.render_html_to_image(html)
|
||||
# Calculate panel size (60% of screen)
|
||||
panel_width = int(self.page_size[0] * 0.6)
|
||||
panel_height = int(self.page_size[1] * 0.7)
|
||||
|
||||
# Generate settings HTML with current values
|
||||
html = generate_settings_overlay(
|
||||
font_scale=font_scale,
|
||||
line_spacing=line_spacing,
|
||||
inter_block_spacing=inter_block_spacing,
|
||||
word_spacing=word_spacing,
|
||||
page_size=(panel_width, panel_height)
|
||||
)
|
||||
|
||||
# Create reader for overlay and keep it alive for querying
|
||||
if self._overlay_reader:
|
||||
self._overlay_reader.close()
|
||||
|
||||
self._overlay_reader = EbookReader(
|
||||
page_size=(panel_width, panel_height),
|
||||
margin=15,
|
||||
background_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
# Load the HTML content
|
||||
success = self._overlay_reader.load_html(
|
||||
html_string=html,
|
||||
title="Settings",
|
||||
author="",
|
||||
document_id="settings_overlay"
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise ValueError("Failed to load settings overlay HTML")
|
||||
|
||||
# Get the rendered page
|
||||
overlay_panel = self._overlay_reader.get_current_page()
|
||||
|
||||
# Calculate and store panel position for coordinate translation
|
||||
panel_x = int((self.page_size[0] - panel_width) / 2)
|
||||
panel_y = int((self.page_size[1] - panel_height) / 2)
|
||||
self._overlay_panel_offset = (panel_x, panel_y)
|
||||
|
||||
# Cache for later use
|
||||
self._cached_base_page = base_page.copy()
|
||||
self._cached_overlay_image = overlay_image
|
||||
self._cached_overlay_image = overlay_panel
|
||||
self.current_overlay = OverlayState.SETTINGS
|
||||
|
||||
# Composite and return
|
||||
return self.composite_overlay(base_page, overlay_image)
|
||||
return self.composite_overlay(base_page, overlay_panel)
|
||||
|
||||
def refresh_settings_overlay(
|
||||
self,
|
||||
updated_base_page: Image.Image,
|
||||
font_scale: float,
|
||||
line_spacing: int,
|
||||
inter_block_spacing: int,
|
||||
word_spacing: int = 0
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Refresh the settings overlay with updated values and background page.
|
||||
|
||||
This is used for live preview when settings change - it updates both
|
||||
the background page (with new settings applied) and the overlay panel
|
||||
(with new values displayed).
|
||||
|
||||
Args:
|
||||
updated_base_page: Updated reading page with new settings applied
|
||||
font_scale: Updated font scale
|
||||
line_spacing: Updated line spacing
|
||||
inter_block_spacing: Updated inter-block spacing
|
||||
word_spacing: Updated word spacing
|
||||
|
||||
Returns:
|
||||
Composited image with updated settings overlay
|
||||
"""
|
||||
# Import here to avoid circular dependency
|
||||
from .application import EbookReader
|
||||
|
||||
# Calculate panel size (60% of screen)
|
||||
panel_width = int(self.page_size[0] * 0.6)
|
||||
panel_height = int(self.page_size[1] * 0.7)
|
||||
|
||||
# Generate updated settings HTML
|
||||
html = generate_settings_overlay(
|
||||
font_scale=font_scale,
|
||||
line_spacing=line_spacing,
|
||||
inter_block_spacing=inter_block_spacing,
|
||||
word_spacing=word_spacing,
|
||||
page_size=(panel_width, panel_height)
|
||||
)
|
||||
|
||||
# Recreate overlay reader with updated HTML
|
||||
if self._overlay_reader:
|
||||
self._overlay_reader.close()
|
||||
|
||||
self._overlay_reader = EbookReader(
|
||||
page_size=(panel_width, panel_height),
|
||||
margin=15,
|
||||
background_color=(255, 255, 255)
|
||||
)
|
||||
|
||||
success = self._overlay_reader.load_html(
|
||||
html_string=html,
|
||||
title="Settings",
|
||||
author="",
|
||||
document_id="settings_overlay"
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise ValueError("Failed to load updated settings overlay HTML")
|
||||
|
||||
# Get the updated rendered panel
|
||||
overlay_panel = self._overlay_reader.get_current_page()
|
||||
|
||||
# Update caches
|
||||
self._cached_base_page = updated_base_page.copy()
|
||||
self._cached_overlay_image = overlay_panel
|
||||
|
||||
# Composite and return
|
||||
return self.composite_overlay(updated_base_page, overlay_panel)
|
||||
|
||||
def open_bookmarks_overlay(self, bookmarks: List[Dict[str, Any]], base_page: Image.Image) -> Image.Image:
|
||||
"""
|
||||
|
||||
425
examples/demo_settings_overlay.py
Normal file
425
examples/demo_settings_overlay.py
Normal file
@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo script for Settings overlay feature.
|
||||
|
||||
This script demonstrates the complete settings overlay workflow:
|
||||
1. Display reading page
|
||||
2. Swipe down from top to open settings overlay
|
||||
3. Display settings overlay with controls
|
||||
4. Tap on font size increase button
|
||||
5. Show live preview update (background page changes)
|
||||
6. Tap on line spacing increase button
|
||||
7. Show another live preview update
|
||||
8. Close overlay and show final page with new settings
|
||||
|
||||
Generates a GIF showing all these interactions.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from dreader import EbookReader, TouchEvent, GestureType
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
def add_gesture_annotation(image: Image.Image, text: str, position: str = "top") -> Image.Image:
|
||||
"""
|
||||
Add a text annotation to an image showing what gesture is being performed.
|
||||
|
||||
Args:
|
||||
image: Base image
|
||||
text: Annotation text
|
||||
position: "top" or "bottom"
|
||||
|
||||
Returns:
|
||||
Image with annotation
|
||||
"""
|
||||
# Create a copy
|
||||
annotated = image.copy()
|
||||
draw = ImageDraw.Draw(annotated)
|
||||
|
||||
# Try to use a nice font, fall back to default
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Calculate text position
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
x = (image.width - text_width) // 2
|
||||
if position == "top":
|
||||
y = 20
|
||||
else:
|
||||
y = image.height - text_height - 20
|
||||
|
||||
# Draw background rectangle
|
||||
padding = 10
|
||||
draw.rectangle(
|
||||
[x - padding, y - padding, x + text_width + padding, y + text_height + padding],
|
||||
fill=(0, 0, 0, 200)
|
||||
)
|
||||
|
||||
# Draw text
|
||||
draw.text((x, y), text, fill=(255, 255, 255), font=font)
|
||||
|
||||
return annotated
|
||||
|
||||
|
||||
def add_swipe_arrow(image: Image.Image, start_y: int, end_y: int) -> Image.Image:
|
||||
"""
|
||||
Add a visual swipe arrow to show gesture direction.
|
||||
|
||||
Args:
|
||||
image: Base image
|
||||
start_y: Starting Y position
|
||||
end_y: Ending Y position
|
||||
|
||||
Returns:
|
||||
Image with arrow overlay
|
||||
"""
|
||||
annotated = image.copy()
|
||||
draw = ImageDraw.Draw(annotated)
|
||||
|
||||
# Draw arrow in center of screen
|
||||
x = image.width // 2
|
||||
|
||||
# Draw line
|
||||
draw.line([(x, start_y), (x, end_y)], fill=(255, 100, 100), width=5)
|
||||
|
||||
# Draw arrowhead
|
||||
arrow_size = 20
|
||||
if end_y < start_y: # Upward arrow
|
||||
draw.polygon([
|
||||
(x, end_y),
|
||||
(x - arrow_size, end_y + arrow_size),
|
||||
(x + arrow_size, end_y + arrow_size)
|
||||
], fill=(255, 100, 100))
|
||||
else: # Downward arrow
|
||||
draw.polygon([
|
||||
(x, end_y),
|
||||
(x - arrow_size, end_y - arrow_size),
|
||||
(x + arrow_size, end_y - arrow_size)
|
||||
], fill=(255, 100, 100))
|
||||
|
||||
return annotated
|
||||
|
||||
|
||||
def add_tap_indicator(image: Image.Image, x: int, y: int, label: str = "") -> Image.Image:
|
||||
"""
|
||||
Add a visual tap indicator to show where user tapped.
|
||||
|
||||
Args:
|
||||
image: Base image
|
||||
x, y: Tap coordinates
|
||||
label: Optional label for the tap
|
||||
|
||||
Returns:
|
||||
Image with tap indicator
|
||||
"""
|
||||
annotated = image.copy()
|
||||
draw = ImageDraw.Draw(annotated)
|
||||
|
||||
# Draw circle at tap location
|
||||
radius = 30
|
||||
draw.ellipse(
|
||||
[x - radius, y - radius, x + radius, y + radius],
|
||||
outline=(255, 100, 100),
|
||||
width=5
|
||||
)
|
||||
|
||||
# Draw crosshair
|
||||
draw.line([(x - radius - 10, y), (x + radius + 10, y)], fill=(255, 100, 100), width=3)
|
||||
draw.line([(x, y - radius - 10), (x, y + radius + 10)], fill=(255, 100, 100), width=3)
|
||||
|
||||
# Add label if provided
|
||||
if label:
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
bbox = draw.textbbox((0, 0), label, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
|
||||
# Position label above tap point
|
||||
label_x = x - text_width // 2
|
||||
label_y = y - radius - 40
|
||||
|
||||
draw.text((label_x, label_y), label, fill=(255, 100, 100), font=font)
|
||||
|
||||
return annotated
|
||||
|
||||
|
||||
def main():
|
||||
"""Generate Settings overlay demo GIF"""
|
||||
print("=== Settings Overlay Demo ===")
|
||||
print()
|
||||
|
||||
# Find a test EPUB
|
||||
epub_dir = Path(__file__).parent.parent / 'tests' / 'data' / 'library-epub'
|
||||
epubs = list(epub_dir.glob('*.epub'))
|
||||
|
||||
if not epubs:
|
||||
print("Error: No test EPUB files found!")
|
||||
print(f"Looked in: {epub_dir}")
|
||||
return
|
||||
|
||||
epub_path = epubs[0]
|
||||
print(f"Using book: {epub_path.name}")
|
||||
|
||||
# Create reader
|
||||
reader = EbookReader(page_size=(800, 1200))
|
||||
|
||||
# Load book
|
||||
print("Loading book...")
|
||||
success = reader.load_epub(str(epub_path))
|
||||
|
||||
if not success:
|
||||
print("Error: Failed to load EPUB!")
|
||||
return
|
||||
|
||||
print(f"Loaded: {reader.book_title} by {reader.book_author}")
|
||||
print()
|
||||
|
||||
# Prepare frames for GIF
|
||||
frames = []
|
||||
frame_duration = [] # Duration in milliseconds for each frame
|
||||
|
||||
# Frame 1: Initial reading page
|
||||
print("Frame 1: Initial reading page...")
|
||||
page1 = reader.get_current_page()
|
||||
annotated1 = add_gesture_annotation(page1, f"Reading: {reader.book_title}", "top")
|
||||
frames.append(annotated1)
|
||||
frame_duration.append(2000) # 2 seconds
|
||||
|
||||
# Frame 2: Show swipe down gesture
|
||||
print("Frame 2: Swipe down gesture...")
|
||||
swipe_visual = add_swipe_arrow(page1, 100, 300)
|
||||
annotated2 = add_gesture_annotation(swipe_visual, "Swipe down from top", "top")
|
||||
frames.append(annotated2)
|
||||
frame_duration.append(1000) # 1 second
|
||||
|
||||
# Frame 3: Settings overlay appears
|
||||
print("Frame 3: Settings overlay opens...")
|
||||
event_swipe_down = TouchEvent(gesture=GestureType.SWIPE_DOWN, x=400, y=100)
|
||||
response = reader.handle_touch(event_swipe_down)
|
||||
print(f" Response: {response.action}")
|
||||
|
||||
# Get the overlay image by calling open_settings_overlay again
|
||||
overlay_image = reader.open_settings_overlay()
|
||||
annotated3 = add_gesture_annotation(overlay_image, "Settings", "top")
|
||||
frames.append(annotated3)
|
||||
frame_duration.append(3000) # 3 seconds to read
|
||||
|
||||
# Find actual button coordinates by querying the overlay
|
||||
print("Querying overlay for button positions...")
|
||||
link_positions = {}
|
||||
if reader.overlay_manager._overlay_reader:
|
||||
page = reader.overlay_manager._overlay_reader.manager.get_current_page()
|
||||
|
||||
# Scan for all links with very fine granularity to catch all buttons
|
||||
for y in range(0, 840, 3):
|
||||
for x in range(0, 480, 3):
|
||||
result = page.query_point((x, y))
|
||||
if result and result.link_target:
|
||||
if result.link_target not in link_positions:
|
||||
# Translate to screen coordinates
|
||||
panel_x_offset = int((800 - 480) / 2)
|
||||
panel_y_offset = int((1200 - 840) / 2)
|
||||
screen_x = x + panel_x_offset
|
||||
screen_y = y + panel_y_offset
|
||||
link_positions[result.link_target] = (screen_x, screen_y)
|
||||
|
||||
for link, (x, y) in sorted(link_positions.items()):
|
||||
print(f" Found: {link} at ({x}, {y})")
|
||||
|
||||
# Frame 4: Tap on font size increase button
|
||||
print("Frame 4: Tap on font size increase...")
|
||||
if 'setting:font_increase' in link_positions:
|
||||
tap_x, tap_y = link_positions['setting:font_increase']
|
||||
print(f" Using coordinates: ({tap_x}, {tap_y})")
|
||||
|
||||
tap_visual = add_tap_indicator(overlay_image, tap_x, tap_y, "Increase")
|
||||
annotated4 = add_gesture_annotation(tap_visual, "Tap to increase font size", "bottom")
|
||||
frames.append(annotated4)
|
||||
frame_duration.append(1500) # 1.5 seconds
|
||||
|
||||
# Frames 5-9: Font size increased (live preview) - show each tap individually
|
||||
print("Frames 5-9: Font size increased with live preview (5 taps, showing each)...")
|
||||
for i in range(5):
|
||||
event_tap_font = TouchEvent(gesture=GestureType.TAP, x=tap_x, y=tap_y)
|
||||
response = reader.handle_touch(event_tap_font)
|
||||
print(f" Tap {i+1}: {response.action} - Font scale: {response.data.get('font_scale', 'N/A')}")
|
||||
|
||||
# Get updated overlay image after each tap
|
||||
updated_overlay = reader.get_current_page() # This gets the composited overlay
|
||||
annotated = add_gesture_annotation(
|
||||
updated_overlay,
|
||||
f"Font: {int(reader.base_font_scale * 100)}% (tap {i+1}/5)",
|
||||
"top"
|
||||
)
|
||||
frames.append(annotated)
|
||||
frame_duration.append(800) # 0.8 seconds per tap
|
||||
|
||||
# Hold on final font size for a bit longer
|
||||
final_font_overlay = reader.get_current_page()
|
||||
annotated_final = add_gesture_annotation(
|
||||
final_font_overlay,
|
||||
f"Font: {int(reader.base_font_scale * 100)}% (complete)",
|
||||
"top"
|
||||
)
|
||||
frames.append(annotated_final)
|
||||
frame_duration.append(1500) # 1.5 seconds to see the final result
|
||||
else:
|
||||
print(" Skipping - button not found")
|
||||
updated_overlay = overlay_image
|
||||
|
||||
# Get current overlay state for line spacing section
|
||||
current_overlay = reader.get_current_page()
|
||||
|
||||
# Frame N: Tap on line spacing increase button
|
||||
print("Frame N: Tap on line spacing increase...")
|
||||
if 'setting:line_spacing_increase' in link_positions:
|
||||
tap_x2, tap_y2 = link_positions['setting:line_spacing_increase']
|
||||
print(f" Using coordinates: ({tap_x2}, {tap_y2})")
|
||||
|
||||
tap_visual2 = add_tap_indicator(current_overlay, tap_x2, tap_y2, "Increase")
|
||||
annotated_ls_tap = add_gesture_annotation(tap_visual2, "Tap to increase line spacing", "bottom")
|
||||
frames.append(annotated_ls_tap)
|
||||
frame_duration.append(1500) # 1.5 seconds
|
||||
|
||||
# Frames N+1 to N+5: Line spacing increased (live preview) - show each tap individually
|
||||
print("Frames N+1 to N+5: Line spacing increased with live preview (5 taps, showing each)...")
|
||||
for i in range(5):
|
||||
event_tap_spacing = TouchEvent(gesture=GestureType.TAP, x=tap_x2, y=tap_y2)
|
||||
response = reader.handle_touch(event_tap_spacing)
|
||||
print(f" Tap {i+1}: {response.action} - Line spacing: {response.data.get('line_spacing', 'N/A')}")
|
||||
|
||||
# Get updated overlay image after each tap
|
||||
updated_overlay2 = reader.get_current_page()
|
||||
annotated = add_gesture_annotation(
|
||||
updated_overlay2,
|
||||
f"Line Spacing: {reader.page_style.line_spacing}px (tap {i+1}/5)",
|
||||
"top"
|
||||
)
|
||||
frames.append(annotated)
|
||||
frame_duration.append(800) # 0.8 seconds per tap
|
||||
|
||||
# Hold on final line spacing for a bit longer
|
||||
final_spacing_overlay = reader.get_current_page()
|
||||
annotated_final_ls = add_gesture_annotation(
|
||||
final_spacing_overlay,
|
||||
f"Line Spacing: {reader.page_style.line_spacing}px (complete)",
|
||||
"top"
|
||||
)
|
||||
frames.append(annotated_final_ls)
|
||||
frame_duration.append(1500) # 1.5 seconds to see the final result
|
||||
else:
|
||||
print(" Skipping - button not found")
|
||||
|
||||
# Get current overlay state for paragraph spacing section
|
||||
current_overlay2 = reader.get_current_page()
|
||||
|
||||
# Frame M: Tap on paragraph spacing increase button
|
||||
print("Frame M: Tap on paragraph spacing increase...")
|
||||
if 'setting:block_spacing_increase' in link_positions:
|
||||
tap_x3, tap_y3 = link_positions['setting:block_spacing_increase']
|
||||
print(f" Using coordinates: ({tap_x3}, {tap_y3})")
|
||||
|
||||
tap_visual3 = add_tap_indicator(current_overlay2, tap_x3, tap_y3, "Increase")
|
||||
annotated_ps_tap = add_gesture_annotation(tap_visual3, "Tap to increase paragraph spacing", "bottom")
|
||||
frames.append(annotated_ps_tap)
|
||||
frame_duration.append(1500) # 1.5 seconds
|
||||
|
||||
# Frames M+1 to M+5: Paragraph spacing increased (live preview) - show each tap individually
|
||||
print("Frames M+1 to M+5: Paragraph spacing increased with live preview (5 taps, showing each)...")
|
||||
for i in range(5):
|
||||
event_tap_para = TouchEvent(gesture=GestureType.TAP, x=tap_x3, y=tap_y3)
|
||||
response = reader.handle_touch(event_tap_para)
|
||||
print(f" Tap {i+1}: {response.action} - Paragraph spacing: {response.data.get('inter_block_spacing', 'N/A')}")
|
||||
|
||||
# Get updated overlay image after each tap
|
||||
updated_overlay3 = reader.get_current_page()
|
||||
annotated = add_gesture_annotation(
|
||||
updated_overlay3,
|
||||
f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (tap {i+1}/5)",
|
||||
"top"
|
||||
)
|
||||
frames.append(annotated)
|
||||
frame_duration.append(800) # 0.8 seconds per tap
|
||||
|
||||
# Hold on final paragraph spacing for a bit longer
|
||||
final_para_overlay = reader.get_current_page()
|
||||
annotated_final_ps = add_gesture_annotation(
|
||||
final_para_overlay,
|
||||
f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (complete)",
|
||||
"top"
|
||||
)
|
||||
frames.append(annotated_final_ps)
|
||||
frame_duration.append(1500) # 1.5 seconds to see the final result
|
||||
else:
|
||||
print(" Skipping - button not found")
|
||||
|
||||
# Frame Z: Tap outside to close
|
||||
print("Frame Z: Close overlay...")
|
||||
final_overlay_state = reader.get_current_page()
|
||||
tap_visual_close = add_tap_indicator(final_overlay_state, 100, 600, "Close")
|
||||
annotated_close = add_gesture_annotation(tap_visual_close, "Tap outside to close", "bottom")
|
||||
frames.append(annotated_close)
|
||||
frame_duration.append(1500) # 1.5 seconds
|
||||
|
||||
# Final Frame: Back to reading with new settings applied
|
||||
print("Final Frame: Back to reading with new settings...")
|
||||
event_close = TouchEvent(gesture=GestureType.TAP, x=100, y=600)
|
||||
response = reader.handle_touch(event_close)
|
||||
print(f" Response: {response.action}")
|
||||
|
||||
final_page = reader.get_current_page()
|
||||
annotated_final = add_gesture_annotation(
|
||||
final_page,
|
||||
f"Settings Applied: {int(reader.base_font_scale * 100)}% font, {reader.page_style.line_spacing}px line, {reader.page_style.inter_block_spacing}px para",
|
||||
"top"
|
||||
)
|
||||
frames.append(annotated_final)
|
||||
frame_duration.append(3000) # 3 seconds
|
||||
|
||||
# Save as GIF
|
||||
output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'settings_overlay_demo.gif'
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print()
|
||||
print(f"Saving GIF with {len(frames)} frames...")
|
||||
frames[0].save(
|
||||
output_path,
|
||||
save_all=True,
|
||||
append_images=frames[1:],
|
||||
duration=frame_duration,
|
||||
loop=0,
|
||||
optimize=False
|
||||
)
|
||||
|
||||
print(f"✓ GIF saved to: {output_path}")
|
||||
print(f" Size: {output_path.stat().st_size / 1024:.1f} KB")
|
||||
print(f" Frames: {len(frames)}")
|
||||
print(f" Total duration: {sum(frame_duration) / 1000:.1f}s")
|
||||
|
||||
# Also save individual frames for documentation
|
||||
frames_dir = output_path.parent / 'settings_overlay_frames'
|
||||
frames_dir.mkdir(exist_ok=True)
|
||||
|
||||
for i, frame in enumerate(frames):
|
||||
frame_path = frames_dir / f'frame_{i+1:02d}.png'
|
||||
frame.save(frame_path)
|
||||
|
||||
print(f"✓ Individual frames saved to: {frames_dir}")
|
||||
|
||||
# Cleanup
|
||||
reader.close()
|
||||
|
||||
print()
|
||||
print("=== Demo Complete ===")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
345
tests/test_settings_overlay.py
Normal file
345
tests/test_settings_overlay.py
Normal file
@ -0,0 +1,345 @@
|
||||
"""
|
||||
Unit tests for Settings overlay functionality.
|
||||
|
||||
Tests the complete workflow of:
|
||||
1. Opening settings overlay with swipe down gesture
|
||||
2. Adjusting settings (font size, line spacing, etc.)
|
||||
3. Live preview updates
|
||||
4. Closing overlay
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from dreader import (
|
||||
EbookReader,
|
||||
TouchEvent,
|
||||
GestureType,
|
||||
ActionType,
|
||||
OverlayState
|
||||
)
|
||||
|
||||
|
||||
class TestSettingsOverlay(unittest.TestCase):
|
||||
"""Test Settings overlay opening, interaction, and closing"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test reader with a book"""
|
||||
self.reader = EbookReader(page_size=(800, 1200))
|
||||
|
||||
# Load a test EPUB
|
||||
test_epub = Path(__file__).parent / 'data' / 'library-epub' / 'alice.epub'
|
||||
if not test_epub.exists():
|
||||
# Try to find any EPUB in test data
|
||||
epub_dir = Path(__file__).parent / 'data' / 'library-epub'
|
||||
epubs = list(epub_dir.glob('*.epub'))
|
||||
if epubs:
|
||||
test_epub = epubs[0]
|
||||
else:
|
||||
self.skipTest("No test EPUB files available")
|
||||
|
||||
success = self.reader.load_epub(str(test_epub))
|
||||
self.assertTrue(success, "Failed to load test EPUB")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up"""
|
||||
self.reader.close()
|
||||
|
||||
def test_open_settings_overlay_directly(self):
|
||||
"""Test opening settings overlay using direct API call"""
|
||||
# Initially no overlay
|
||||
self.assertFalse(self.reader.is_overlay_open())
|
||||
|
||||
# Open settings overlay
|
||||
overlay_image = self.reader.open_settings_overlay()
|
||||
|
||||
# Should return an image
|
||||
self.assertIsNotNone(overlay_image)
|
||||
self.assertEqual(overlay_image.size, (800, 1200))
|
||||
|
||||
# Overlay should be open
|
||||
self.assertTrue(self.reader.is_overlay_open())
|
||||
self.assertEqual(self.reader.get_overlay_state(), OverlayState.SETTINGS)
|
||||
|
||||
def test_close_settings_overlay_directly(self):
|
||||
"""Test closing settings overlay using direct API call"""
|
||||
# Open overlay first
|
||||
self.reader.open_settings_overlay()
|
||||
self.assertTrue(self.reader.is_overlay_open())
|
||||
|
||||
# Close overlay
|
||||
page_image = self.reader.close_overlay()
|
||||
|
||||
# Should return base page
|
||||
self.assertIsNotNone(page_image)
|
||||
|
||||
# Overlay should be closed
|
||||
self.assertFalse(self.reader.is_overlay_open())
|
||||
self.assertEqual(self.reader.get_overlay_state(), OverlayState.NONE)
|
||||
|
||||
def test_swipe_down_from_top_opens_settings(self):
|
||||
"""Test that swipe down from top of screen opens settings overlay"""
|
||||
# Create swipe down event from top of screen (y=100, which is < 20% of 1200)
|
||||
event = TouchEvent(
|
||||
gesture=GestureType.SWIPE_DOWN,
|
||||
x=400,
|
||||
y=100
|
||||
)
|
||||
|
||||
# Handle gesture
|
||||
response = self.reader.handle_touch(event)
|
||||
|
||||
# Should open overlay
|
||||
self.assertEqual(response.action, ActionType.OVERLAY_OPENED)
|
||||
self.assertEqual(response.data['overlay_type'], 'settings')
|
||||
self.assertTrue(self.reader.is_overlay_open())
|
||||
|
||||
def test_swipe_down_from_middle_does_not_open_settings(self):
|
||||
"""Test that swipe down from middle of screen does NOT open settings"""
|
||||
# Create swipe down event from middle of screen (y=600, which is > 20% of 1200)
|
||||
event = TouchEvent(
|
||||
gesture=GestureType.SWIPE_DOWN,
|
||||
x=400,
|
||||
y=600
|
||||
)
|
||||
|
||||
# Handle gesture
|
||||
response = self.reader.handle_touch(event)
|
||||
|
||||
# Should not open overlay
|
||||
self.assertEqual(response.action, ActionType.NONE)
|
||||
self.assertFalse(self.reader.is_overlay_open())
|
||||
|
||||
def test_tap_outside_closes_settings_overlay(self):
|
||||
"""Test that tapping outside the settings panel closes it"""
|
||||
# Open overlay first
|
||||
self.reader.open_settings_overlay()
|
||||
self.assertTrue(self.reader.is_overlay_open())
|
||||
|
||||
# Tap in the far left (outside the centered panel)
|
||||
event = TouchEvent(
|
||||
gesture=GestureType.TAP,
|
||||
x=50, # Well outside panel
|
||||
y=600
|
||||
)
|
||||
|
||||
# Handle gesture
|
||||
response = self.reader.handle_touch(event)
|
||||
|
||||
# Should close overlay
|
||||
self.assertEqual(response.action, ActionType.OVERLAY_CLOSED)
|
||||
self.assertFalse(self.reader.is_overlay_open())
|
||||
|
||||
def test_font_size_increase(self):
|
||||
"""Test increasing font size through settings overlay"""
|
||||
# Open overlay
|
||||
self.reader.open_settings_overlay()
|
||||
initial_font_scale = self.reader.base_font_scale
|
||||
|
||||
# Get overlay reader to query button positions
|
||||
overlay_manager = self.reader.overlay_manager
|
||||
overlay_reader = overlay_manager._overlay_reader
|
||||
|
||||
if not overlay_reader or not overlay_reader.manager:
|
||||
self.skipTest("Overlay reader not available for querying")
|
||||
|
||||
# Query the overlay to find the "A+" button link
|
||||
# We'll search for it by looking for links with "setting:font_increase"
|
||||
page = overlay_reader.manager.get_current_page()
|
||||
|
||||
# Try multiple Y positions in the font size row to find the button
|
||||
# Panel is 60% of screen width (480px) centered (x offset = 160)
|
||||
# First setting row should be around y=100-150 in panel coordinates
|
||||
found_button = False
|
||||
tap_x = None
|
||||
tap_y = None
|
||||
|
||||
for y in range(80, 180, 10):
|
||||
for x in range(300, 450, 20): # Right side of panel where buttons are
|
||||
# Translate to panel coordinates
|
||||
panel_x_offset = int((800 - 480) / 2)
|
||||
panel_y_offset = int((1200 - 840) / 2)
|
||||
panel_x = x - panel_x_offset
|
||||
panel_y = y - panel_y_offset
|
||||
|
||||
if panel_x < 0 or panel_y < 0:
|
||||
continue
|
||||
|
||||
result = page.query_point((panel_x, panel_y))
|
||||
if result and result.link_target == "setting:font_increase":
|
||||
tap_x = x
|
||||
tap_y = y
|
||||
found_button = True
|
||||
break
|
||||
if found_button:
|
||||
break
|
||||
|
||||
if not found_button:
|
||||
# Fallback: use approximate coordinates
|
||||
# Based on HTML layout: panel center + right side button
|
||||
tap_x = 550
|
||||
tap_y = 350
|
||||
|
||||
# Tap the increase button (in screen coordinates)
|
||||
event = TouchEvent(
|
||||
gesture=GestureType.TAP,
|
||||
x=tap_x,
|
||||
y=tap_y
|
||||
)
|
||||
|
||||
response = self.reader.handle_touch(event)
|
||||
|
||||
# Should either change setting or close (depending on whether we hit the button)
|
||||
if response.action == ActionType.SETTING_CHANGED:
|
||||
# Font size should have increased
|
||||
self.assertGreater(self.reader.base_font_scale, initial_font_scale)
|
||||
# Overlay should still be open
|
||||
self.assertTrue(self.reader.is_overlay_open())
|
||||
else:
|
||||
# If we missed the button, that's OK for this test
|
||||
pass
|
||||
|
||||
def test_line_spacing_adjustment(self):
|
||||
"""Test adjusting line spacing through settings overlay"""
|
||||
# Open overlay
|
||||
self.reader.open_settings_overlay()
|
||||
initial_spacing = self.reader.page_style.line_spacing
|
||||
|
||||
# Close overlay for this test (full interaction would require precise coordinates)
|
||||
self.reader.close_overlay()
|
||||
|
||||
# Verify we can adjust line spacing programmatically
|
||||
self.reader.set_line_spacing(initial_spacing + 2)
|
||||
self.assertEqual(self.reader.page_style.line_spacing, initial_spacing + 2)
|
||||
|
||||
def test_settings_values_displayed_in_overlay(self):
|
||||
"""Test that current settings values are shown in the overlay"""
|
||||
# Set specific values
|
||||
self.reader.set_font_size(1.5) # 150%
|
||||
self.reader.set_line_spacing(10)
|
||||
|
||||
# Open overlay
|
||||
overlay_image = self.reader.open_settings_overlay()
|
||||
self.assertIsNotNone(overlay_image)
|
||||
|
||||
# Overlay should be open with current values
|
||||
# (Visual verification would show "150%" and "10px" in the HTML)
|
||||
self.assertTrue(self.reader.is_overlay_open())
|
||||
|
||||
def test_multiple_setting_changes(self):
|
||||
"""Test making multiple setting changes in sequence"""
|
||||
initial_font = self.reader.base_font_scale
|
||||
initial_spacing = self.reader.page_style.line_spacing
|
||||
|
||||
# Change font size
|
||||
self.reader.increase_font_size()
|
||||
self.assertNotEqual(self.reader.base_font_scale, initial_font)
|
||||
|
||||
# Change line spacing
|
||||
self.reader.set_line_spacing(initial_spacing + 5)
|
||||
self.assertNotEqual(self.reader.page_style.line_spacing, initial_spacing)
|
||||
|
||||
# Open overlay to verify values
|
||||
overlay_image = self.reader.open_settings_overlay()
|
||||
self.assertIsNotNone(overlay_image)
|
||||
|
||||
def test_settings_persist_after_overlay_close(self):
|
||||
"""Test that setting changes persist after closing overlay"""
|
||||
# Make a change
|
||||
initial_font = self.reader.base_font_scale
|
||||
self.reader.increase_font_size()
|
||||
new_font = self.reader.base_font_scale
|
||||
|
||||
# Open and close overlay
|
||||
self.reader.open_settings_overlay()
|
||||
self.reader.close_overlay()
|
||||
|
||||
# Settings should still be changed
|
||||
self.assertEqual(self.reader.base_font_scale, new_font)
|
||||
self.assertNotEqual(self.reader.base_font_scale, initial_font)
|
||||
|
||||
def test_overlay_refresh_after_setting_change(self):
|
||||
"""Test that overlay can be refreshed with updated values"""
|
||||
# Open overlay
|
||||
self.reader.open_settings_overlay()
|
||||
|
||||
# Access refresh method through overlay manager
|
||||
overlay_manager = self.reader.overlay_manager
|
||||
|
||||
# Change a setting programmatically
|
||||
self.reader.increase_font_size()
|
||||
new_page = self.reader.get_current_page(include_highlights=False)
|
||||
|
||||
# Refresh overlay
|
||||
refreshed_image = overlay_manager.refresh_settings_overlay(
|
||||
updated_base_page=new_page,
|
||||
font_scale=self.reader.base_font_scale,
|
||||
line_spacing=self.reader.page_style.line_spacing,
|
||||
inter_block_spacing=self.reader.page_style.inter_block_spacing
|
||||
)
|
||||
|
||||
self.assertIsNotNone(refreshed_image)
|
||||
self.assertEqual(refreshed_image.size, (800, 1200))
|
||||
|
||||
def test_line_spacing_actually_changes_rendering(self):
|
||||
"""Verify that line spacing changes produce different rendered images"""
|
||||
# Close any open overlay first
|
||||
if self.reader.is_overlay_open():
|
||||
self.reader.close_overlay()
|
||||
|
||||
# Set initial line spacing and get page
|
||||
self.reader.set_line_spacing(5)
|
||||
page1 = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page1)
|
||||
|
||||
# Change line spacing significantly
|
||||
self.reader.set_line_spacing(30)
|
||||
page2 = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page2)
|
||||
|
||||
# Images should be different (different line spacing should affect rendering)
|
||||
self.assertNotEqual(page1.tobytes(), page2.tobytes(),
|
||||
"Line spacing change should affect rendering")
|
||||
|
||||
def test_inter_block_spacing_actually_changes_rendering(self):
|
||||
"""Verify that inter-block spacing changes produce different rendered images"""
|
||||
# Close any open overlay first
|
||||
if self.reader.is_overlay_open():
|
||||
self.reader.close_overlay()
|
||||
|
||||
# Set initial inter-block spacing and get page
|
||||
self.reader.set_inter_block_spacing(15)
|
||||
page1 = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page1)
|
||||
|
||||
# Change inter-block spacing significantly
|
||||
self.reader.set_inter_block_spacing(50)
|
||||
page2 = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page2)
|
||||
|
||||
# Images should be different
|
||||
self.assertNotEqual(page1.tobytes(), page2.tobytes(),
|
||||
"Inter-block spacing change should affect rendering")
|
||||
|
||||
def test_word_spacing_actually_changes_rendering(self):
|
||||
"""Verify that word spacing changes produce different rendered images"""
|
||||
# Close any open overlay first
|
||||
if self.reader.is_overlay_open():
|
||||
self.reader.close_overlay()
|
||||
|
||||
# Set initial word spacing and get page
|
||||
self.reader.set_word_spacing(0)
|
||||
page1 = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page1)
|
||||
|
||||
# Change word spacing significantly
|
||||
self.reader.set_word_spacing(20)
|
||||
page2 = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page2)
|
||||
|
||||
# Images should be different
|
||||
self.assertNotEqual(page1.tobytes(), page2.tobytes(),
|
||||
"Word spacing change should affect rendering")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
x
Reference in New Issue
Block a user