// Shared PDF download logic used by the markdown and textResponse // plugin views. Encapsulates the POST /api/pdf/markdown call, the // in-flight `pdfDownloading` flag, the `pdfError` state, and the // blob-to-download dance (createObjectURL → click → revoke). // // Error handling contract: any failure path (network, non-OK HTTP, // malformed blob) sets pdfDownloading back to false and populates // pdfError with a user-facing message. Callers can render pdfError // below the download button. import { ref, type Ref } from "vue"; import { API_ROUTES } from "../config/apiRoutes"; import { postMarkdownForBlob, type MarkdownRenderOptions } from "../utils/markdownBlobRequest"; import { errorMessage } from "../utils/errors"; export interface UsePdfDownloadHandle { pdfDownloading: Ref; pdfError: Ref; downloadPdf: (markdown: string, filename: string, options?: MarkdownRenderOptions) => Promise; } export function usePdfDownload(): UsePdfDownloadHandle { const pdfDownloading = ref(false); const pdfError = ref(null); async function downloadPdf(markdown: string, filename: string, options: MarkdownRenderOptions = {}): Promise { pdfError.value = null; pdfDownloading.value = true; let url: string | null = null; try { const response = await postMarkdownForBlob(API_ROUTES.pdf.markdown, markdown, filename, options); if (!response.ok) { const errText = await response.text().catch(() => ""); pdfError.value = `PDF error ${response.status}: ${errText}`; return; } const blob = await response.blob(); url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.click(); } catch (err) { pdfError.value = errorMessage(err); } finally { // Always clean up the object URL and release the in-flight flag // so the button is never left disabled forever. if (url) URL.revokeObjectURL(url); pdfDownloading.value = false; } } return { pdfDownloading, pdfError, downloadPdf }; }