improved library screen, fixed issues with image rendering and navigation
This commit is contained in:
+214
-71
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user