Files
chrish-slingshot-CrasHUtils/js/lora_preview_cache.js
T
Chris b4d46bf7d8 Add grid and list LoRA loaders with browse UI and thumbnail support.
Introduces rgthree-style LoRA pickers, browser APIs, preview caching, CivitAI info, trigger tags, and resolution picker nodes.
2026-07-07 00:40:15 +01:00

182 lines
4.3 KiB
JavaScript

const API_BASE = "/crashutils/loras";
const DB_NAME = "CrashUtilsLoraPreviews";
const DB_VERSION = 1;
const STORE_NAME = "previews";
const memoryCache = new Map();
const previewVersions = new Map();
let dbPromise = null;
function cacheKey(loraPath) {
return loraPath ? String(loraPath).replace(/\\/g, "/") : "";
}
function openDb() {
if (dbPromise) {
return dbPromise;
}
dbPromise = new Promise((resolve, reject) => {
if (!globalThis.indexedDB) {
reject(new Error("IndexedDB unavailable"));
return;
}
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
request.result.createObjectStore(STORE_NAME);
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
}).catch(() => null);
return dbPromise;
}
async function idbGet(key) {
const db = await openDb();
if (!db) {
return null;
}
return new Promise((resolve) => {
const tx = db.transaction(STORE_NAME, "readonly");
const request = tx.objectStore(STORE_NAME).get(key);
request.onsuccess = () => resolve(request.result ?? null);
request.onerror = () => resolve(null);
});
}
async function idbPut(key, value) {
const db = await openDb();
if (!db) {
return;
}
return new Promise((resolve) => {
const tx = db.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).put(value, key);
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
});
}
async function idbDelete(key) {
const db = await openDb();
if (!db) {
return;
}
return new Promise((resolve) => {
const tx = db.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
});
}
function rememberObjectUrl(key, blob) {
const existing = memoryCache.get(key);
if (existing) {
URL.revokeObjectURL(existing);
}
const url = URL.createObjectURL(blob);
memoryCache.set(key, url);
return url;
}
export async function invalidateLoraPreview(loraPath) {
const key = cacheKey(loraPath);
if (!key) {
return;
}
const cached = memoryCache.get(key);
if (cached) {
URL.revokeObjectURL(cached);
memoryCache.delete(key);
}
await idbDelete(key);
previewVersions.set(key, (previewVersions.get(key) || 0) + 1);
}
export function previewRequestUrl(loraPath) {
const key = cacheKey(loraPath);
const version = previewVersions.get(key);
const versionParam = version ? `&v=${version}` : "";
return `${API_BASE}/preview?path=${encodeURIComponent(key)}${versionParam}`;
}
export async function getPreviewObjectUrl(loraPath) {
const key = cacheKey(loraPath);
if (!key) {
return null;
}
if (memoryCache.has(key)) {
return memoryCache.get(key);
}
const cached = await idbGet(key);
const headers = {};
if (cached?.etag) {
headers["If-None-Match"] = cached.etag;
}
let response;
try {
response = await fetch(previewRequestUrl(key), { headers });
} catch {
if (cached?.blob) {
return rememberObjectUrl(key, cached.blob);
}
return null;
}
if (response.status === 304 && cached?.blob) {
return rememberObjectUrl(key, cached.blob);
}
if (!response.ok) {
return null;
}
const blob = await response.blob();
const etag = response.headers.get("ETag") || "";
await idbPut(key, { blob, etag, savedAt: Date.now() });
return rememberObjectUrl(key, blob);
}
export function bindPreviewImage(img, loraPath, onError) {
if (!img || !loraPath) {
return;
}
getPreviewObjectUrl(loraPath).then((url) => {
if (!img.isConnected) {
return;
}
if (url) {
img.src = url;
img.onerror = () => onError?.(img);
return;
}
onError?.(img);
});
}
export function replaceWithPreviewPlaceholder(img) {
if (!img?.replaceWith) {
return;
}
img.replaceWith(
Object.assign(document.createElement("div"), {
className: "placeholder",
textContent: "🎨",
})
);
}