dreader-application/dreader/html_generator.py
Duncan Tourolle 131d39080e
All checks were successful
Python CI / test (push) Successful in 3m24s
first application elements
2025-11-07 20:00:05 +01:00

677 lines
18 KiB
Python

"""
HTML generation functions for dreader UI.
Generates HTML strings programmatically for library view, reader view,
and various overlays (settings, TOC, etc.) that can be passed to a HAL
for rendering.
"""
from pathlib import Path
from typing import List, Dict, Optional
from dreader import create_ebook_reader
import base64
from io import BytesIO
def generate_library_html(books: List[Dict[str, str]]) -> str:
"""
Generate HTML for the library view showing all books in a grid.
Args:
books: List of book dictionaries with keys:
- title: Book title
- author: Book author
- filename: EPUB filename
- cover_data: Optional base64 encoded cover image
Returns:
Complete HTML string for library view
"""
books_html = []
for book in books:
cover_img = ''
if book.get('cover_data'):
cover_img = f'<img src="data:image/png;base64,{book["cover_data"]}" alt="Cover">'
else:
# Placeholder if no cover
cover_img = f'<div class="no-cover">{book["title"][:1]}</div>'
books_html.append(f'''
<td class="book-item" data-filename="{book['filename']}">
<table style="width: 100%;">
<tr>
<td class="cover-cell">
{cover_img}
</td>
</tr>
<tr>
<td class="title-cell">{book['title']}</td>
</tr>
<tr>
<td class="author-cell">{book['author']}</td>
</tr>
</table>
</td>
''')
# Arrange books in rows of 3
rows = []
for i in range(0, len(books_html), 3):
row_books = books_html[i:i+3]
# Pad with empty cells if needed
while len(row_books) < 3:
row_books.append('<td class="book-item empty"></td>')
rows.append(f'<tr>{"".join(row_books)}</tr>')
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Library</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: Arial, sans-serif;
background-color: #f5f5f5;
padding: 20px;
}}
.header {{
text-align: center;
padding: 20px;
background-color: #333;
color: white;
margin-bottom: 30px;
}}
.library-grid {{
width: 100%;
border-collapse: separate;
border-spacing: 20px;
}}
.book-item {{
background-color: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
cursor: pointer;
vertical-align: top;
width: 33%;
}}
.book-item:hover {{
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}}
.book-item.empty {{
background: none;
box-shadow: none;
cursor: default;
}}
.cover-cell {{
text-align: center;
padding-bottom: 10px;
}}
.cover-cell img {{
max-width: 200px;
max-height: 300px;
border: 1px solid #ddd;
}}
.no-cover {{
width: 200px;
height: 300px;
background-color: #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 72px;
color: #999;
margin: 0 auto;
}}
.title-cell {{
font-weight: bold;
font-size: 16px;
text-align: center;
padding: 5px;
}}
.author-cell {{
color: #666;
font-size: 14px;
text-align: center;
padding: 5px;
}}
</style>
</head>
<body>
<div class="header">
<h1>My Library</h1>
<p>{len(books)} books</p>
</div>
<table class="library-grid">
{"".join(rows)}
</table>
</body>
</html>
'''
return html
def generate_reader_html(book_title: str, book_author: str, page_image_data: str) -> str:
"""
Generate HTML for the reader view with page display.
Args:
book_title: Title of current book
book_author: Author of current book
page_image_data: Base64 encoded page image
Returns:
Complete HTML string for reader view (page layer only)
"""
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{book_title}</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: Arial, sans-serif;
background-color: #2c2c2c;
display: flex;
flex-direction: column;
height: 100vh;
}}
.header {{
background-color: #1a1a1a;
color: white;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}}
.book-info {{
flex: 1;
}}
.book-title {{
font-weight: bold;
font-size: 16px;
}}
.book-author {{
font-size: 12px;
color: #aaa;
}}
.header-buttons {{
display: flex;
gap: 10px;
}}
.header-button {{
background-color: #444;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}}
.header-button:hover {{
background-color: #555;
}}
.page-container {{
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}}
.page-image {{
max-width: 100%;
max-height: 100%;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}}
.footer {{
background-color: #1a1a1a;
color: white;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}}
.nav-button {{
background-color: #444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}}
.nav-button:hover {{
background-color: #555;
}}
</style>
</head>
<body>
<div class="header">
<div class="book-info">
<div class="book-title">{book_title}</div>
<div class="book-author">{book_author}</div>
</div>
<div class="header-buttons">
<button class="header-button" id="btn-library">Library</button>
<button class="header-button" id="btn-toc">Contents</button>
<button class="header-button" id="btn-settings">Settings</button>
</div>
</div>
<div class="page-container">
<img src="data:image/png;base64,{page_image_data}" alt="Page" class="page-image">
</div>
<div class="footer">
<button class="nav-button" id="btn-prev">← Previous</button>
<div id="page-info"></div>
<button class="nav-button" id="btn-next">Next →</button>
</div>
</body>
</html>
'''
return html
def generate_settings_overlay() -> str:
"""
Generate HTML for the settings overlay.
Returns:
HTML string for settings overlay
"""
html = '''
<!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>
<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>
</div>
</body>
</html>
'''
return html
def generate_toc_overlay(chapters: List[Dict]) -> str:
"""
Generate HTML for the table of contents overlay.
Args:
chapters: List of chapter dictionaries with keys:
- index: Chapter index
- title: Chapter title
Returns:
HTML string for TOC overlay
"""
chapter_rows = []
for chapter in chapters:
chapter_rows.append(f'''
<tr class="chapter-row" data-chapter-index="{chapter['index']}">
<td class="chapter-cell">{chapter['title']}</td>
</tr>
''')
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Table of Contents</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: 500px;
max-height: 80vh;
display: flex;
flex-direction: column;
}}
.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;
}}
.chapters-container {{
overflow-y: auto;
flex: 1;
}}
.chapters-table {{
width: 100%;
border-collapse: collapse;
}}
.chapter-row {{
cursor: pointer;
}}
.chapter-row:hover {{
background-color: #f0f0f0;
}}
.chapter-cell {{
padding: 12px;
border-bottom: 1px solid #eee;
}}
</style>
</head>
<body>
<div class="overlay-panel">
<div class="overlay-header">
<span class="overlay-title">Table of Contents</span>
<button class="close-button" id="btn-close">Close</button>
</div>
<div class="chapters-container">
<table class="chapters-table">
{"".join(chapter_rows)}
</table>
</div>
</div>
</body>
</html>
'''
return html
def generate_bookmarks_overlay(bookmarks: List[Dict]) -> str:
"""
Generate HTML for the bookmarks overlay.
Args:
bookmarks: List of bookmark dictionaries with keys:
- name: Bookmark name
- position: Position info
Returns:
HTML string for bookmarks overlay
"""
bookmark_rows = []
for bookmark in bookmarks:
bookmark_rows.append(f'''
<tr class="bookmark-row" data-bookmark-name="{bookmark['name']}">
<td class="bookmark-cell">
<div class="bookmark-name">{bookmark['name']}</div>
<div class="bookmark-position">{bookmark.get('position', '')}</div>
</td>
<td class="bookmark-actions">
<button class="action-button delete-button" data-bookmark="{bookmark['name']}">Delete</button>
</td>
</tr>
''')
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookmarks</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: 500px;
max-height: 80vh;
display: flex;
flex-direction: column;
}}
.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;
}}
.bookmarks-container {{
overflow-y: auto;
flex: 1;
}}
.bookmarks-table {{
width: 100%;
border-collapse: collapse;
}}
.bookmark-row {{
cursor: pointer;
}}
.bookmark-row:hover {{
background-color: #f0f0f0;
}}
.bookmark-cell {{
padding: 12px;
border-bottom: 1px solid #eee;
}}
.bookmark-name {{
font-weight: bold;
margin-bottom: 4px;
}}
.bookmark-position {{
font-size: 12px;
color: #666;
}}
.bookmark-actions {{
padding: 12px;
border-bottom: 1px solid #eee;
text-align: right;
width: 100px;
}}
.action-button {{
background-color: #dc3545;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}}
.action-button:hover {{
background-color: #c82333;
}}
</style>
</head>
<body>
<div class="overlay-panel">
<div class="overlay-header">
<span class="overlay-title">Bookmarks</span>
<button class="close-button" id="btn-close">Close</button>
</div>
<div class="bookmarks-container">
<table class="bookmarks-table">
{"".join(bookmark_rows)}
</table>
</div>
</div>
</body>
</html>
'''
return html