Create Library
Edit Library
Browse Folders
Delete Library
Are you sure you want to delete ?
- Permanently removed, no archive or undo: the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.
- Not touched: media files on disk.
Are you sure you want to delete ?
- Permanently removed, no archive or undo: the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.
- Not touched: media files on disk.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
No Libraries Yet
Create your first library to get started
Create Your First Library")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1038,7 +1038,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\" onclick=\"document.getElementById(this.dataset.targetInput).value = this.dataset.currentPath; document.getElementById('folder-browser-modal').classList.add('hidden')\" class=\"btn btn-primary text-sm\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "\" @click=\"pickBrowsePath($event)\" class=\"btn btn-primary text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/audio_reader.templ b/templates/audio_reader.templ
index 7f1c3c43..09e349da 100644
--- a/templates/audio_reader.templ
+++ b/templates/audio_reader.templ
@@ -184,7 +184,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
src={ metadata.CoverImagePath }
alt={ metadata.Title }
class="w-full h-full object-cover"
- onerror="this.style.display='none'"
+ data-fallback="hide"
/>
} else {
🎧
diff --git a/templates/audio_reader_templ.go b/templates/audio_reader_templ.go
index 1937e0e8..cd19e757 100644
--- a/templates/audio_reader_templ.go
+++ b/templates/audio_reader_templ.go
@@ -220,7 +220,7 @@ func AudioReader(user User, metadata ReaderMetadata) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"w-full h-full object-cover\" onerror=\"this.style.display='none'\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" class=\"w-full h-full object-cover\" data-fallback=\"hide\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/book_detail.templ b/templates/book_detail.templ
index 8055e169..8b3181d1 100644
--- a/templates/book_detail.templ
+++ b/templates/book_detail.templ
@@ -88,7 +88,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
alt={ book.Title }
class="w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl"
style="box-shadow: var(--shadow-pop);"
- onerror="this.src='/static/placeholder-book.svg'"
+
/>
} else {
![]()
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"w-48 md:w-56 h-72 md:h-80 object-cover rounded-2xl\" style=\"box-shadow: var(--shadow-pop);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/collections.templ b/templates/collections.templ
index 177b7b1d..2b75af37 100644
--- a/templates/collections.templ
+++ b/templates/collections.templ
@@ -63,7 +63,7 @@ templ CollectionBooksGrid(collection CollectionData, books []handlers.BookInfo)
src={ book.CoverImagePath }
alt="Cover"
class="w-full aspect-[3/4] object-cover rounded-lg shadow-md"
- onerror="this.src='/static/placeholder-book.svg'"
+
/>
} else {
if book.CoverImagePath != "" {
-

+

} else {

}
diff --git a/templates/collections_templ.go b/templates/collections_templ.go
index b10c7128..c7bcf329 100644
--- a/templates/collections_templ.go
+++ b/templates/collections_templ.go
@@ -236,7 +236,7 @@ func CollectionBooksGrid(collection CollectionData, books []handlers.BookInfo) t
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\" onerror=\"this.src='/static/placeholder-book.svg'\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" alt=\"Cover\" class=\"w-full aspect-[3/4] object-cover rounded-lg shadow-md\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -970,7 +970,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "\" class=\"w-full aspect-[2/3] object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "\" class=\"w-full aspect-[2/3] object-cover\" loading=\"lazy\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/dashboard.templ b/templates/dashboard.templ
index 3584dce4..dfdfc091 100644
--- a/templates/dashboard.templ
+++ b/templates/dashboard.templ
@@ -139,7 +139,7 @@ templ BookCard(item handlers.BookInfo) {
alt={ item.Title }
class="w-full h-full object-cover"
loading="lazy"
- onerror="this.src='/static/placeholder-book.svg'"
+
/>
} else {
![]()
🔥 { fmt.Sprintf("%d", week.CurrentStreak) }-day streak
}
- Full stats
+ Full stats
@Icon("chevron-down", "h-4 w-4 transition-transform group-open:rotate-180")
diff --git a/templates/dashboard_templ.go b/templates/dashboard_templ.go
index f9c8fa28..114df93d 100644
--- a/templates/dashboard_templ.go
+++ b/templates/dashboard_templ.go
@@ -456,7 +456,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" class=\"w-full h-full object-cover\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" class=\"w-full h-full object-cover\" loading=\"lazy\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1027,7 +1027,7 @@ func DashboardWeekWidget(week WeekStatsData, activeLibType string) templ.Compone
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "
Full stats")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "Full stats")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/docs.templ b/templates/docs.templ
index 96f75dbb..089366ef 100644
--- a/templates/docs.templ
+++ b/templates/docs.templ
@@ -26,7 +26,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
@DocsTopbar(user)
-
+
@DocsSidebar(nav, currentPath)
@@ -243,7 +243,7 @@ templ DocsSearchResults(results []SearchResultItem) {
for _, r := range results {
-
-
+
{ r.Title }
if r.Category != "" {
{ r.Category }
diff --git a/templates/docs_templ.go b/templates/docs_templ.go
index cd570049..c7d763c4 100644
--- a/templates/docs_templ.go
+++ b/templates/docs_templ.go
@@ -92,7 +92,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -823,7 +823,7 @@ func DocsSearchResults(results []SearchResultItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" class=\"block px-4 py-2 no-underline transition-colors hover:bg-surface-hover\" onclick=\"this.closest('#docs-search-results').classList.add('hidden')\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" class=\"block px-4 py-2 no-underline transition-colors hover:bg-surface-hover\" @click=\"closeSearchResults($event)\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/progress.templ b/templates/progress.templ
index ae310561..b75798f9 100644
--- a/templates/progress.templ
+++ b/templates/progress.templ
@@ -44,7 +44,7 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
src={ item.CoverImagePath }
alt="Cover"
class="w-24 h-32 object-cover rounded-lg"
- onerror="this.src='/static/placeholder-book.svg'"
+
/>
diff --git a/templates/progress_templ.go b/templates/progress_templ.go
index 655f5998..edcb26a8 100644
--- a/templates/progress_templ.go
+++ b/templates/progress_templ.go
@@ -102,7 +102,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" alt=\"Cover\" class=\"w-24 h-32 object-cover rounded-lg\" onerror=\"this.src='/static/placeholder-book.svg'\">
}
if len(series.CoverPaths) == 0 {
diff --git a/templates/series_templ.go b/templates/series_templ.go
index 487ec075..e57e1be8 100644
--- a/templates/series_templ.go
+++ b/templates/series_templ.go
@@ -323,7 +323,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" loading=\"lazy\" onerror=\"this.src='/static/placeholder-book.svg'\"> ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" loading=\"lazy\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/templates/unlinked_books.templ b/templates/unlinked_books.templ
index 7b9742cc..65a7eb45 100644
--- a/templates/unlinked_books.templ
+++ b/templates/unlinked_books.templ
@@ -28,7 +28,7 @@ templ UnlinkedMatchCards(progressId string, matches []services.BookMatch) {
src={ match.CoverImagePath }
alt="Cover"
class="w-16 h-24 object-cover rounded"
- onerror="this.src='/static/placeholder-book.svg'"
+
/>
{ match.Title }
diff --git a/templates/unlinked_books_templ.go b/templates/unlinked_books_templ.go
index 245fb0e4..f4cd1471 100644
--- a/templates/unlinked_books_templ.go
+++ b/templates/unlinked_books_templ.go
@@ -98,7 +98,7 @@ func UnlinkedMatchCards(progressId string, matches []services.BookMatch) templ.C
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" alt=\"Cover\" class=\"w-16 h-24 object-cover rounded\" onerror=\"this.src='/static/placeholder-book.svg'\">
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" alt=\"Cover\" class=\"w-16 h-24 object-cover rounded\">
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/src/admin.ts b/web/src/admin.ts
index 81cc3102..6643d606 100644
--- a/web/src/admin.ts
+++ b/web/src/admin.ts
@@ -427,8 +427,6 @@ async function purgeArchivedItems(): Promise {
}
}
-(window as any).purgeArchivedItems = purgeArchivedItems;
-
// Restore a hidden/archived item to library visibility without waiting for
// its file to return (POST /api/media-items/:id/unarchive). If the file is
// still gone the next scan hides it again.
@@ -599,6 +597,21 @@ Alpine.data("admin", () => ({
this.loadWatchStatus();
this.checkActiveScans();
},
+ purgeArchivedItems,
+ openCreateLibraryModal() {
+ document.getElementById("create-library-modal")?.classList.remove("hidden");
+ },
+ hideModal(id: string) {
+ document.getElementById(id)?.classList.add("hidden");
+ },
+ pickBrowsePath(e: Event) {
+ const button = e.currentTarget as HTMLElement;
+ const input = document.getElementById(
+ button.dataset.targetInput ?? "",
+ ) as HTMLInputElement | null;
+ if (input) input.value = button.dataset.currentPath ?? "";
+ this.hideModal("folder-browser-modal");
+ },
cancelAllScans,
checkActiveScans,
hideScanProgress,
diff --git a/web/src/docs.ts b/web/src/docs.ts
index fd9fa936..95a3092c 100644
--- a/web/src/docs.ts
+++ b/web/src/docs.ts
@@ -158,6 +158,13 @@ Alpine.data("docs", () => ({
this.initializeSearch();
this.initializeCodeCopyButtons();
},
+ closeDocsSidebar(e: Event) {
+ document.getElementById("docs-sidebar")?.classList.add("-translate-x-full");
+ (e.currentTarget as HTMLElement).classList.add("hidden");
+ },
+ closeSearchResults(e: Event) {
+ (e.currentTarget as HTMLElement).closest("#docs-search-results")?.classList.add("hidden");
+ },
toggleSidebar,
toggleCollapsible,
initializeSearch: initializeDocsSearch,
diff --git a/web/src/image_fallback.ts b/web/src/image_fallback.ts
new file mode 100644
index 00000000..31592a51
--- /dev/null
+++ b/web/src/image_fallback.ts
@@ -0,0 +1,38 @@
+// Delegated image-fallback handling for strict CSP.
+//
+// Cover images used to carry inline `onerror` attributes; under the
+// hash-allow-listed script-src (no 'unsafe-inline') the browser refuses to
+// execute them. This module restores the same behavior with one
+// capture-phase listener on , so htmx-swapped containers are covered
+// without any per-element re-binding.
+//
+// Opt-ins via data attributes on the element:
+// data-fallback="hide" → hide the element instead of swapping the src
+// (audiobook covers: placeholder would overflow)
+// default → swap a broken cover to the placeholder SVG
+
+const PLACEHOLDER = "/static/placeholder-book.svg";
+
+let armed = false;
+
+function onResourceError(event: Event) {
+ const target = event.target;
+ // Only interested in resource elements (img/audio/video/source), not
+ // generic events bubbling up (error does not bubble, but capture sees it).
+ if (!(target instanceof HTMLImageElement)) return;
+
+ if (target.dataset.fallback === "hide") {
+ target.style.display = "none";
+ return;
+ }
+ // Never chase our own placeholder (if it 404s, stop).
+ if (target.src.endsWith(PLACEHOLDER)) return;
+ target.src = PLACEHOLDER;
+}
+
+export function armImageFallbacks() {
+ if (armed) return;
+ armed = true;
+ // Capture is required: 'error' doesn't bubble.
+ document.documentElement.addEventListener("error", onResourceError, true);
+}
diff --git a/web/src/main.ts b/web/src/main.ts
index 155cd863..a4913947 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -17,6 +17,7 @@ import "./device-management";
import "./docs";
import "./header";
import "./index";
+import "./image_fallback";
import "./library-switcher";
import "./linking";
import "./login";