| const DB_NAME = "textphantom_local_gallery"; |
| const DB_VERSION = 1; |
| const STORE_NAME = "sessions"; |
| const IMAGE_PREFIX = "image/"; |
|
|
| let dbPromise = null; |
|
|
| function openDb() { |
| if (dbPromise) return 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_NAME)) { |
| db.createObjectStore(STORE_NAME, { keyPath: "id" }); |
| } |
| }; |
| req.onsuccess = () => resolve(req.result); |
| req.onerror = () => reject(req.error || new Error("indexeddb_open_failed")); |
| }); |
| return dbPromise; |
| } |
|
|
| function txComplete(tx) { |
| return new Promise((resolve, reject) => { |
| tx.oncomplete = () => resolve(); |
| tx.onabort = () => reject(tx.error || new Error("indexeddb_tx_aborted")); |
| tx.onerror = () => reject(tx.error || new Error("indexeddb_tx_failed")); |
| }); |
| } |
|
|
| function trimString(v) { |
| return String(v || "").trim(); |
| } |
|
|
| export function naturalCompare(a, b) { |
| return new Intl.Collator(undefined, { |
| numeric: true, |
| sensitivity: "base", |
| }).compare(String(a || ""), String(b || "")); |
| } |
|
|
| export function sortLocalPages(pages) { |
| return [...(Array.isArray(pages) ? pages : [])].sort((a, b) => { |
| const pa = trimString(a?.relativePath || a?.name || a?.id); |
| const pb = trimString(b?.relativePath || b?.name || b?.id); |
| const byPath = naturalCompare(pa, pb); |
| if (byPath) return byPath; |
| return naturalCompare(a?.id, b?.id); |
| }); |
| } |
|
|
| export function toLocalPageRecord(file, index = 0) { |
| const name = trimString(file?.name) || `image-${index + 1}`; |
| const type = trimString(file?.type) || "application/octet-stream"; |
| const relativePath = trimString(file?.webkitRelativePath || ""); |
| return { |
| id: crypto.randomUUID(), |
| name, |
| relativePath, |
| type, |
| size: Number(file?.size) || 0, |
| lastModified: Number(file?.lastModified) || 0, |
| blob: file, |
| }; |
| } |
|
|
| export function filterImageFiles(files) { |
| return [...(files || [])].filter((file) => { |
| const type = trimString(file?.type).toLowerCase(); |
| return type.startsWith(IMAGE_PREFIX); |
| }); |
| } |
|
|
| export async function saveLocalSession(session) { |
| const db = await openDb(); |
| const record = { |
| id: trimString(session?.id) || crypto.randomUUID(), |
| createdAt: Number(session?.createdAt) || Date.now(), |
| title: trimString(session?.title), |
| pages: Array.isArray(session?.pages) ? [...session.pages] : [], |
| }; |
| const tx = db.transaction(STORE_NAME, 'readwrite'); |
| tx.objectStore(STORE_NAME).put(record); |
| await txComplete(tx); |
| return record; |
| } |
|
|
| export async function loadLocalSession(id) { |
| const key = trimString(id); |
| if (!key) return null; |
| const db = await openDb(); |
| return await new Promise((resolve, reject) => { |
| const tx = db.transaction(STORE_NAME, 'readonly'); |
| const req = tx.objectStore(STORE_NAME).get(key); |
| req.onsuccess = () => resolve(req.result || null); |
| req.onerror = () => reject(req.error || new Error("indexeddb_get_failed")); |
| }); |
| } |
|
|
| export async function deleteLocalSession(id) { |
| const key = trimString(id); |
| if (!key) return; |
| const db = await openDb(); |
| const tx = db.transaction(STORE_NAME, 'readwrite'); |
| tx.objectStore(STORE_NAME).delete(key); |
| await txComplete(tx); |
| } |
|
|