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:
+54
-8
@@ -1,5 +1,3 @@
|
||||
// Header functionality
|
||||
|
||||
import { Alpine } from "./alpine";
|
||||
import { initializeSearch } from "./search";
|
||||
import {
|
||||
@@ -10,6 +8,7 @@ import {
|
||||
updateWoodPanelingIndicators,
|
||||
} from "./theme";
|
||||
import { getSelectedLibrary } from "./storage";
|
||||
import { addListener, removeListener } from "./websocket";
|
||||
|
||||
const logout = (): void => {
|
||||
localStorage.removeItem("token");
|
||||
@@ -17,16 +16,14 @@ const logout = (): void => {
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
// Looks for saved library in localStorage and selects it in current page's dropdown
|
||||
const restoreLibrarySelection = (): void => {
|
||||
const savedLibrary = getSelectedLibrary();
|
||||
if (!savedLibrary) return;
|
||||
|
||||
// Common library dropdown IDs across pages
|
||||
const dropdownSelectors = [
|
||||
"#library-select", // dashboard, bookshelf
|
||||
"#selected-library", // alternate naming
|
||||
"#library_id", // form field
|
||||
"#library-select",
|
||||
"#selected-library",
|
||||
"#library_id",
|
||||
];
|
||||
|
||||
for (const selector of dropdownSelectors) {
|
||||
@@ -35,7 +32,6 @@ const restoreLibrarySelection = (): void => {
|
||||
const option = dropdown.querySelector(`option[value="${savedLibrary}"]`);
|
||||
if (option) {
|
||||
dropdown.value = savedLibrary;
|
||||
// Trigger any htmx/change handlers
|
||||
dropdown.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
console.log("Restored library selection:", savedLibrary);
|
||||
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 };
|
||||
|
||||
Alpine.data("header", () => ({
|
||||
@@ -55,4 +96,9 @@ Alpine.data("header", () => ({
|
||||
loadWoodPaneling,
|
||||
updateWoodPanelingIndicators,
|
||||
restoreLibrarySelection,
|
||||
initializeScanListener,
|
||||
scanning: false,
|
||||
scanProgress: 0,
|
||||
scanNewItems: 0,
|
||||
scanFilesScanned: 0,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user