feat(header): add scan progress spinner and dispatch scan-complete event

Add a scan progress indicator to the header that shows during library scans:
- Spinning SVG icon next to the BookHoard title
- Percentage display during active scans
- Dispatches bookhoard:scan-complete custom DOM event on window when scan
  finishes, enabling other components (dashboard) to react without polling
- Auto-resets progress display after 3 seconds
- Uses WebSocket pub/sub via addListener/removeListener with cleanup on
  header element removal
This commit is contained in:
2026-05-16 19:31:23 -04:00
parent d61abb1be1
commit 4f7794767d
+54 -8
View File
@@ -1,5 +1,3 @@
// Header functionality
import { Alpine } from "./alpine"; import { Alpine } from "./alpine";
import { initializeSearch } from "./search"; import { initializeSearch } from "./search";
import { import {
@@ -10,6 +8,7 @@ import {
updateWoodPanelingIndicators, updateWoodPanelingIndicators,
} from "./theme"; } from "./theme";
import { getSelectedLibrary } from "./storage"; import { getSelectedLibrary } from "./storage";
import { addListener, removeListener } from "./websocket";
const logout = (): void => { const logout = (): void => {
localStorage.removeItem("token"); localStorage.removeItem("token");
@@ -17,16 +16,14 @@ const logout = (): void => {
window.location.href = "/"; window.location.href = "/";
}; };
// Looks for saved library in localStorage and selects it in current page's dropdown
const restoreLibrarySelection = (): void => { const restoreLibrarySelection = (): void => {
const savedLibrary = getSelectedLibrary(); const savedLibrary = getSelectedLibrary();
if (!savedLibrary) return; if (!savedLibrary) return;
// Common library dropdown IDs across pages
const dropdownSelectors = [ const dropdownSelectors = [
"#library-select", // dashboard, bookshelf "#library-select",
"#selected-library", // alternate naming "#selected-library",
"#library_id", // form field "#library_id",
]; ];
for (const selector of dropdownSelectors) { for (const selector of dropdownSelectors) {
@@ -35,7 +32,6 @@ const restoreLibrarySelection = (): void => {
const option = dropdown.querySelector(`option[value="${savedLibrary}"]`); const option = dropdown.querySelector(`option[value="${savedLibrary}"]`);
if (option) { if (option) {
dropdown.value = savedLibrary; dropdown.value = savedLibrary;
// Trigger any htmx/change handlers
dropdown.dispatchEvent(new Event("change", { bubbles: true })); dropdown.dispatchEvent(new Event("change", { bubbles: true }));
console.log("Restored library selection:", savedLibrary); console.log("Restored library selection:", savedLibrary);
return; return;
@@ -44,6 +40,51 @@ const restoreLibrarySelection = (): void => {
} }
}; };
const initializeScanListener = function (this: any) {
const handler = (message: any) => {
switch (message.type) {
case "scan_progress":
this.scanning = true;
this.scanProgress = Math.round((message.data.progress || 0) * 100);
this.scanNewItems = message.data.new_items || 0;
this.scanFilesScanned = message.data.files_scanned || 0;
break;
case "scan_complete":
this.scanning = false;
this.scanProgress = 100;
this.scanNewItems = message.data?.new_items || this.scanNewItems;
window.dispatchEvent(
new CustomEvent("bookhoard:scan-complete", {
detail: message.data,
}),
);
setTimeout(() => {
this.scanProgress = 0;
this.scanNewItems = 0;
this.scanFilesScanned = 0;
}, 3000);
break;
case "scan_error":
this.scanning = false;
this.scanProgress = 0;
break;
}
};
addListener(handler);
const headerEl = document.querySelector("[x-data='header']");
if (headerEl) {
const observer = new MutationObserver(() => {
if (!document.contains(headerEl)) {
removeListener(handler);
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
};
export { logout, restoreLibrarySelection }; export { logout, restoreLibrarySelection };
Alpine.data("header", () => ({ Alpine.data("header", () => ({
@@ -55,4 +96,9 @@ Alpine.data("header", () => ({
loadWoodPaneling, loadWoodPaneling,
updateWoodPanelingIndicators, updateWoodPanelingIndicators,
restoreLibrarySelection, restoreLibrarySelection,
initializeScanListener,
scanning: false,
scanProgress: 0,
scanNewItems: 0,
scanFilesScanned: 0,
})); }));