import { Alpine } from "./alpine"; import { initializeSearch } from "./search"; import { initializeTheme, changeTheme, changeWoodPaneling, loadWoodPaneling, revealCurrentTheme, updateWoodPanelingIndicators, } from "./theme"; import { getSelectedLibrary } from "./storage"; import { addListener, removeListener } from "./websocket"; const logout = (): void => { localStorage.removeItem("token"); localStorage.removeItem("user"); window.location.href = "/"; }; const restoreLibrarySelection = (): void => { const savedLibrary = getSelectedLibrary(); if (!savedLibrary) return; const dropdownSelectors = ["#library-select", "#selected-library", "#library_id"]; for (const selector of dropdownSelectors) { const dropdown = document.querySelector(selector) as HTMLSelectElement; if (dropdown) { const option = dropdown.querySelector(`option[value="${savedLibrary}"]`); if (option) { // SSR pages already render the select with the stored library // (setSelectedLibrary keeps the cookie in sync and resolveLibrary // renders from it). Re-firing `change` here made every page load // re-run the page's switch handler — which re-fetches content at // hardcoded page 1 / offset 0 and stomps URL-tracked state (this // broke the /series paginator: page 2 snapped back to page 1). // Only re-fire when the select actually drifted from storage. if (dropdown.value === savedLibrary) return; dropdown.value = savedLibrary; dropdown.dispatchEvent(new Event("change", { bubbles: true })); console.log("Restored library selection:", savedLibrary); return; } } } }; const initializeScanListener = function (this: any) { // Restore state on page load: a scan that started before this page // was opened (or a refresh mid-scan) emits no websocket event until // its next progress tick, so ask the server directly first. const restoreActiveScan = async () => { try { const token = localStorage.getItem("token"); if (!token) return; const resp = await fetch("/api/scanner/active-summary", { headers: { Authorization: `Bearer ${token}` }, }); if (!resp.ok) return; const data = await resp.json(); if (data.scanning) { this.scanning = true; this.scanProgress = Math.round((data.progress || 0) * 100); } } catch { // Offline or endpoint unavailable — websocket events still drive // the state once they arrive. } }; restoreActiveScan(); 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; case "scan_cancelled": // A cancelled scan is not an error: stop the spinner quietly. 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", () => ({ mobileMenuOpen: false, logout, initializeSearch, initializeTheme, changeTheme, changeWoodPaneling, loadWoodPaneling, revealCurrentTheme, updateWoodPanelingIndicators, restoreLibrarySelection, initializeScanListener, scanning: false, scanProgress: 0, scanNewItems: 0, scanFilesScanned: 0, }));