Introduces rgthree-style LoRA pickers, browser APIs, preview caching, CivitAI info, trigger tags, and resolution picker nodes.
182 lines
4.3 KiB
JavaScript
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: "🎨",
|
|
})
|
|
);
|
|
}
|