/** * Persistent storage for uploaded images, backed by IndexedDB. * * We store the original (compressed) file Blob plus a small thumbnail data URL * and basic metadata. The full image is decoded from the Blob on demand when a * stored upload is selected, so the database stays compact. */ import type { RawImage } from "./imageConvert.ts"; import { loadImageFromFile, rawToThumbDataUrl } from "./imageLoad.ts"; const DB_NAME = "numbl-image-filter"; const DB_VERSION = 1; const STORE = "uploads"; export interface UploadRecord { id: string; name: string; width: number; height: number; /** Small data URL shown in the picker. */ thumb: string; /** Original file bytes (compressed); decoded on demand. */ blob: Blob; createdAt: number; } function genId(): string { if (typeof crypto !== "undefined" && crypto.randomUUID) { return crypto.randomUUID(); } return `up-${Date.now()}-${Math.random().toString(36).slice(2)}`; } let dbPromise: Promise | null = null; function getDb(): Promise { if (!dbPromise) { dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(STORE)) { db.createObjectStore(STORE, { keyPath: "id" }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error ?? new Error("Failed to open IndexedDB")); }); } return dbPromise; } function runRequest( mode: IDBTransactionMode, fn: (store: IDBObjectStore) => IDBRequest ): Promise { return getDb().then( (db) => new Promise((resolve, reject) => { const t = db.transaction(STORE, mode); const req = fn(t.objectStore(STORE)); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }) ); } /** All stored uploads, newest first. */ export async function listUploads(): Promise { const all = await runRequest( "readonly", (s) => s.getAll() as IDBRequest ); return all.sort((a, b) => b.createdAt - a.createdAt); } /** Decode + store an uploaded file. Returns the record and decoded image. */ export async function addUpload( file: File ): Promise<{ record: UploadRecord; image: RawImage }> { const image = await loadImageFromFile(file); const record: UploadRecord = { id: genId(), name: file.name || "upload", width: image.width, height: image.height, thumb: rawToThumbDataUrl(image, 72), blob: file, createdAt: Date.now(), }; await runRequest("readwrite", (s) => s.put(record)); return { record, image }; } export async function deleteUpload(id: string): Promise { await runRequest("readwrite", (s) => s.delete(id)); } /** Decode the full image for a stored upload. */ export function getUploadImage(rec: UploadRecord): Promise { return loadImageFromFile(rec.blob); }