From da1f6892639bce52e515e3ad5f3cc47a37cd47ed Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 11 Sep 2026 23:08:11 -0400 Subject: [PATCH] feat(ui): add Rescan button to Edit Metadata dialog Adds a Rescan button to the MetadataEditorModal footer that POSTs to the new per-book rescan endpoint, with rescanning state (disabled + spinner, matching the existing Mark Read pattern), success/error toasts, and a page reload to pick up the refreshed cover and metadata. --- templates/book_detail_modals.templ | 15 +++++++++++++- templates/book_detail_modals_templ.go | 12 ++++++++++-- web/src/book-detail.ts | 28 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/templates/book_detail_modals.templ b/templates/book_detail_modals.templ index 0585dfc..ad1b0b3 100644 --- a/templates/book_detail_modals.templ +++ b/templates/book_detail_modals.templ @@ -609,7 +609,19 @@ templ MetadataEditorModal(book handlers.MediaDetail) { -
+
+ +
+
} diff --git a/templates/book_detail_modals_templ.go b/templates/book_detail_modals_templ.go index e3be864..21437c4 100644 --- a/templates/book_detail_modals_templ.go +++ b/templates/book_detail_modals_templ.go @@ -1336,7 +1336,15 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" readonly class=\"input opacity-60\">
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "Save") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/src/book-detail.ts b/web/src/book-detail.ts index ba2aa72..faaeeeb 100644 --- a/web/src/book-detail.ts +++ b/web/src/book-detail.ts @@ -97,6 +97,7 @@ interface MetadataEditorState { coverFile: Blob | null; coverAction: string; saving: boolean; + rescanning: boolean; userRating: number; ratingHover: number; ratingSaving: boolean; @@ -110,6 +111,7 @@ interface MetadataEditorState { generateCover(): Promise; removeCover(): void; saveMetadata(): Promise; + rescanBook(): Promise; starFill(i: number): string; ratingText(): string; setRating(value: number): Promise; @@ -149,6 +151,7 @@ Alpine.data("bookDetail", () => { coverFile: null as Blob | null, coverAction: "keep", saving: false, + rescanning: false, userRating: 0, ratingHover: 0, ratingSaving: false, @@ -559,6 +562,31 @@ Alpine.data("bookDetail", () => { } }, + async rescanBook() { + if (this.rescanning) return; + this.rescanning = true; + const mediaId = getMediaId(); + try { + const resp = await fetch(`/api/media-items/${mediaId}/rescan`, { + method: "POST", + headers: { Authorization: getAuthHeader() }, + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(err.error || "Failed to rescan book"); + } + showToast("Book rescanned successfully", "success"); + setTimeout(() => window.location.reload(), 500); + } catch (e) { + showToast( + e instanceof Error ? e.message : "Failed to rescan book", + "error", + ); + } finally { + this.rescanning = false; + } + }, + async searchEditorTags() { const libraryId = document.body.getAttribute("data-library-id") || ""; if (!this.tagSearch || this.tagSearch.length < 2 || !libraryId) {