improved library screen, fixed issues with image rendering and navigation
Python CI / test (3.12) (push) Successful in 6m50s
Python CI / test (3.13) (push) Has been cancelled

This commit is contained in:
2025-11-10 14:13:59 +01:00
parent 8f6205dcfe
commit dd392d2e15
11 changed files with 990 additions and 642 deletions
+214 -71
View File
@@ -45,7 +45,8 @@ class LibraryManager:
self,
library_path: str,
cache_dir: Optional[str] = None,
page_size: Tuple[int, int] = (800, 1200)
page_size: Tuple[int, int] = (800, 1200),
books_per_page: int = 10
):
"""
Initialize library manager.
@@ -54,9 +55,11 @@ class LibraryManager:
library_path: Path to directory containing EPUB files
cache_dir: Optional cache directory for covers. If None, uses default.
page_size: Page size for library view rendering
books_per_page: Number of books to display per page (must be even for 2-column layout)
"""
self.library_path = Path(library_path)
self.page_size = page_size
self.books_per_page = books_per_page if books_per_page % 2 == 0 else books_per_page + 1
# Set cache directory
if cache_dir:
@@ -75,6 +78,7 @@ class LibraryManager:
self.temp_cover_files: List[str] = [] # Track temp files for cleanup
self.row_bounds: List[Tuple[int, int, int, int]] = [] # Bounding boxes for rows (x, y, w, h)
self.table_renderer: Optional[TableRenderer] = None # Store renderer for bounds info
self.current_page: int = 0 # Current page index for pagination
@staticmethod
def _get_default_cache_dir() -> Path:
@@ -149,12 +153,13 @@ class LibraryManager:
print(f"Error caching cover for {book['title']}: {e}")
return None
def create_library_table(self, books: Optional[List[Dict]] = None) -> Table:
def create_library_table(self, books: Optional[List[Dict]] = None, page: Optional[int] = None) -> Table:
"""
Create interactive library table with book covers and info.
Create interactive library table with book covers and info in 2-column grid.
Args:
books: List of books to display. If None, uses self.books
page: Page number to display (0-indexed). If None, uses self.current_page
Returns:
Table object ready for rendering
@@ -162,83 +167,133 @@ class LibraryManager:
if books is None:
books = self.books
if page is None:
page = self.current_page
if not books:
print("No books to display in library")
books = []
print(f"Creating library table with {len(books)} books...")
# Calculate pagination
total_pages = (len(books) + self.books_per_page - 1) // self.books_per_page
start_idx = page * self.books_per_page
end_idx = min(start_idx + self.books_per_page, len(books))
page_books = books[start_idx:end_idx]
# Create table
table = Table(caption="My Library", style=Font(font_size=18, weight="bold"))
print(f"Creating library table with {len(page_books)} books (page {page + 1}/{total_pages})...")
# Add books as rows
for i, book in enumerate(books):
row = table.create_row("body")
# Create table with caption showing page info
caption_text = f"My Library (Page {page + 1}/{total_pages})" if total_pages > 1 else "My Library"
table = Table(caption=caption_text, style=Font(font_size=18, weight="bold"))
# Cover cell with interactive image
cover_cell = row.create_cell()
cover_path = book.get('cover_path')
book_path = book['path']
# Add books in 2-column grid (each pair of books gets 2 rows: covers then details)
for i in range(0, len(page_books), 2):
# Row 1: Covers for this pair
cover_row = table.create_row("body")
# Create callback that returns book path
callback = lambda point, path=book_path: path
# Add first book's cover (left column)
self._add_book_cover(cover_row, page_books[i])
if cover_path and Path(cover_path).exists():
# Use cached cover with callback
img = InteractiveImage.create_and_add_to(
cover_cell,
source=cover_path,
alt_text=book['title'],
callback=callback
)
elif book.get('cover_data'):
# Decode base64 and save to temp file for InteractiveImage
try:
img_data = base64.b64decode(book['cover_data'])
img = Image.open(BytesIO(img_data))
# Save to temp file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
img.save(tmp.name, 'PNG')
temp_path = tmp.name
self.temp_cover_files.append(temp_path)
img = InteractiveImage.create_and_add_to(
cover_cell,
source=temp_path,
alt_text=book['title'],
callback=callback
)
except Exception as e:
print(f"Error creating cover image for {book['title']}: {e}")
self._add_no_cover_text(cover_cell)
# Add second book's cover (right column) if it exists
if i + 1 < len(page_books):
self._add_book_cover(cover_row, page_books[i + 1])
else:
# No cover available
self._add_no_cover_text(cover_cell)
# Add empty cell if odd number of books
cover_row.create_cell()
# Book info cell
info_cell = row.create_cell()
# Row 2: Details for this pair
details_row = table.create_row("body")
# Title paragraph
title_para = info_cell.create_paragraph()
for word in book['title'].split():
title_para.add_word(Word(word, Font(font_size=14, weight="bold")))
# Add first book's details (left column)
self._add_book_details(details_row, page_books[i])
# Author paragraph
author_para = info_cell.create_paragraph()
for word in book.get('author', 'Unknown').split():
author_para.add_word(Word(word, Font(font_size=12)))
# Filename paragraph (small, gray)
filename_para = info_cell.create_paragraph()
filename_para.add_word(Word(
Path(book['path']).name,
Font(font_size=10, colour=(150, 150, 150))
))
# Add second book's details (right column) if it exists
if i + 1 < len(page_books):
self._add_book_details(details_row, page_books[i + 1])
else:
# Add empty cell if odd number of books
details_row.create_cell()
self.library_table = table
return table
def _add_book_cover(self, row, book: Dict):
"""
Add a book cover to a table row.
Args:
row: Table row to add cover to
book: Book dictionary with metadata
"""
cover_cell = row.create_cell()
cover_path = book.get('cover_path')
book_path = book['path']
# Create callback that returns book path
callback = lambda point, path=book_path: path
# Add cover image
if cover_path and Path(cover_path).exists():
# Use cached cover with callback
img = InteractiveImage.create_and_add_to(
cover_cell,
source=cover_path,
alt_text=book['title'],
callback=callback
)
elif book.get('cover_data'):
# Decode base64 and save to temp file for InteractiveImage
try:
img_data = base64.b64decode(book['cover_data'])
img = Image.open(BytesIO(img_data))
# Save to temp file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
img.save(tmp.name, 'PNG')
temp_path = tmp.name
self.temp_cover_files.append(temp_path)
img = InteractiveImage.create_and_add_to(
cover_cell,
source=temp_path,
alt_text=book['title'],
callback=callback
)
except Exception as e:
print(f"Error creating cover image for {book['title']}: {e}")
self._add_no_cover_text(cover_cell)
else:
# No cover available
self._add_no_cover_text(cover_cell)
def _add_book_details(self, row, book: Dict):
"""
Add book details (title, author, filename) to a table row.
Args:
row: Table row to add details to
book: Book dictionary with metadata
"""
details_cell = row.create_cell()
# Title paragraph
title_para = details_cell.create_paragraph()
for word in book['title'].split():
title_para.add_word(Word(word, Font(font_size=14, weight="bold")))
# Author paragraph
author_para = details_cell.create_paragraph()
for word in book.get('author', 'Unknown').split():
author_para.add_word(Word(word, Font(font_size=12)))
# Filename paragraph (small, gray)
filename_para = details_cell.create_paragraph()
filename_para.add_word(Word(
Path(book['path']).name,
Font(font_size=10, colour=(150, 150, 150))
))
def _add_no_cover_text(self, cell):
"""Add placeholder text when no cover is available"""
para = cell.create_paragraph()
@@ -306,10 +361,11 @@ class LibraryManager:
def handle_library_tap(self, x: int, y: int) -> Optional[str]:
"""
Handle tap event on library view.
Handle tap event on library view with 2-column grid.
Checks if the tap is within any row's bounds and returns the corresponding
book path. This makes the entire row interactive, not just the cover image.
The layout has alternating rows: cover rows and detail rows.
Each pair of rows (cover + detail) represents one pair of books (2 books).
Tapping on either the cover row or detail row selects the corresponding book.
Args:
x: X coordinate of tap
@@ -323,6 +379,11 @@ class LibraryManager:
return None
try:
# Get paginated books for current page
start_idx = self.current_page * self.books_per_page
end_idx = min(start_idx + self.books_per_page, len(self.books))
page_books = self.books[start_idx:end_idx]
# Build a mapping of row sections in order
all_rows = list(self.library_table.all_rows())
@@ -345,11 +406,43 @@ class LibraryManager:
# Find which body row this is (0-indexed)
body_row_index = sum(1 for s, _ in all_rows[:row_idx] if s == "body")
# Return the corresponding book
if body_row_index < len(self.books):
book_path = self.books[body_row_index]['path']
print(f"Book selected (row {body_row_index}): {book_path}")
return book_path
# Each pair of books uses 2 rows (cover row + detail row)
# Determine which book pair this row belongs to
book_pair_index = body_row_index // 2 # Which pair of books (0, 1, 2, ...)
is_cover_row = body_row_index % 2 == 0 # Even rows are covers, odd are details
# Check cell renderers in this row
if hasattr(row_renderer, '_cell_renderers') and len(row_renderer._cell_renderers) >= 1:
# Check left cell (first book in pair)
left_cell = row_renderer._cell_renderers[0]
left_x, left_y = left_cell._origin
left_w, left_h = left_cell._size
if (left_x <= x <= left_x + left_w and
left_y <= y <= left_y + left_h):
# Left column (first book in pair)
book_index = book_pair_index * 2
if book_index < len(page_books):
book_path = page_books[book_index]['path']
row_type = "cover" if is_cover_row else "detail"
print(f"Book selected (pair {book_pair_index}, left {row_type}): {book_path}")
return book_path
# Check right cell (second book in pair) if it exists
if len(row_renderer._cell_renderers) >= 2:
right_cell = row_renderer._cell_renderers[1]
right_x, right_y = right_cell._origin
right_w, right_h = right_cell._size
if (right_x <= x <= right_x + right_w and
right_y <= y <= right_y + right_h):
# Right column (second book in pair)
book_index = book_pair_index * 2 + 1
if book_index < len(page_books):
book_path = page_books[book_index]['path']
row_type = "cover" if is_cover_row else "detail"
print(f"Book selected (pair {book_pair_index}, right {row_type}): {book_path}")
return book_path
print(f"No book tapped at ({x}, {y})")
return None
@@ -374,6 +467,56 @@ class LibraryManager:
return self.books[index]
return None
def next_page(self) -> bool:
"""
Navigate to next page of library.
Returns:
True if page changed, False if already on last page
"""
total_pages = (len(self.books) + self.books_per_page - 1) // self.books_per_page
if self.current_page < total_pages - 1:
self.current_page += 1
return True
return False
def previous_page(self) -> bool:
"""
Navigate to previous page of library.
Returns:
True if page changed, False if already on first page
"""
if self.current_page > 0:
self.current_page -= 1
return True
return False
def set_page(self, page: int) -> bool:
"""
Set current page.
Args:
page: Page number (0-indexed)
Returns:
True if page changed, False if invalid page
"""
total_pages = (len(self.books) + self.books_per_page - 1) // self.books_per_page
if 0 <= page < total_pages:
self.current_page = page
return True
return False
def get_total_pages(self) -> int:
"""
Get total number of pages.
Returns:
Total number of pages
"""
return (len(self.books) + self.books_per_page - 1) // self.books_per_page
def get_library_state(self) -> LibraryState:
"""
Get current library state for persistence.