677 lines
18 KiB
Python
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
|