ebk 0.3.1__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ebk might be problematic. Click here for more details.
- ebk/ai/__init__.py +23 -0
- ebk/ai/knowledge_graph.py +443 -0
- ebk/ai/llm_providers/__init__.py +21 -0
- ebk/ai/llm_providers/base.py +230 -0
- ebk/ai/llm_providers/ollama.py +362 -0
- ebk/ai/metadata_enrichment.py +396 -0
- ebk/ai/question_generator.py +328 -0
- ebk/ai/reading_companion.py +224 -0
- ebk/ai/semantic_search.py +434 -0
- ebk/ai/text_extractor.py +394 -0
- ebk/cli.py +1097 -9
- ebk/db/__init__.py +37 -0
- ebk/db/migrations.py +180 -0
- ebk/db/models.py +526 -0
- ebk/db/session.py +144 -0
- ebk/exports/__init__.py +0 -0
- ebk/exports/base_exporter.py +218 -0
- ebk/exports/html_library.py +1390 -0
- ebk/exports/html_utils.py +117 -0
- ebk/exports/hugo.py +59 -0
- ebk/exports/jinja_export.py +287 -0
- ebk/exports/multi_facet_export.py +164 -0
- ebk/exports/symlink_dag.py +479 -0
- ebk/exports/zip.py +25 -0
- ebk/library_db.py +155 -0
- ebk/repl/__init__.py +9 -0
- ebk/repl/find.py +126 -0
- ebk/repl/grep.py +174 -0
- ebk/repl/shell.py +1677 -0
- ebk/repl/text_utils.py +320 -0
- ebk/services/__init__.py +11 -0
- ebk/services/import_service.py +442 -0
- ebk/services/tag_service.py +282 -0
- ebk/services/text_extraction.py +317 -0
- ebk/similarity/__init__.py +77 -0
- ebk/similarity/base.py +154 -0
- ebk/similarity/core.py +445 -0
- ebk/similarity/extractors.py +168 -0
- ebk/similarity/metrics.py +376 -0
- ebk/vfs/__init__.py +101 -0
- ebk/vfs/base.py +301 -0
- ebk/vfs/library_vfs.py +124 -0
- ebk/vfs/nodes/__init__.py +54 -0
- ebk/vfs/nodes/authors.py +196 -0
- ebk/vfs/nodes/books.py +480 -0
- ebk/vfs/nodes/files.py +155 -0
- ebk/vfs/nodes/metadata.py +385 -0
- ebk/vfs/nodes/root.py +100 -0
- ebk/vfs/nodes/similar.py +165 -0
- ebk/vfs/nodes/subjects.py +184 -0
- ebk/vfs/nodes/tags.py +371 -0
- ebk/vfs/resolver.py +228 -0
- {ebk-0.3.1.dist-info → ebk-0.3.2.dist-info}/METADATA +1 -1
- ebk-0.3.2.dist-info/RECORD +69 -0
- ebk-0.3.2.dist-info/entry_points.txt +2 -0
- ebk-0.3.2.dist-info/top_level.txt +1 -0
- ebk-0.3.1.dist-info/RECORD +0 -19
- ebk-0.3.1.dist-info/entry_points.txt +0 -6
- ebk-0.3.1.dist-info/top_level.txt +0 -2
- {ebk-0.3.1.dist-info → ebk-0.3.2.dist-info}/WHEEL +0 -0
- {ebk-0.3.1.dist-info → ebk-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1390 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export library to a self-contained HTML5 file with embedded CSS and JavaScript.
|
|
3
|
+
|
|
4
|
+
Creates an interactive, searchable, filterable library catalog that works offline.
|
|
5
|
+
All metadata, including contributors, series, keywords, etc., is preserved.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
import json
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def export_to_html(books: List, output_path: Path, include_stats: bool = True, base_url: str = ""):
|
|
15
|
+
"""
|
|
16
|
+
Export library to a single self-contained HTML file.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
books: List of Book ORM objects
|
|
20
|
+
output_path: Path to write HTML file
|
|
21
|
+
include_stats: Include library statistics
|
|
22
|
+
base_url: Base URL for file links (e.g., '/library' or 'https://example.com/books')
|
|
23
|
+
If empty, uses relative paths from HTML file location
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Serialize books to JSON-compatible format
|
|
27
|
+
books_data = []
|
|
28
|
+
for book in books:
|
|
29
|
+
# Get primary cover if available
|
|
30
|
+
cover_path = None
|
|
31
|
+
if book.covers:
|
|
32
|
+
primary_cover = next((c for c in book.covers if c.is_primary), book.covers[0])
|
|
33
|
+
cover_path = primary_cover.path
|
|
34
|
+
|
|
35
|
+
book_data = {
|
|
36
|
+
'id': book.id,
|
|
37
|
+
'title': book.title,
|
|
38
|
+
'subtitle': book.subtitle,
|
|
39
|
+
'authors': [{'name': a.name, 'sort_name': a.sort_name} for a in book.authors],
|
|
40
|
+
'contributors': [
|
|
41
|
+
{'name': c.name, 'role': c.role, 'file_as': c.file_as}
|
|
42
|
+
for c in book.contributors
|
|
43
|
+
] if hasattr(book, 'contributors') else [],
|
|
44
|
+
'subjects': [s.name for s in book.subjects],
|
|
45
|
+
'language': book.language,
|
|
46
|
+
'publisher': book.publisher,
|
|
47
|
+
'publication_date': book.publication_date,
|
|
48
|
+
'series': book.series,
|
|
49
|
+
'series_index': book.series_index,
|
|
50
|
+
'edition': book.edition,
|
|
51
|
+
'description': book.description,
|
|
52
|
+
'page_count': book.page_count,
|
|
53
|
+
'word_count': book.word_count,
|
|
54
|
+
'keywords': book.keywords or [],
|
|
55
|
+
'rights': book.rights,
|
|
56
|
+
'identifiers': [
|
|
57
|
+
{'scheme': i.scheme, 'value': i.value}
|
|
58
|
+
for i in book.identifiers
|
|
59
|
+
],
|
|
60
|
+
'files': [
|
|
61
|
+
{
|
|
62
|
+
'format': f.format,
|
|
63
|
+
'size_bytes': f.size_bytes,
|
|
64
|
+
'mime_type': f.mime_type,
|
|
65
|
+
'creator_application': f.creator_application,
|
|
66
|
+
'created_date': f.created_date.isoformat() if f.created_date else None,
|
|
67
|
+
'modified_date': f.modified_date.isoformat() if f.modified_date else None,
|
|
68
|
+
'path': f.path, # Store relative path from library root
|
|
69
|
+
}
|
|
70
|
+
for f in book.files
|
|
71
|
+
],
|
|
72
|
+
'cover_path': cover_path,
|
|
73
|
+
'personal': {
|
|
74
|
+
'rating': book.personal.rating if book.personal else None,
|
|
75
|
+
'favorite': book.personal.favorite if book.personal else False,
|
|
76
|
+
'reading_status': book.personal.reading_status if book.personal else 'unread',
|
|
77
|
+
'tags': book.personal.personal_tags or [] if book.personal else [],
|
|
78
|
+
},
|
|
79
|
+
'created_at': book.created_at.isoformat(),
|
|
80
|
+
}
|
|
81
|
+
books_data.append(book_data)
|
|
82
|
+
|
|
83
|
+
# Generate statistics
|
|
84
|
+
stats = {}
|
|
85
|
+
if include_stats:
|
|
86
|
+
all_authors = set()
|
|
87
|
+
all_subjects = set()
|
|
88
|
+
all_languages = set()
|
|
89
|
+
all_formats = set()
|
|
90
|
+
series_count = 0
|
|
91
|
+
|
|
92
|
+
for book in books:
|
|
93
|
+
for author in book.authors:
|
|
94
|
+
all_authors.add(author.name)
|
|
95
|
+
for subject in book.subjects:
|
|
96
|
+
all_subjects.add(subject.name)
|
|
97
|
+
if book.language:
|
|
98
|
+
all_languages.add(book.language)
|
|
99
|
+
for file in book.files:
|
|
100
|
+
all_formats.add(file.format)
|
|
101
|
+
if book.series:
|
|
102
|
+
series_count += 1
|
|
103
|
+
|
|
104
|
+
stats = {
|
|
105
|
+
'total_books': len(books),
|
|
106
|
+
'total_authors': len(all_authors),
|
|
107
|
+
'total_subjects': len(all_subjects),
|
|
108
|
+
'languages': list(all_languages),
|
|
109
|
+
'formats': list(all_formats),
|
|
110
|
+
'books_in_series': series_count,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Create HTML content
|
|
114
|
+
html_content = _generate_html_template(books_data, stats, base_url)
|
|
115
|
+
|
|
116
|
+
# Write to file
|
|
117
|
+
output_path.write_text(html_content, encoding='utf-8')
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _generate_html_template(books_data: List[dict], stats: dict, base_url: str = "") -> str:
|
|
121
|
+
"""Generate the complete HTML template with embedded CSS and JavaScript."""
|
|
122
|
+
|
|
123
|
+
books_json = json.dumps(books_data, indent=2, ensure_ascii=False)
|
|
124
|
+
stats_json = json.dumps(stats, indent=2, ensure_ascii=False)
|
|
125
|
+
base_url_json = json.dumps(base_url, ensure_ascii=False)
|
|
126
|
+
export_date = datetime.now().isoformat()
|
|
127
|
+
|
|
128
|
+
return f'''<!DOCTYPE html>
|
|
129
|
+
<html lang="en">
|
|
130
|
+
<head>
|
|
131
|
+
<meta charset="UTF-8">
|
|
132
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
133
|
+
<title>eBook Library Catalog</title>
|
|
134
|
+
<style>
|
|
135
|
+
* {{
|
|
136
|
+
margin: 0;
|
|
137
|
+
padding: 0;
|
|
138
|
+
box-sizing: border-box;
|
|
139
|
+
}}
|
|
140
|
+
|
|
141
|
+
:root {{
|
|
142
|
+
--primary: #2563eb;
|
|
143
|
+
--primary-dark: #1e40af;
|
|
144
|
+
--secondary: #64748b;
|
|
145
|
+
--background: #f8fafc;
|
|
146
|
+
--surface: #ffffff;
|
|
147
|
+
--text: #1e293b;
|
|
148
|
+
--text-light: #64748b;
|
|
149
|
+
--border: #e2e8f0;
|
|
150
|
+
--success: #10b981;
|
|
151
|
+
--warning: #f59e0b;
|
|
152
|
+
--danger: #ef4444;
|
|
153
|
+
}}
|
|
154
|
+
|
|
155
|
+
body {{
|
|
156
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
157
|
+
background: var(--background);
|
|
158
|
+
color: var(--text);
|
|
159
|
+
line-height: 1.6;
|
|
160
|
+
}}
|
|
161
|
+
|
|
162
|
+
.container {{
|
|
163
|
+
max-width: 1400px;
|
|
164
|
+
margin: 0 auto;
|
|
165
|
+
padding: 20px;
|
|
166
|
+
}}
|
|
167
|
+
|
|
168
|
+
header {{
|
|
169
|
+
background: var(--surface);
|
|
170
|
+
border-bottom: 2px solid var(--border);
|
|
171
|
+
padding: 20px 0;
|
|
172
|
+
margin-bottom: 30px;
|
|
173
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
174
|
+
}}
|
|
175
|
+
|
|
176
|
+
h1 {{
|
|
177
|
+
font-size: 2rem;
|
|
178
|
+
font-weight: 700;
|
|
179
|
+
color: var(--primary);
|
|
180
|
+
margin-bottom: 10px;
|
|
181
|
+
}}
|
|
182
|
+
|
|
183
|
+
.stats {{
|
|
184
|
+
display: flex;
|
|
185
|
+
gap: 30px;
|
|
186
|
+
flex-wrap: wrap;
|
|
187
|
+
margin-top: 15px;
|
|
188
|
+
color: var(--text-light);
|
|
189
|
+
font-size: 0.9rem;
|
|
190
|
+
}}
|
|
191
|
+
|
|
192
|
+
.stat-item {{
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
gap: 5px;
|
|
196
|
+
}}
|
|
197
|
+
|
|
198
|
+
.stat-value {{
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
color: var(--text);
|
|
201
|
+
}}
|
|
202
|
+
|
|
203
|
+
.controls {{
|
|
204
|
+
background: var(--surface);
|
|
205
|
+
padding: 20px;
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
margin-bottom: 20px;
|
|
208
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
209
|
+
}}
|
|
210
|
+
|
|
211
|
+
.search-bar {{
|
|
212
|
+
width: 100%;
|
|
213
|
+
padding: 12px 16px;
|
|
214
|
+
font-size: 1rem;
|
|
215
|
+
border: 2px solid var(--border);
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
margin-bottom: 15px;
|
|
218
|
+
transition: border-color 0.2s;
|
|
219
|
+
}}
|
|
220
|
+
|
|
221
|
+
.search-bar:focus {{
|
|
222
|
+
outline: none;
|
|
223
|
+
border-color: var(--primary);
|
|
224
|
+
}}
|
|
225
|
+
|
|
226
|
+
.filters {{
|
|
227
|
+
display: flex;
|
|
228
|
+
gap: 15px;
|
|
229
|
+
flex-wrap: wrap;
|
|
230
|
+
}}
|
|
231
|
+
|
|
232
|
+
.filter-group {{
|
|
233
|
+
flex: 1;
|
|
234
|
+
min-width: 200px;
|
|
235
|
+
}}
|
|
236
|
+
|
|
237
|
+
.filter-group label {{
|
|
238
|
+
display: block;
|
|
239
|
+
font-size: 0.875rem;
|
|
240
|
+
font-weight: 600;
|
|
241
|
+
margin-bottom: 5px;
|
|
242
|
+
color: var(--text-light);
|
|
243
|
+
}}
|
|
244
|
+
|
|
245
|
+
.filter-group select {{
|
|
246
|
+
width: 100%;
|
|
247
|
+
padding: 8px 12px;
|
|
248
|
+
border: 1px solid var(--border);
|
|
249
|
+
border-radius: 6px;
|
|
250
|
+
font-size: 0.9rem;
|
|
251
|
+
background: white;
|
|
252
|
+
cursor: pointer;
|
|
253
|
+
}}
|
|
254
|
+
|
|
255
|
+
.book-grid {{
|
|
256
|
+
display: grid;
|
|
257
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
258
|
+
gap: 20px;
|
|
259
|
+
}}
|
|
260
|
+
|
|
261
|
+
.book-card {{
|
|
262
|
+
background: var(--surface);
|
|
263
|
+
border-radius: 8px;
|
|
264
|
+
padding: 20px;
|
|
265
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
266
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
}}
|
|
269
|
+
|
|
270
|
+
.book-card:hover {{
|
|
271
|
+
transform: translateY(-2px);
|
|
272
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.15);
|
|
273
|
+
}}
|
|
274
|
+
|
|
275
|
+
.book-header {{
|
|
276
|
+
display: flex;
|
|
277
|
+
justify-content: space-between;
|
|
278
|
+
align-items: flex-start;
|
|
279
|
+
margin-bottom: 12px;
|
|
280
|
+
}}
|
|
281
|
+
|
|
282
|
+
.book-title {{
|
|
283
|
+
font-size: 1.1rem;
|
|
284
|
+
font-weight: 600;
|
|
285
|
+
color: var(--text);
|
|
286
|
+
line-height: 1.3;
|
|
287
|
+
flex: 1;
|
|
288
|
+
}}
|
|
289
|
+
|
|
290
|
+
.favorite-badge {{
|
|
291
|
+
color: var(--warning);
|
|
292
|
+
font-size: 1.2rem;
|
|
293
|
+
margin-left: 8px;
|
|
294
|
+
}}
|
|
295
|
+
|
|
296
|
+
.book-subtitle {{
|
|
297
|
+
font-size: 0.9rem;
|
|
298
|
+
color: var(--text-light);
|
|
299
|
+
margin-bottom: 8px;
|
|
300
|
+
}}
|
|
301
|
+
|
|
302
|
+
.book-authors {{
|
|
303
|
+
font-size: 0.95rem;
|
|
304
|
+
color: var(--text-light);
|
|
305
|
+
margin-bottom: 8px;
|
|
306
|
+
}}
|
|
307
|
+
|
|
308
|
+
.book-meta {{
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-wrap: wrap;
|
|
311
|
+
gap: 8px;
|
|
312
|
+
margin-top: 12px;
|
|
313
|
+
}}
|
|
314
|
+
|
|
315
|
+
.badge {{
|
|
316
|
+
display: inline-block;
|
|
317
|
+
padding: 3px 8px;
|
|
318
|
+
border-radius: 4px;
|
|
319
|
+
font-size: 0.75rem;
|
|
320
|
+
font-weight: 600;
|
|
321
|
+
text-transform: uppercase;
|
|
322
|
+
}}
|
|
323
|
+
|
|
324
|
+
.badge-series {{
|
|
325
|
+
background: #dbeafe;
|
|
326
|
+
color: var(--primary);
|
|
327
|
+
}}
|
|
328
|
+
|
|
329
|
+
.badge-format {{
|
|
330
|
+
background: #f3f4f6;
|
|
331
|
+
color: var(--secondary);
|
|
332
|
+
}}
|
|
333
|
+
|
|
334
|
+
.badge-language {{
|
|
335
|
+
background: #fef3c7;
|
|
336
|
+
color: #92400e;
|
|
337
|
+
}}
|
|
338
|
+
|
|
339
|
+
.rating {{
|
|
340
|
+
color: var(--warning);
|
|
341
|
+
font-size: 0.9rem;
|
|
342
|
+
}}
|
|
343
|
+
|
|
344
|
+
.modal {{
|
|
345
|
+
display: none;
|
|
346
|
+
position: fixed;
|
|
347
|
+
top: 0;
|
|
348
|
+
left: 0;
|
|
349
|
+
right: 0;
|
|
350
|
+
bottom: 0;
|
|
351
|
+
background: rgba(0,0,0,0.6);
|
|
352
|
+
z-index: 1000;
|
|
353
|
+
padding: 20px;
|
|
354
|
+
overflow-y: auto;
|
|
355
|
+
}}
|
|
356
|
+
|
|
357
|
+
.modal.active {{
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
justify-content: center;
|
|
361
|
+
}}
|
|
362
|
+
|
|
363
|
+
.modal-content {{
|
|
364
|
+
background: var(--surface);
|
|
365
|
+
border-radius: 12px;
|
|
366
|
+
max-width: 800px;
|
|
367
|
+
width: 100%;
|
|
368
|
+
max-height: 90vh;
|
|
369
|
+
overflow-y: auto;
|
|
370
|
+
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
|
|
371
|
+
}}
|
|
372
|
+
|
|
373
|
+
.modal-header {{
|
|
374
|
+
padding: 24px;
|
|
375
|
+
border-bottom: 1px solid var(--border);
|
|
376
|
+
display: flex;
|
|
377
|
+
justify-content: space-between;
|
|
378
|
+
align-items: flex-start;
|
|
379
|
+
}}
|
|
380
|
+
|
|
381
|
+
.modal-title {{
|
|
382
|
+
font-size: 1.5rem;
|
|
383
|
+
font-weight: 700;
|
|
384
|
+
color: var(--text);
|
|
385
|
+
flex: 1;
|
|
386
|
+
}}
|
|
387
|
+
|
|
388
|
+
.close-btn {{
|
|
389
|
+
background: none;
|
|
390
|
+
border: none;
|
|
391
|
+
font-size: 2rem;
|
|
392
|
+
color: var(--text-light);
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
padding: 0;
|
|
395
|
+
width: 30px;
|
|
396
|
+
height: 30px;
|
|
397
|
+
line-height: 1;
|
|
398
|
+
}}
|
|
399
|
+
|
|
400
|
+
.close-btn:hover {{
|
|
401
|
+
color: var(--text);
|
|
402
|
+
}}
|
|
403
|
+
|
|
404
|
+
.modal-body {{
|
|
405
|
+
padding: 24px;
|
|
406
|
+
}}
|
|
407
|
+
|
|
408
|
+
.detail-section {{
|
|
409
|
+
margin-bottom: 24px;
|
|
410
|
+
}}
|
|
411
|
+
|
|
412
|
+
.detail-label {{
|
|
413
|
+
font-weight: 600;
|
|
414
|
+
color: var(--text-light);
|
|
415
|
+
font-size: 0.875rem;
|
|
416
|
+
text-transform: uppercase;
|
|
417
|
+
letter-spacing: 0.05em;
|
|
418
|
+
margin-bottom: 8px;
|
|
419
|
+
}}
|
|
420
|
+
|
|
421
|
+
.detail-value {{
|
|
422
|
+
color: var(--text);
|
|
423
|
+
margin-bottom: 8px;
|
|
424
|
+
}}
|
|
425
|
+
|
|
426
|
+
.contributors-list, .identifiers-list {{
|
|
427
|
+
list-style: none;
|
|
428
|
+
padding: 0;
|
|
429
|
+
}}
|
|
430
|
+
|
|
431
|
+
.contributors-list li, .identifiers-list li {{
|
|
432
|
+
padding: 4px 0;
|
|
433
|
+
color: var(--text);
|
|
434
|
+
}}
|
|
435
|
+
|
|
436
|
+
.contributor-role {{
|
|
437
|
+
color: var(--text-light);
|
|
438
|
+
font-size: 0.875rem;
|
|
439
|
+
margin-left: 8px;
|
|
440
|
+
}}
|
|
441
|
+
|
|
442
|
+
.file-link {{
|
|
443
|
+
color: var(--primary);
|
|
444
|
+
text-decoration: none;
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
transition: color 0.2s;
|
|
447
|
+
}}
|
|
448
|
+
|
|
449
|
+
.file-link:hover {{
|
|
450
|
+
color: var(--primary-dark);
|
|
451
|
+
text-decoration: underline;
|
|
452
|
+
}}
|
|
453
|
+
|
|
454
|
+
.tags {{
|
|
455
|
+
display: flex;
|
|
456
|
+
flex-wrap: wrap;
|
|
457
|
+
gap: 6px;
|
|
458
|
+
}}
|
|
459
|
+
|
|
460
|
+
.tag {{
|
|
461
|
+
background: #e0e7ff;
|
|
462
|
+
color: var(--primary);
|
|
463
|
+
padding: 4px 10px;
|
|
464
|
+
border-radius: 12px;
|
|
465
|
+
font-size: 0.8rem;
|
|
466
|
+
}}
|
|
467
|
+
|
|
468
|
+
.no-results {{
|
|
469
|
+
text-align: center;
|
|
470
|
+
padding: 60px 20px;
|
|
471
|
+
color: var(--text-light);
|
|
472
|
+
}}
|
|
473
|
+
|
|
474
|
+
.no-results-icon {{
|
|
475
|
+
font-size: 4rem;
|
|
476
|
+
margin-bottom: 16px;
|
|
477
|
+
}}
|
|
478
|
+
|
|
479
|
+
@media (max-width: 768px) {{
|
|
480
|
+
.book-grid {{
|
|
481
|
+
grid-template-columns: 1fr;
|
|
482
|
+
}}
|
|
483
|
+
|
|
484
|
+
.filters {{
|
|
485
|
+
flex-direction: column;
|
|
486
|
+
}}
|
|
487
|
+
|
|
488
|
+
.stats {{
|
|
489
|
+
flex-direction: column;
|
|
490
|
+
gap: 8px;
|
|
491
|
+
}}
|
|
492
|
+
}}
|
|
493
|
+
|
|
494
|
+
code {{
|
|
495
|
+
background: rgba(37, 99, 235, 0.1);
|
|
496
|
+
padding: 2px 6px;
|
|
497
|
+
border-radius: 4px;
|
|
498
|
+
font-family: 'Courier New', monospace;
|
|
499
|
+
font-size: 0.9em;
|
|
500
|
+
color: var(--primary);
|
|
501
|
+
}}
|
|
502
|
+
|
|
503
|
+
#search-help ul {{
|
|
504
|
+
list-style-type: none;
|
|
505
|
+
}}
|
|
506
|
+
|
|
507
|
+
#search-help li {{
|
|
508
|
+
padding: 3px 0;
|
|
509
|
+
}}
|
|
510
|
+
</style>
|
|
511
|
+
</head>
|
|
512
|
+
<body>
|
|
513
|
+
<header>
|
|
514
|
+
<div class="container">
|
|
515
|
+
<h1>📚 eBook Library</h1>
|
|
516
|
+
<div class="stats">
|
|
517
|
+
<div class="stat-item">
|
|
518
|
+
<span>Total Books:</span>
|
|
519
|
+
<span class="stat-value" id="total-books">0</span>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="stat-item">
|
|
522
|
+
<span>Authors:</span>
|
|
523
|
+
<span class="stat-value" id="total-authors">0</span>
|
|
524
|
+
</div>
|
|
525
|
+
<div class="stat-item">
|
|
526
|
+
<span>Subjects:</span>
|
|
527
|
+
<span class="stat-value" id="total-subjects">0</span>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="stat-item">
|
|
530
|
+
<span>Exported:</span>
|
|
531
|
+
<span class="stat-value">{export_date[:10]}</span>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</header>
|
|
536
|
+
|
|
537
|
+
<div class="container">
|
|
538
|
+
<div class="controls">
|
|
539
|
+
<div style="position: relative; display: flex; align-items: center; gap: 8px;">
|
|
540
|
+
<input
|
|
541
|
+
type="text"
|
|
542
|
+
class="search-bar"
|
|
543
|
+
id="search-input"
|
|
544
|
+
placeholder="Search books... (try: title:Python rating:>=4)"
|
|
545
|
+
style="flex: 1;"
|
|
546
|
+
>
|
|
547
|
+
<button
|
|
548
|
+
onclick="toggleSearchHelp()"
|
|
549
|
+
style="background: var(--secondary); color: white; border: none; border-radius: 6px; width: 36px; height: 36px; cursor: pointer; font-size: 1.2rem; display: flex; align-items: center; justify-content: center;"
|
|
550
|
+
title="Search Help"
|
|
551
|
+
>
|
|
552
|
+
?
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<div id="search-help" style="display: none; margin-top: 10px; padding: 15px; background: var(--card-bg); border-radius: 8px; border: 2px solid var(--primary); font-size: 0.875rem;">
|
|
557
|
+
<h4 style="margin-top: 0; color: var(--primary);">Advanced Search Syntax</h4>
|
|
558
|
+
|
|
559
|
+
<p style="margin: 5px 0;"><strong>Field Searches:</strong></p>
|
|
560
|
+
<ul style="margin: 5px 0 10px 20px; padding: 0;">
|
|
561
|
+
<li><code>title:Python</code> - Search in title only</li>
|
|
562
|
+
<li><code>author:Knuth</code> - Search author name</li>
|
|
563
|
+
<li><code>tag:programming</code> - Search subjects/tags</li>
|
|
564
|
+
<li><code>description:algorithms</code> - Search description</li>
|
|
565
|
+
<li><code>series:TAOCP</code> - Search series name</li>
|
|
566
|
+
</ul>
|
|
567
|
+
|
|
568
|
+
<p style="margin: 5px 0;"><strong>Filters:</strong></p>
|
|
569
|
+
<ul style="margin: 5px 0 10px 20px; padding: 0;">
|
|
570
|
+
<li><code>language:en</code> - Language code</li>
|
|
571
|
+
<li><code>format:pdf</code> - File format</li>
|
|
572
|
+
<li><code>rating:5</code> or <code>rating:>=4</code> - Rating filter</li>
|
|
573
|
+
<li><code>favorite:true</code> - Favorites only</li>
|
|
574
|
+
<li><code>status:reading</code> - Reading status</li>
|
|
575
|
+
</ul>
|
|
576
|
+
|
|
577
|
+
<p style="margin: 5px 0;"><strong>Boolean Logic:</strong></p>
|
|
578
|
+
<ul style="margin: 5px 0 10px 20px; padding: 0;">
|
|
579
|
+
<li><code>python java</code> - Both terms (implicit AND)</li>
|
|
580
|
+
<li><code>python OR java</code> - Either term</li>
|
|
581
|
+
<li><code>NOT java</code> or <code>-java</code> - Exclude term</li>
|
|
582
|
+
</ul>
|
|
583
|
+
|
|
584
|
+
<p style="margin: 5px 0;"><strong>Quotes:</strong></p>
|
|
585
|
+
<ul style="margin: 5px 0 10px 20px; padding: 0;">
|
|
586
|
+
<li><code>"machine learning"</code> - Exact phrase</li>
|
|
587
|
+
<li><code>author:"Donald Knuth"</code> - Field with phrase</li>
|
|
588
|
+
</ul>
|
|
589
|
+
|
|
590
|
+
<p style="margin: 5px 0;"><strong>Examples:</strong></p>
|
|
591
|
+
<ul style="margin: 5px 0 0 20px; padding: 0;">
|
|
592
|
+
<li><code>title:Python rating:>=4 format:pdf</code></li>
|
|
593
|
+
<li><code>author:"Donald Knuth" series:TAOCP</code></li>
|
|
594
|
+
<li><code>tag:programming favorite:true NOT java</code></li>
|
|
595
|
+
</ul>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<div class="filters">
|
|
599
|
+
<div class="filter-group">
|
|
600
|
+
<label for="sort-field">Sort By</label>
|
|
601
|
+
<select id="sort-field">
|
|
602
|
+
<option value="title">Title</option>
|
|
603
|
+
<option value="created_at">Date Added</option>
|
|
604
|
+
<option value="publication_date">Publication Date</option>
|
|
605
|
+
<option value="rating">Rating</option>
|
|
606
|
+
</select>
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
<div class="filter-group">
|
|
610
|
+
<label for="sort-order">Order</label>
|
|
611
|
+
<select id="sort-order">
|
|
612
|
+
<option value="asc">Ascending</option>
|
|
613
|
+
<option value="desc">Descending</option>
|
|
614
|
+
</select>
|
|
615
|
+
</div>
|
|
616
|
+
|
|
617
|
+
<div class="filter-group">
|
|
618
|
+
<label for="language-filter">Language</label>
|
|
619
|
+
<select id="language-filter">
|
|
620
|
+
<option value="">All Languages</option>
|
|
621
|
+
</select>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<div class="filter-group">
|
|
625
|
+
<label for="format-filter">Format</label>
|
|
626
|
+
<select id="format-filter">
|
|
627
|
+
<option value="">All Formats</option>
|
|
628
|
+
</select>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<div class="filter-group">
|
|
632
|
+
<label for="series-filter">Series</label>
|
|
633
|
+
<select id="series-filter">
|
|
634
|
+
<option value="">All Books</option>
|
|
635
|
+
<option value="has-series">In Series</option>
|
|
636
|
+
<option value="no-series">Not in Series</option>
|
|
637
|
+
</select>
|
|
638
|
+
</div>
|
|
639
|
+
|
|
640
|
+
<div class="filter-group">
|
|
641
|
+
<label for="favorite-filter">Favorites</label>
|
|
642
|
+
<select id="favorite-filter">
|
|
643
|
+
<option value="">All Books</option>
|
|
644
|
+
<option value="true">Favorites Only</option>
|
|
645
|
+
</select>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<div class="filter-group">
|
|
649
|
+
<label for="rating-filter">Min Rating</label>
|
|
650
|
+
<select id="rating-filter">
|
|
651
|
+
<option value="">Any Rating</option>
|
|
652
|
+
<option value="1">1+ Stars</option>
|
|
653
|
+
<option value="2">2+ Stars</option>
|
|
654
|
+
<option value="3">3+ Stars</option>
|
|
655
|
+
<option value="4">4+ Stars</option>
|
|
656
|
+
<option value="5">5 Stars</option>
|
|
657
|
+
</select>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
<button onclick="clearFilters()" style="margin-top: 10px; padding: 8px 16px; background: var(--secondary); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem;">Clear Filters</button>
|
|
661
|
+
</div>
|
|
662
|
+
|
|
663
|
+
<div id="results-info" style="margin-bottom: 15px; color: var(--text-light); font-size: 0.9rem;"></div>
|
|
664
|
+
|
|
665
|
+
<div class="book-grid" id="book-grid"></div>
|
|
666
|
+
|
|
667
|
+
<div class="no-results" id="no-results" style="display: none;">
|
|
668
|
+
<div class="no-results-icon">🔍</div>
|
|
669
|
+
<h2>No books found</h2>
|
|
670
|
+
<p>Try adjusting your search or filters</p>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<div id="pagination" style="display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 30px; flex-wrap: wrap;"></div>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<div class="modal" id="book-modal">
|
|
677
|
+
<div class="modal-content">
|
|
678
|
+
<div class="modal-header">
|
|
679
|
+
<h2 class="modal-title" id="modal-title"></h2>
|
|
680
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
681
|
+
<button class="btn-primary" onclick="toggleEditMode()" id="edit-mode-btn" style="padding: 6px 12px; font-size: 0.875rem;">Edit Metadata</button>
|
|
682
|
+
<button class="close-btn" onclick="closeModal()">×</button>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
<div class="modal-body" id="modal-body"></div>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<script>
|
|
690
|
+
// Embedded data
|
|
691
|
+
const BOOKS = {books_json};
|
|
692
|
+
const STATS = {stats_json};
|
|
693
|
+
const BASE_URL = {base_url_json};
|
|
694
|
+
|
|
695
|
+
// State
|
|
696
|
+
let filteredBooks = [...BOOKS];
|
|
697
|
+
let editMode = false;
|
|
698
|
+
let currentBookId = null;
|
|
699
|
+
let currentPage = 1;
|
|
700
|
+
const booksPerPage = 50;
|
|
701
|
+
|
|
702
|
+
// Initialize
|
|
703
|
+
document.addEventListener('DOMContentLoaded', () => {{
|
|
704
|
+
populateStats();
|
|
705
|
+
populateFilters();
|
|
706
|
+
restoreStateFromURL();
|
|
707
|
+
applyFilters();
|
|
708
|
+
setupEventListeners();
|
|
709
|
+
|
|
710
|
+
// Handle browser back/forward
|
|
711
|
+
window.addEventListener('popstate', () => {{
|
|
712
|
+
restoreStateFromURL();
|
|
713
|
+
applyFilters();
|
|
714
|
+
}});
|
|
715
|
+
}});
|
|
716
|
+
|
|
717
|
+
function populateStats() {{
|
|
718
|
+
document.getElementById('total-books').textContent = STATS.total_books;
|
|
719
|
+
document.getElementById('total-authors').textContent = STATS.total_authors;
|
|
720
|
+
document.getElementById('total-subjects').textContent = STATS.total_subjects;
|
|
721
|
+
}}
|
|
722
|
+
|
|
723
|
+
function populateFilters() {{
|
|
724
|
+
// Languages
|
|
725
|
+
const languageSelect = document.getElementById('language-filter');
|
|
726
|
+
STATS.languages.forEach(lang => {{
|
|
727
|
+
const option = document.createElement('option');
|
|
728
|
+
option.value = lang;
|
|
729
|
+
option.textContent = lang.toUpperCase();
|
|
730
|
+
languageSelect.appendChild(option);
|
|
731
|
+
}});
|
|
732
|
+
|
|
733
|
+
// Formats
|
|
734
|
+
const formatSelect = document.getElementById('format-filter');
|
|
735
|
+
STATS.formats.forEach(fmt => {{
|
|
736
|
+
const option = document.createElement('option');
|
|
737
|
+
option.value = fmt;
|
|
738
|
+
option.textContent = fmt.toUpperCase();
|
|
739
|
+
formatSelect.appendChild(option);
|
|
740
|
+
}});
|
|
741
|
+
}}
|
|
742
|
+
|
|
743
|
+
// localStorage helpers
|
|
744
|
+
function loadMetadataOverrides() {{
|
|
745
|
+
const overrides = localStorage.getItem('ebk_metadata_overrides');
|
|
746
|
+
return overrides ? JSON.parse(overrides) : {{}};
|
|
747
|
+
}}
|
|
748
|
+
|
|
749
|
+
function saveMetadataOverride(bookId, field, value) {{
|
|
750
|
+
const overrides = loadMetadataOverrides();
|
|
751
|
+
if (!overrides[bookId]) overrides[bookId] = {{}};
|
|
752
|
+
overrides[bookId][field] = value;
|
|
753
|
+
localStorage.setItem('ebk_metadata_overrides', JSON.stringify(overrides));
|
|
754
|
+
}}
|
|
755
|
+
|
|
756
|
+
function applyMetadataOverrides(book) {{
|
|
757
|
+
const overrides = loadMetadataOverrides();
|
|
758
|
+
if (overrides[book.id]) {{
|
|
759
|
+
return {{ ...book, ...overrides[book.id] }};
|
|
760
|
+
}}
|
|
761
|
+
return book;
|
|
762
|
+
}}
|
|
763
|
+
|
|
764
|
+
// URL State Management
|
|
765
|
+
function updateURL() {{
|
|
766
|
+
const params = new URLSearchParams();
|
|
767
|
+
|
|
768
|
+
if (currentPage > 1) params.set('page', currentPage);
|
|
769
|
+
|
|
770
|
+
const searchQuery = document.getElementById('search-input').value;
|
|
771
|
+
if (searchQuery) params.set('q', searchQuery);
|
|
772
|
+
|
|
773
|
+
const language = document.getElementById('language-filter').value;
|
|
774
|
+
if (language) params.set('language', language);
|
|
775
|
+
|
|
776
|
+
const format = document.getElementById('format-filter').value;
|
|
777
|
+
if (format) params.set('format', format);
|
|
778
|
+
|
|
779
|
+
const series = document.getElementById('series-filter').value;
|
|
780
|
+
if (series) params.set('series', series);
|
|
781
|
+
|
|
782
|
+
const favorite = document.getElementById('favorite-filter').value;
|
|
783
|
+
if (favorite) params.set('favorite', favorite);
|
|
784
|
+
|
|
785
|
+
const rating = document.getElementById('rating-filter').value;
|
|
786
|
+
if (rating) params.set('rating', rating);
|
|
787
|
+
|
|
788
|
+
const sortField = document.getElementById('sort-field').value;
|
|
789
|
+
if (sortField !== 'title') params.set('sort', sortField);
|
|
790
|
+
|
|
791
|
+
const sortOrder = document.getElementById('sort-order').value;
|
|
792
|
+
if (sortOrder !== 'asc') params.set('order', sortOrder);
|
|
793
|
+
|
|
794
|
+
const newURL = params.toString() ? `?${{params.toString()}}` : window.location.pathname;
|
|
795
|
+
window.history.pushState({{}}, '', newURL);
|
|
796
|
+
}}
|
|
797
|
+
|
|
798
|
+
function restoreStateFromURL() {{
|
|
799
|
+
const params = new URLSearchParams(window.location.search);
|
|
800
|
+
|
|
801
|
+
currentPage = parseInt(params.get('page')) || 1;
|
|
802
|
+
|
|
803
|
+
document.getElementById('search-input').value = params.get('q') || '';
|
|
804
|
+
document.getElementById('language-filter').value = params.get('language') || '';
|
|
805
|
+
document.getElementById('format-filter').value = params.get('format') || '';
|
|
806
|
+
document.getElementById('series-filter').value = params.get('series') || '';
|
|
807
|
+
document.getElementById('favorite-filter').value = params.get('favorite') || '';
|
|
808
|
+
document.getElementById('rating-filter').value = params.get('rating') || '';
|
|
809
|
+
document.getElementById('sort-field').value = params.get('sort') || 'title';
|
|
810
|
+
document.getElementById('sort-order').value = params.get('order') || 'asc';
|
|
811
|
+
}}
|
|
812
|
+
|
|
813
|
+
function applyFilters() {{
|
|
814
|
+
filterBooks();
|
|
815
|
+
updateURL();
|
|
816
|
+
}}
|
|
817
|
+
|
|
818
|
+
function setupEventListeners() {{
|
|
819
|
+
document.getElementById('search-input').addEventListener('input', () => {{
|
|
820
|
+
currentPage = 1; // Reset to page 1 on new search
|
|
821
|
+
applyFilters();
|
|
822
|
+
}});
|
|
823
|
+
document.getElementById('language-filter').addEventListener('change', () => {{
|
|
824
|
+
currentPage = 1;
|
|
825
|
+
applyFilters();
|
|
826
|
+
}});
|
|
827
|
+
document.getElementById('format-filter').addEventListener('change', () => {{
|
|
828
|
+
currentPage = 1;
|
|
829
|
+
applyFilters();
|
|
830
|
+
}});
|
|
831
|
+
document.getElementById('series-filter').addEventListener('change', () => {{
|
|
832
|
+
currentPage = 1;
|
|
833
|
+
applyFilters();
|
|
834
|
+
}});
|
|
835
|
+
document.getElementById('favorite-filter').addEventListener('change', () => {{
|
|
836
|
+
currentPage = 1;
|
|
837
|
+
applyFilters();
|
|
838
|
+
}});
|
|
839
|
+
document.getElementById('rating-filter').addEventListener('change', () => {{
|
|
840
|
+
currentPage = 1;
|
|
841
|
+
applyFilters();
|
|
842
|
+
}});
|
|
843
|
+
document.getElementById('sort-field').addEventListener('change', applyFilters);
|
|
844
|
+
document.getElementById('sort-order').addEventListener('change', applyFilters);
|
|
845
|
+
|
|
846
|
+
// Close modal on outside click
|
|
847
|
+
document.getElementById('book-modal').addEventListener('click', (e) => {{
|
|
848
|
+
if (e.target.id === 'book-modal') {{
|
|
849
|
+
closeModal();
|
|
850
|
+
}}
|
|
851
|
+
}});
|
|
852
|
+
}}
|
|
853
|
+
|
|
854
|
+
function filterBooks() {{
|
|
855
|
+
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
|
856
|
+
const languageFilter = document.getElementById('language-filter').value;
|
|
857
|
+
const formatFilter = document.getElementById('format-filter').value;
|
|
858
|
+
const seriesFilter = document.getElementById('series-filter').value;
|
|
859
|
+
const favoriteFilter = document.getElementById('favorite-filter').value;
|
|
860
|
+
const ratingFilter = document.getElementById('rating-filter').value;
|
|
861
|
+
const sortField = document.getElementById('sort-field').value;
|
|
862
|
+
const sortOrder = document.getElementById('sort-order').value;
|
|
863
|
+
|
|
864
|
+
// Apply metadata overrides and filter
|
|
865
|
+
filteredBooks = BOOKS.map(applyMetadataOverrides).filter(book => {{
|
|
866
|
+
// Search
|
|
867
|
+
if (searchTerm) {{
|
|
868
|
+
const searchable = [
|
|
869
|
+
book.title,
|
|
870
|
+
book.subtitle,
|
|
871
|
+
...book.authors.map(a => a.name),
|
|
872
|
+
...book.subjects,
|
|
873
|
+
...book.keywords,
|
|
874
|
+
book.description
|
|
875
|
+
].filter(x => x).join(' ').toLowerCase();
|
|
876
|
+
|
|
877
|
+
if (!searchable.includes(searchTerm)) return false;
|
|
878
|
+
}}
|
|
879
|
+
|
|
880
|
+
// Language
|
|
881
|
+
if (languageFilter && book.language !== languageFilter) return false;
|
|
882
|
+
|
|
883
|
+
// Format
|
|
884
|
+
if (formatFilter && !book.files.some(f => f.format === formatFilter)) return false;
|
|
885
|
+
|
|
886
|
+
// Series
|
|
887
|
+
if (seriesFilter === 'has-series' && !book.series) return false;
|
|
888
|
+
if (seriesFilter === 'no-series' && book.series) return false;
|
|
889
|
+
|
|
890
|
+
// Favorite
|
|
891
|
+
if (favoriteFilter === 'true' && !book.personal.favorite) return false;
|
|
892
|
+
|
|
893
|
+
// Rating
|
|
894
|
+
if (ratingFilter && (!book.personal.rating || book.personal.rating < parseFloat(ratingFilter))) return false;
|
|
895
|
+
|
|
896
|
+
return true;
|
|
897
|
+
}});
|
|
898
|
+
|
|
899
|
+
// Sort
|
|
900
|
+
filteredBooks.sort((a, b) => {{
|
|
901
|
+
let aVal, bVal;
|
|
902
|
+
|
|
903
|
+
switch(sortField) {{
|
|
904
|
+
case 'title':
|
|
905
|
+
aVal = (a.title || '').toLowerCase();
|
|
906
|
+
bVal = (b.title || '').toLowerCase();
|
|
907
|
+
break;
|
|
908
|
+
case 'created_at':
|
|
909
|
+
aVal = new Date(a.created_at || 0);
|
|
910
|
+
bVal = new Date(b.created_at || 0);
|
|
911
|
+
break;
|
|
912
|
+
case 'publication_date':
|
|
913
|
+
aVal = new Date(a.publication_date || 0);
|
|
914
|
+
bVal = new Date(b.publication_date || 0);
|
|
915
|
+
break;
|
|
916
|
+
case 'rating':
|
|
917
|
+
aVal = a.personal?.rating || 0;
|
|
918
|
+
bVal = b.personal?.rating || 0;
|
|
919
|
+
break;
|
|
920
|
+
default:
|
|
921
|
+
return 0;
|
|
922
|
+
}}
|
|
923
|
+
|
|
924
|
+
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
|
|
925
|
+
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
|
|
926
|
+
return 0;
|
|
927
|
+
}});
|
|
928
|
+
|
|
929
|
+
renderBooks();
|
|
930
|
+
}}
|
|
931
|
+
|
|
932
|
+
function clearFilters() {{
|
|
933
|
+
document.getElementById('search-input').value = '';
|
|
934
|
+
document.getElementById('language-filter').value = '';
|
|
935
|
+
document.getElementById('format-filter').value = '';
|
|
936
|
+
document.getElementById('series-filter').value = '';
|
|
937
|
+
document.getElementById('favorite-filter').value = '';
|
|
938
|
+
document.getElementById('rating-filter').value = '';
|
|
939
|
+
document.getElementById('sort-field').value = 'title';
|
|
940
|
+
document.getElementById('sort-order').value = 'asc';
|
|
941
|
+
filterBooks();
|
|
942
|
+
}}
|
|
943
|
+
|
|
944
|
+
function renderBooks() {{
|
|
945
|
+
const grid = document.getElementById('book-grid');
|
|
946
|
+
const noResults = document.getElementById('no-results');
|
|
947
|
+
const resultsInfo = document.getElementById('results-info');
|
|
948
|
+
const pagination = document.getElementById('pagination');
|
|
949
|
+
|
|
950
|
+
if (filteredBooks.length === 0) {{
|
|
951
|
+
grid.style.display = 'none';
|
|
952
|
+
noResults.style.display = 'block';
|
|
953
|
+
resultsInfo.style.display = 'none';
|
|
954
|
+
pagination.style.display = 'none';
|
|
955
|
+
return;
|
|
956
|
+
}}
|
|
957
|
+
|
|
958
|
+
grid.style.display = 'grid';
|
|
959
|
+
noResults.style.display = 'none';
|
|
960
|
+
resultsInfo.style.display = 'block';
|
|
961
|
+
|
|
962
|
+
// Pagination
|
|
963
|
+
const totalPages = Math.ceil(filteredBooks.length / booksPerPage);
|
|
964
|
+
const startIdx = (currentPage - 1) * booksPerPage;
|
|
965
|
+
const endIdx = Math.min(startIdx + booksPerPage, filteredBooks.length);
|
|
966
|
+
const booksToShow = filteredBooks.slice(startIdx, endIdx);
|
|
967
|
+
|
|
968
|
+
// Update results info
|
|
969
|
+
resultsInfo.textContent = `Showing ${{startIdx + 1}}-${{endIdx}} of ${{filteredBooks.length}} books`;
|
|
970
|
+
|
|
971
|
+
grid.innerHTML = booksToShow.map(book => `
|
|
972
|
+
<div class="book-card" onclick="showBookDetails(${{book.id}})">
|
|
973
|
+
${{book.cover_path ? `
|
|
974
|
+
<div style="text-align: center; margin-bottom: 12px;">
|
|
975
|
+
<img src="${{BASE_URL ? BASE_URL + '/' + book.cover_path : book.cover_path}}"
|
|
976
|
+
alt="Cover"
|
|
977
|
+
style="max-width: 100%; max-height: 220px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"
|
|
978
|
+
onerror="this.style.display='none'">
|
|
979
|
+
</div>
|
|
980
|
+
` : ''}}
|
|
981
|
+
<div class="book-header">
|
|
982
|
+
<div class="book-title">
|
|
983
|
+
${{escapeHtml(book.title)}}
|
|
984
|
+
${{book.personal.favorite ? '<span class="favorite-badge">⭐</span>' : ''}}
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
${{book.subtitle ? `<div class="book-subtitle">${{escapeHtml(book.subtitle)}}</div>` : ''}}
|
|
988
|
+
<div class="book-authors">
|
|
989
|
+
${{book.authors.map(a => a.name).join(', ') || 'Unknown Author'}}
|
|
990
|
+
</div>
|
|
991
|
+
${{book.publication_date ? `<div style="color: #6b7280; font-size: 0.875rem; margin-top: 4px;">📅 ${{book.publication_date}}</div>` : ''}}
|
|
992
|
+
${{book.personal.rating ? `<div class="rating">${{'★'.repeat(Math.round(book.personal.rating))}} ${{book.personal.rating}}</div>` : ''}}
|
|
993
|
+
<div class="book-meta">
|
|
994
|
+
${{book.series ? `<span class="badge badge-series">${{escapeHtml(book.series)}} #${{book.series_index}}</span>` : ''}}
|
|
995
|
+
${{book.files.map(f => `<span class="badge badge-format">${{f.format.toUpperCase()}}</span>`).join('')}}
|
|
996
|
+
${{book.language ? `<span class="badge badge-language">${{book.language.toUpperCase()}}</span>` : ''}}
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
`).join('');
|
|
1000
|
+
|
|
1001
|
+
// Render pagination controls
|
|
1002
|
+
renderPagination(totalPages);
|
|
1003
|
+
}}
|
|
1004
|
+
|
|
1005
|
+
function renderPagination(totalPages) {{
|
|
1006
|
+
const pagination = document.getElementById('pagination');
|
|
1007
|
+
|
|
1008
|
+
if (totalPages <= 1) {{
|
|
1009
|
+
pagination.style.display = 'none';
|
|
1010
|
+
return;
|
|
1011
|
+
}}
|
|
1012
|
+
|
|
1013
|
+
pagination.style.display = 'flex';
|
|
1014
|
+
|
|
1015
|
+
let html = '';
|
|
1016
|
+
|
|
1017
|
+
// Previous button
|
|
1018
|
+
html += `<button onclick="goToPage(${{currentPage - 1}})" ${{currentPage === 1 ? 'disabled' : ''}}
|
|
1019
|
+
style="padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; ${{currentPage === 1 ? 'opacity: 0.5; cursor: not-allowed;' : ''}}">
|
|
1020
|
+
← Previous
|
|
1021
|
+
</button>`;
|
|
1022
|
+
|
|
1023
|
+
// Page numbers
|
|
1024
|
+
const maxButtons = 7;
|
|
1025
|
+
let startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2));
|
|
1026
|
+
let endPage = Math.min(totalPages, startPage + maxButtons - 1);
|
|
1027
|
+
|
|
1028
|
+
if (endPage - startPage < maxButtons - 1) {{
|
|
1029
|
+
startPage = Math.max(1, endPage - maxButtons + 1);
|
|
1030
|
+
}}
|
|
1031
|
+
|
|
1032
|
+
if (startPage > 1) {{
|
|
1033
|
+
html += `<button onclick="goToPage(1)" style="padding: 8px 12px; background: white; color: var(--text); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-size: 0.875rem;">1</button>`;
|
|
1034
|
+
if (startPage > 2) {{
|
|
1035
|
+
html += `<span style="padding: 8px;">...</span>`;
|
|
1036
|
+
}}
|
|
1037
|
+
}}
|
|
1038
|
+
|
|
1039
|
+
for (let i = startPage; i <= endPage; i++) {{
|
|
1040
|
+
const isActive = i === currentPage;
|
|
1041
|
+
html += `<button onclick="goToPage(${{i}})"
|
|
1042
|
+
style="padding: 8px 12px; background: ${{isActive ? 'var(--primary)' : 'white'}}; color: ${{isActive ? 'white' : 'var(--text)'}}; border: 1px solid ${{isActive ? 'var(--primary)' : 'var(--border)'}}; border-radius: 6px; cursor: pointer; font-size: 0.875rem; font-weight: ${{isActive ? '600' : '400'}};">
|
|
1043
|
+
${{i}}
|
|
1044
|
+
</button>`;
|
|
1045
|
+
}}
|
|
1046
|
+
|
|
1047
|
+
if (endPage < totalPages) {{
|
|
1048
|
+
if (endPage < totalPages - 1) {{
|
|
1049
|
+
html += `<span style="padding: 8px;">...</span>`;
|
|
1050
|
+
}}
|
|
1051
|
+
html += `<button onclick="goToPage(${{totalPages}})" style="padding: 8px 12px; background: white; color: var(--text); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-size: 0.875rem;">${{totalPages}}</button>`;
|
|
1052
|
+
}}
|
|
1053
|
+
|
|
1054
|
+
// Next button
|
|
1055
|
+
html += `<button onclick="goToPage(${{currentPage + 1}})" ${{currentPage === totalPages ? 'disabled' : ''}}
|
|
1056
|
+
style="padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; ${{currentPage === totalPages ? 'opacity: 0.5; cursor: not-allowed;' : ''}}">
|
|
1057
|
+
Next →
|
|
1058
|
+
</button>`;
|
|
1059
|
+
|
|
1060
|
+
pagination.innerHTML = html;
|
|
1061
|
+
}}
|
|
1062
|
+
|
|
1063
|
+
function goToPage(page) {{
|
|
1064
|
+
const totalPages = Math.ceil(filteredBooks.length / booksPerPage);
|
|
1065
|
+
if (page < 1 || page > totalPages) return;
|
|
1066
|
+
currentPage = page;
|
|
1067
|
+
renderBooks();
|
|
1068
|
+
updateURL();
|
|
1069
|
+
window.scrollTo({{ top: 0, behavior: 'smooth' }});
|
|
1070
|
+
}}
|
|
1071
|
+
|
|
1072
|
+
function showBookDetails(bookId) {{
|
|
1073
|
+
currentBookId = bookId;
|
|
1074
|
+
editMode = false;
|
|
1075
|
+
document.getElementById('edit-mode-btn').textContent = 'Edit Metadata';
|
|
1076
|
+
|
|
1077
|
+
const book = BOOKS.find(b => b.id === bookId);
|
|
1078
|
+
if (!book) return;
|
|
1079
|
+
|
|
1080
|
+
const modal = document.getElementById('book-modal');
|
|
1081
|
+
const modalTitle = document.getElementById('modal-title');
|
|
1082
|
+
const modalBody = document.getElementById('modal-body');
|
|
1083
|
+
|
|
1084
|
+
modalTitle.textContent = book.title;
|
|
1085
|
+
|
|
1086
|
+
let html = '';
|
|
1087
|
+
|
|
1088
|
+
// Cover image
|
|
1089
|
+
if (book.cover_path) {{
|
|
1090
|
+
html += `
|
|
1091
|
+
<div style="text-align: center; margin-bottom: 20px;">
|
|
1092
|
+
<img src="${{BASE_URL ? BASE_URL + '/' + book.cover_path : book.cover_path}}"
|
|
1093
|
+
alt="Cover"
|
|
1094
|
+
style="max-width: 300px; max-height: 400px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2);"
|
|
1095
|
+
onerror="this.style.display='none'">
|
|
1096
|
+
</div>
|
|
1097
|
+
`;
|
|
1098
|
+
}}
|
|
1099
|
+
|
|
1100
|
+
// Authors
|
|
1101
|
+
if (book.authors.length > 0) {{
|
|
1102
|
+
html += `
|
|
1103
|
+
<div class="detail-section">
|
|
1104
|
+
<div class="detail-label">Authors</div>
|
|
1105
|
+
<div class="detail-value">${{book.authors.map(a => a.name).join(', ')}}</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
`;
|
|
1108
|
+
}}
|
|
1109
|
+
|
|
1110
|
+
// Contributors
|
|
1111
|
+
if (book.contributors && book.contributors.length > 0) {{
|
|
1112
|
+
html += `
|
|
1113
|
+
<div class="detail-section">
|
|
1114
|
+
<div class="detail-label">Contributors</div>
|
|
1115
|
+
<ul class="contributors-list">
|
|
1116
|
+
${{book.contributors.map(c => `<li>${{c.name}} <span class="contributor-role">(${{c.role}})</span></li>`).join('')}}
|
|
1117
|
+
</ul>
|
|
1118
|
+
</div>
|
|
1119
|
+
`;
|
|
1120
|
+
}}
|
|
1121
|
+
|
|
1122
|
+
// Series
|
|
1123
|
+
if (book.series) {{
|
|
1124
|
+
html += `
|
|
1125
|
+
<div class="detail-section">
|
|
1126
|
+
<div class="detail-label">Series</div>
|
|
1127
|
+
<div class="detail-value">${{book.series}} #${{book.series_index}}</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
`;
|
|
1130
|
+
}}
|
|
1131
|
+
|
|
1132
|
+
// Description (rendered as HTML)
|
|
1133
|
+
if (book.description) {{
|
|
1134
|
+
html += `
|
|
1135
|
+
<div class="detail-section">
|
|
1136
|
+
<div class="detail-label">Description</div>
|
|
1137
|
+
<div class="detail-value">${{book.description}}</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
`;
|
|
1140
|
+
}}
|
|
1141
|
+
|
|
1142
|
+
// Metadata
|
|
1143
|
+
const metadata = [];
|
|
1144
|
+
if (book.publisher) metadata.push(`Publisher: ${{book.publisher}}`);
|
|
1145
|
+
if (book.publication_date) metadata.push(`Published: ${{book.publication_date}}`);
|
|
1146
|
+
if (book.edition) metadata.push(`Edition: ${{book.edition}}`);
|
|
1147
|
+
if (book.language) metadata.push(`Language: ${{book.language.toUpperCase()}}`);
|
|
1148
|
+
if (book.page_count) metadata.push(`Pages: ${{book.page_count}}`);
|
|
1149
|
+
|
|
1150
|
+
if (metadata.length > 0) {{
|
|
1151
|
+
html += `
|
|
1152
|
+
<div class="detail-section">
|
|
1153
|
+
<div class="detail-label">Metadata</div>
|
|
1154
|
+
<div class="detail-value">${{metadata.join(' • ')}}</div>
|
|
1155
|
+
</div>
|
|
1156
|
+
`;
|
|
1157
|
+
}}
|
|
1158
|
+
|
|
1159
|
+
// Subjects
|
|
1160
|
+
if (book.subjects.length > 0) {{
|
|
1161
|
+
html += `
|
|
1162
|
+
<div class="detail-section">
|
|
1163
|
+
<div class="detail-label">Subjects</div>
|
|
1164
|
+
<div class="tags">
|
|
1165
|
+
${{book.subjects.map(s => `<span class="tag">${{s}}</span>`).join('')}}
|
|
1166
|
+
</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
`;
|
|
1169
|
+
}}
|
|
1170
|
+
|
|
1171
|
+
// Keywords
|
|
1172
|
+
if (book.keywords && book.keywords.length > 0) {{
|
|
1173
|
+
html += `
|
|
1174
|
+
<div class="detail-section">
|
|
1175
|
+
<div class="detail-label">Keywords</div>
|
|
1176
|
+
<div class="tags">
|
|
1177
|
+
${{book.keywords.map(k => `<span class="tag">${{k}}</span>`).join('')}}
|
|
1178
|
+
</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
`;
|
|
1181
|
+
}}
|
|
1182
|
+
|
|
1183
|
+
// Files
|
|
1184
|
+
if (book.files.length > 0) {{
|
|
1185
|
+
html += `
|
|
1186
|
+
<div class="detail-section">
|
|
1187
|
+
<div class="detail-label">Files</div>
|
|
1188
|
+
<ul class="contributors-list">
|
|
1189
|
+
${{book.files.map(f => {{
|
|
1190
|
+
const fileUrl = BASE_URL ? `${{BASE_URL}}/${{f.path}}` : f.path;
|
|
1191
|
+
return `
|
|
1192
|
+
<li>
|
|
1193
|
+
<a href="${{fileUrl}}" class="file-link" target="_blank">
|
|
1194
|
+
${{f.format.toUpperCase()}}
|
|
1195
|
+
</a>
|
|
1196
|
+
• ${{formatBytes(f.size_bytes)}}
|
|
1197
|
+
${{f.creator_application ? ` • Created with ${{f.creator_application}}` : ''}}
|
|
1198
|
+
</li>
|
|
1199
|
+
`;
|
|
1200
|
+
}}).join('')}}
|
|
1201
|
+
</ul>
|
|
1202
|
+
</div>
|
|
1203
|
+
`;
|
|
1204
|
+
}}
|
|
1205
|
+
|
|
1206
|
+
// Identifiers
|
|
1207
|
+
if (book.identifiers.length > 0) {{
|
|
1208
|
+
html += `
|
|
1209
|
+
<div class="detail-section">
|
|
1210
|
+
<div class="detail-label">Identifiers</div>
|
|
1211
|
+
<ul class="identifiers-list">
|
|
1212
|
+
${{book.identifiers.map(i => `<li>${{i.scheme.toUpperCase()}}: ${{i.value}}</li>`).join('')}}
|
|
1213
|
+
</ul>
|
|
1214
|
+
</div>
|
|
1215
|
+
`;
|
|
1216
|
+
}}
|
|
1217
|
+
|
|
1218
|
+
// Personal tags
|
|
1219
|
+
if (book.personal.tags && book.personal.tags.length > 0) {{
|
|
1220
|
+
html += `
|
|
1221
|
+
<div class="detail-section">
|
|
1222
|
+
<div class="detail-label">Personal Tags</div>
|
|
1223
|
+
<div class="tags">
|
|
1224
|
+
${{book.personal.tags.map(t => `<span class="tag">${{t}}</span>`).join('')}}
|
|
1225
|
+
</div>
|
|
1226
|
+
</div>
|
|
1227
|
+
`;
|
|
1228
|
+
}}
|
|
1229
|
+
|
|
1230
|
+
// Rights
|
|
1231
|
+
if (book.rights) {{
|
|
1232
|
+
html += `
|
|
1233
|
+
<div class="detail-section">
|
|
1234
|
+
<div class="detail-label">Rights</div>
|
|
1235
|
+
<div class="detail-value">${{escapeHtml(book.rights)}}</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
`;
|
|
1238
|
+
}}
|
|
1239
|
+
|
|
1240
|
+
modalBody.innerHTML = html;
|
|
1241
|
+
modal.classList.add('active');
|
|
1242
|
+
}}
|
|
1243
|
+
|
|
1244
|
+
function closeModal() {{
|
|
1245
|
+
document.getElementById('book-modal').classList.remove('active');
|
|
1246
|
+
editMode = false;
|
|
1247
|
+
currentBookId = null;
|
|
1248
|
+
}}
|
|
1249
|
+
|
|
1250
|
+
function toggleSearchHelp() {{
|
|
1251
|
+
const helpDiv = document.getElementById('search-help');
|
|
1252
|
+
if (helpDiv.style.display === 'none') {{
|
|
1253
|
+
helpDiv.style.display = 'block';
|
|
1254
|
+
}} else {{
|
|
1255
|
+
helpDiv.style.display = 'none';
|
|
1256
|
+
}}
|
|
1257
|
+
}}
|
|
1258
|
+
|
|
1259
|
+
function toggleEditMode() {{
|
|
1260
|
+
editMode = !editMode;
|
|
1261
|
+
const btn = document.getElementById('edit-mode-btn');
|
|
1262
|
+
|
|
1263
|
+
if (editMode) {{
|
|
1264
|
+
btn.textContent = 'Save & Close';
|
|
1265
|
+
renderEditMode();
|
|
1266
|
+
}} else {{
|
|
1267
|
+
btn.textContent = 'Edit Metadata';
|
|
1268
|
+
saveMetadataOverrides();
|
|
1269
|
+
showBookDetails(currentBookId);
|
|
1270
|
+
}}
|
|
1271
|
+
}}
|
|
1272
|
+
|
|
1273
|
+
function renderEditMode() {{
|
|
1274
|
+
const book = BOOKS.find(b => b.id === currentBookId);
|
|
1275
|
+
if (!book) return;
|
|
1276
|
+
|
|
1277
|
+
const overrides = loadMetadataOverrides()[currentBookId] || {{}};
|
|
1278
|
+
const modalBody = document.getElementById('modal-body');
|
|
1279
|
+
|
|
1280
|
+
modalBody.innerHTML = `
|
|
1281
|
+
<div class="detail-section">
|
|
1282
|
+
<p style="color: #6b7280; margin-bottom: 20px;">
|
|
1283
|
+
ℹ️ Changes are saved locally in your browser and won't affect the original database.
|
|
1284
|
+
</p>
|
|
1285
|
+
</div>
|
|
1286
|
+
|
|
1287
|
+
<div class="detail-section">
|
|
1288
|
+
<label class="detail-label">Title</label>
|
|
1289
|
+
<input type="text" id="edit-title" class="form-control" value="${{escapeHtml(overrides.title || book.title)}}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
|
|
1290
|
+
</div>
|
|
1291
|
+
|
|
1292
|
+
<div class="detail-section">
|
|
1293
|
+
<label class="detail-label">Subtitle</label>
|
|
1294
|
+
<input type="text" id="edit-subtitle" class="form-control" value="${{escapeHtml(overrides.subtitle || book.subtitle || '')}}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
|
|
1295
|
+
</div>
|
|
1296
|
+
|
|
1297
|
+
<div class="detail-section">
|
|
1298
|
+
<label class="detail-label">Publisher</label>
|
|
1299
|
+
<input type="text" id="edit-publisher" class="form-control" value="${{escapeHtml(overrides.publisher || book.publisher || '')}}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
|
|
1300
|
+
</div>
|
|
1301
|
+
|
|
1302
|
+
<div class="detail-section">
|
|
1303
|
+
<label class="detail-label">Publication Date</label>
|
|
1304
|
+
<input type="text" id="edit-publication-date" class="form-control" value="${{escapeHtml(overrides.publication_date || book.publication_date || '')}}" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;" placeholder="YYYY-MM-DD">
|
|
1305
|
+
</div>
|
|
1306
|
+
|
|
1307
|
+
<div class="detail-section">
|
|
1308
|
+
<label class="detail-label">Rating (1-5)</label>
|
|
1309
|
+
<input type="number" id="edit-rating" class="form-control" value="${{overrides.rating !== undefined ? overrides.rating : (book.personal.rating || '')}}" min="1" max="5" step="0.5" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
|
|
1310
|
+
</div>
|
|
1311
|
+
|
|
1312
|
+
<div class="detail-section">
|
|
1313
|
+
<label class="detail-label">
|
|
1314
|
+
<input type="checkbox" id="edit-favorite" ${{(overrides.favorite !== undefined ? overrides.favorite : book.personal.favorite) ? 'checked' : ''}}>
|
|
1315
|
+
Favorite
|
|
1316
|
+
</label>
|
|
1317
|
+
</div>
|
|
1318
|
+
|
|
1319
|
+
<div class="detail-section">
|
|
1320
|
+
<label class="detail-label">Description</label>
|
|
1321
|
+
<textarea id="edit-description" class="form-control" rows="6" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">${{escapeHtml(overrides.description || book.description || '')}}</textarea>
|
|
1322
|
+
</div>
|
|
1323
|
+
|
|
1324
|
+
<div class="detail-section">
|
|
1325
|
+
<button onclick="clearOverrides()" class="btn-secondary" style="padding: 8px 16px; background: #ef4444; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
1326
|
+
Clear Local Overrides
|
|
1327
|
+
</button>
|
|
1328
|
+
</div>
|
|
1329
|
+
`;
|
|
1330
|
+
}}
|
|
1331
|
+
|
|
1332
|
+
function saveMetadataOverrides() {{
|
|
1333
|
+
const overrides = loadMetadataOverrides();
|
|
1334
|
+
if (!overrides[currentBookId]) overrides[currentBookId] = {{}};
|
|
1335
|
+
|
|
1336
|
+
const title = document.getElementById('edit-title').value;
|
|
1337
|
+
const subtitle = document.getElementById('edit-subtitle').value;
|
|
1338
|
+
const publisher = document.getElementById('edit-publisher').value;
|
|
1339
|
+
const publication_date = document.getElementById('edit-publication-date').value;
|
|
1340
|
+
const rating = document.getElementById('edit-rating').value;
|
|
1341
|
+
const favorite = document.getElementById('edit-favorite').checked;
|
|
1342
|
+
const description = document.getElementById('edit-description').value;
|
|
1343
|
+
|
|
1344
|
+
const book = BOOKS.find(b => b.id === currentBookId);
|
|
1345
|
+
|
|
1346
|
+
// Only save if different from original
|
|
1347
|
+
if (title !== book.title) overrides[currentBookId].title = title;
|
|
1348
|
+
if (subtitle !== (book.subtitle || '')) overrides[currentBookId].subtitle = subtitle;
|
|
1349
|
+
if (publisher !== (book.publisher || '')) overrides[currentBookId].publisher = publisher;
|
|
1350
|
+
if (publication_date !== (book.publication_date || '')) overrides[currentBookId].publication_date = publication_date;
|
|
1351
|
+
if (rating !== (book.personal.rating || '')) overrides[currentBookId].rating = parseFloat(rating) || null;
|
|
1352
|
+
if (favorite !== book.personal.favorite) overrides[currentBookId].favorite = favorite;
|
|
1353
|
+
if (description !== (book.description || '')) overrides[currentBookId].description = description;
|
|
1354
|
+
|
|
1355
|
+
localStorage.setItem('ebk_metadata_overrides', JSON.stringify(overrides));
|
|
1356
|
+
|
|
1357
|
+
// Refresh the filtered books with overrides
|
|
1358
|
+
filteredBooks = BOOKS.map(applyMetadataOverrides);
|
|
1359
|
+
filterBooks();
|
|
1360
|
+
}}
|
|
1361
|
+
|
|
1362
|
+
function clearOverrides() {{
|
|
1363
|
+
if (!confirm('Clear all local metadata overrides for this book?')) return;
|
|
1364
|
+
|
|
1365
|
+
const overrides = loadMetadataOverrides();
|
|
1366
|
+
delete overrides[currentBookId];
|
|
1367
|
+
localStorage.setItem('ebk_metadata_overrides', JSON.stringify(overrides));
|
|
1368
|
+
|
|
1369
|
+
editMode = false;
|
|
1370
|
+
showBookDetails(currentBookId);
|
|
1371
|
+
filterBooks();
|
|
1372
|
+
}}
|
|
1373
|
+
|
|
1374
|
+
function escapeHtml(text) {{
|
|
1375
|
+
if (!text) return '';
|
|
1376
|
+
const div = document.createElement('div');
|
|
1377
|
+
div.textContent = text;
|
|
1378
|
+
return div.innerHTML;
|
|
1379
|
+
}}
|
|
1380
|
+
|
|
1381
|
+
function formatBytes(bytes) {{
|
|
1382
|
+
if (bytes === 0) return '0 Bytes';
|
|
1383
|
+
const k = 1024;
|
|
1384
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
1385
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1386
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
1387
|
+
}}
|
|
1388
|
+
</script>
|
|
1389
|
+
</body>
|
|
1390
|
+
</html>'''
|