feat(ui): add Rescan button to Edit Metadata dialog
Release / build-and-push (push) Successful in 2m50s

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.
This commit is contained in:
2026-09-11 23:08:20 -04:00
parent 298330a3a8
commit da1f689263
3 changed files with 52 additions and 3 deletions
+14 -1
View File
@@ -609,7 +609,19 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-end space-x-3 p-6 border-t flex-shrink-0" style="border-color: var(--border);"> <div class="flex items-center justify-between p-6 border-t flex-shrink-0" style="border-color: var(--border);">
<button
@click="rescanBook()"
:disabled="rescanning"
class="btn btn-secondary"
>
<span x-show="!rescanning" class="inline-flex items-center gap-2">@Icon("sync", "h-4 w-4")<span>Rescan</span></span>
<svg x-show="rescanning" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</button>
<div class="flex space-x-3">
<button <button
@click="hideMetadataEditor()" @click="hideMetadataEditor()"
class="btn btn-secondary" class="btn btn-secondary"
@@ -625,5 +637,6 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</button> </button>
</div> </div>
</div> </div>
</div>
</div> </div>
} }
+10 -2
View File
@@ -1336,7 +1336,15 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" readonly class=\"input opacity-60\"></div></div></div></div></div></div><div class=\"flex justify-end space-x-3 p-6 border-t flex-shrink-0\" style=\"border-color: var(--border);\"><button @click=\"hideMetadataEditor()\" class=\"btn btn-secondary\">Cancel</button> <button @click=\"saveMetadata()\" class=\"btn btn-primary\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" readonly class=\"input opacity-60\"></div></div></div></div></div></div><div class=\"flex items-center justify-between p-6 border-t flex-shrink-0\" style=\"border-color: var(--border);\"><button @click=\"rescanBook()\" :disabled=\"rescanning\" class=\"btn btn-secondary\"><span x-show=\"!rescanning\" class=\"inline-flex items-center gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("sync", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "<span>Rescan</span></span> <svg x-show=\"rescanning\" class=\"animate-spin inline-block h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg></button><div class=\"flex space-x-3\"><button @click=\"hideMetadataEditor()\" class=\"btn btn-secondary\">Cancel</button> <button @click=\"saveMetadata()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -1344,7 +1352,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "Save</button></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "Save</button></div></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+28
View File
@@ -97,6 +97,7 @@ interface MetadataEditorState {
coverFile: Blob | null; coverFile: Blob | null;
coverAction: string; coverAction: string;
saving: boolean; saving: boolean;
rescanning: boolean;
userRating: number; userRating: number;
ratingHover: number; ratingHover: number;
ratingSaving: boolean; ratingSaving: boolean;
@@ -110,6 +111,7 @@ interface MetadataEditorState {
generateCover(): Promise<void>; generateCover(): Promise<void>;
removeCover(): void; removeCover(): void;
saveMetadata(): Promise<void>; saveMetadata(): Promise<void>;
rescanBook(): Promise<void>;
starFill(i: number): string; starFill(i: number): string;
ratingText(): string; ratingText(): string;
setRating(value: number): Promise<void>; setRating(value: number): Promise<void>;
@@ -149,6 +151,7 @@ Alpine.data("bookDetail", () => {
coverFile: null as Blob | null, coverFile: null as Blob | null,
coverAction: "keep", coverAction: "keep",
saving: false, saving: false,
rescanning: false,
userRating: 0, userRating: 0,
ratingHover: 0, ratingHover: 0,
ratingSaving: false, 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() { async searchEditorTags() {
const libraryId = document.body.getAttribute("data-library-id") || ""; const libraryId = document.body.getAttribute("data-library-id") || "";
if (!this.tagSearch || this.tagSearch.length < 2 || !libraryId) { if (!this.tagSearch || this.tagSearch.length < 2 || !libraryId) {