import { Alpine } from "./alpine"; import { showToast } from "./toast"; import { generateCoverBlob } from "./cover-generator"; function getMediaId(): string { const parts = window.location.pathname.split("/"); return parts[parts.length - 1] || ""; } function getAuthHeader(): string { const token = localStorage.getItem("token"); return token ? `Bearer ${token}` : ""; } function showProgressSyncModal(): void { const modal = document.getElementById("progress-sync-modal"); if (modal) modal.classList.remove("hidden"); } function showNotesModal(): void { const modal = document.getElementById("notes-modal"); if (modal) modal.classList.remove("hidden"); } function hideProgressSyncModal(): void { const modal = document.getElementById("progress-sync-modal"); if (modal) modal.classList.add("hidden"); } function hideNotesModal(): void { const modal = document.getElementById("notes-modal"); if (modal) modal.classList.add("hidden"); } function showReaderPlaceholder(): void { showToast("Ebook reader coming soon!", "info"); } function showMetadataEditor(): void { const modal = document.getElementById("metadata-editor-modal"); if (modal) modal.classList.remove("hidden"); } function hideMetadataEditor(): void { const modal = document.getElementById("metadata-editor-modal"); if (modal) modal.classList.add("hidden"); } function collectFormData(): Record { const modal = document.getElementById("metadata-editor-modal"); if (!modal) return {}; const data: Record = {}; const inputs = modal.querySelectorAll( "input[name], textarea[name], select[name]", ); for (const el of inputs) { if (el.type === "number") { const val = parseFloat(el.value); data[el.name] = isNaN(val) ? 0 : val; } else if (el.type === "checkbox") { data[el.name] = (el as HTMLInputElement).checked; } else { data[el.name] = el.value; } } const tagsStr = data.tags as string; if (typeof tagsStr === "string") { data.tags = tagsStr .split(",") .map((t: string) => t.trim()) .filter(Boolean); } const contribStr = data.contributors as string; if (typeof contribStr === "string") { data.contributors = contribStr .split(",") .map((c: string) => c.trim()) .filter(Boolean); } return data; } export { showReaderPlaceholder, showMetadataEditor, showProgressSyncModal, showNotesModal, hideProgressSyncModal, hideNotesModal, }; interface MetadataEditorState { openSections: Record; coverGenerating: boolean; hasExistingCover: boolean; newCoverPreview: string; coverFile: Blob | null; coverAction: string; saving: boolean; toggleSection(section: string): void; showMetadataEditor(): void; hideMetadataEditor(): void; handleCoverUpload(event: Event): void; generateCover(): Promise; removeCover(): void; saveMetadata(): Promise; } Alpine.data("bookDetail", () => { const coverImg = document.querySelector( "#metadata-editor-modal img#metadata-cover-preview", ) as HTMLImageElement | null; const hasCover = coverImg?.src && !coverImg.src.includes("placeholder-book.svg"); const coverPreviewEl = document.querySelector( ".aspect-\\[2\\/3\\] img", ) as HTMLImageElement | null; const coverSrc = coverPreviewEl?.src || ""; const fileUrl = coverSrc && !coverSrc.includes("placeholder") ? coverSrc : ""; return { openSections: { basic: true } as Record, coverGenerating: false, hasExistingCover: !!hasCover, newCoverPreview: "", coverFile: null as Blob | null, coverAction: "keep", saving: false, showReaderPlaceholder, showMetadataEditor, hideMetadataEditor, showNotesModal, showProgressSyncModal, hideProgressSyncModal, hideNotesModal, init() { const link = document.getElementById("back-link"); if (!link) return; const storageKey = "book_detail_back"; if (document.referrer) { try { const ref = new URL(document.referrer); if ( ref.origin === window.location.origin && !ref.pathname.startsWith("/readers/") && ref.pathname !== window.location.pathname ) { sessionStorage.setItem(storageKey, ref.pathname + ref.search); } } catch {} } else { sessionStorage.removeItem(storageKey); } const backUrl = sessionStorage.getItem(storageKey) || "/dashboard"; link.addEventListener("click", (e: Event) => { e.preventDefault(); sessionStorage.removeItem(storageKey); window.location.href = backUrl; }); }, toggleSection(section: string) { this.openSections[section] = !this.openSections[section]; }, handleCoverUpload(event: Event) { const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) return; this.coverFile = file; this.coverAction = "upload"; const reader = new FileReader(); reader.onload = (e) => { const preview = document.getElementById( "metadata-cover-preview", ) as HTMLImageElement; if (preview && e.target?.result) { preview.src = e.target.result as string; } this.newCoverPreview = e.target?.result as string; }; reader.readAsDataURL(file); }, async generateCover() { this.coverGenerating = true; try { const formatGroup = document .querySelector('[data-format-group]') ?.getAttribute("data-format-group") || "reflowable"; const blob = await generateCoverBlob(fileUrl, formatGroup); if (!blob) return; this.coverFile = blob; this.coverAction = "upload"; const preview = document.getElementById( "metadata-cover-preview", ) as HTMLImageElement; if (preview) { preview.src = URL.createObjectURL(blob); } this.newCoverPreview = URL.createObjectURL(blob); showToast("Cover generated successfully", "success"); } finally { this.coverGenerating = false; } }, removeCover() { this.coverAction = "remove"; this.coverFile = null; this.newCoverPreview = ""; const preview = document.getElementById( "metadata-cover-preview", ) as HTMLImageElement; if (preview) { preview.src = "/static/placeholder-book.svg"; } }, async saveMetadata() { if (this.saving) return; this.saving = true; try { const mediaId = getMediaId(); const formData = collectFormData(); formData.cover_action = this.coverAction; if (this.coverFile) { const fd = new FormData(); for (const [key, value] of Object.entries(formData)) { if (Array.isArray(value)) { fd.append(key, JSON.stringify(value)); } else if (typeof value === "boolean") { fd.append(key, value ? "true" : "false"); } else { fd.append(key, String(value)); } } fd.append("cover_file", this.coverFile, "cover.jpg"); const resp = await fetch(`/api/media-items/${mediaId}`, { method: "PUT", headers: { Authorization: getAuthHeader() }, body: fd, }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.error || "Failed to save metadata"); } } else { const resp = await fetch(`/api/media-items/${mediaId}`, { method: "PUT", headers: { Authorization: getAuthHeader(), "Content-Type": "application/json", }, body: JSON.stringify(formData), }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.error || "Failed to save metadata"); } } hideMetadataEditor(); showToast("Metadata saved successfully", "success"); setTimeout(() => window.location.reload(), 500); } catch (e) { showToast( e instanceof Error ? e.message : "Failed to save metadata", "error", ); } finally { this.saving = false; } }, } as MetadataEditorState; });