make assets be stored in indexeddb

This commit is contained in:
Reimar 2025-10-14 11:24:22 +02:00
parent 7b096fe79a
commit 05f1c889ee
5 changed files with 134 additions and 52 deletions

View File

@ -1,4 +1,5 @@
import { promptUpload } from "./prompt_upload.js"; import { promptUpload } from "./prompt_upload.js";
import { AssetStore } from "./asset_store.js";
export class AssetEditor { export class AssetEditor {
constructor(rootEl) { constructor(rootEl) {
@ -6,7 +7,8 @@ export class AssetEditor {
this.editor = rootEl.querySelector("#asset-editor"); this.editor = rootEl.querySelector("#asset-editor");
this.toggleButton = rootEl.querySelector("#toggle-asset-editor-button"); this.toggleButton = rootEl.querySelector("#toggle-asset-editor-button");
this.container = rootEl; this.container = rootEl;
this.previewedId = null; this.previewedName = null;
if (localStorage.getItem("asset-editor-expanded")) { if (localStorage.getItem("asset-editor-expanded")) {
this.editor.style.display = "block"; this.editor.style.display = "block";
this.container.style.flexGrow = "1"; this.container.style.flexGrow = "1";
@ -18,61 +20,76 @@ export class AssetEditor {
this.preview.title = rootEl.querySelector("#asset-editor-preview-title"); this.preview.title = rootEl.querySelector("#asset-editor-preview-title");
this.preview.image = rootEl.querySelector("#asset-editor-preview-image"); this.preview.image = rootEl.querySelector("#asset-editor-preview-image");
this.preview.snippet = rootEl.querySelector("#asset-editor-preview-snippet"); this.preview.snippet = rootEl.querySelector("#asset-editor-preview-snippet");
this.assets = [];
rootEl.querySelector("#asset-editor-upload-button").addEventListener("click", () => { rootEl.querySelector("#asset-editor-upload-button").addEventListener("click", () => {
this.promptUpload(); this.promptUpload();
}); });
this.renderList(); AssetStore.load("assetdb").then((store) => {
this.store = store;
this.renderList();
});
} }
importAssets(assets) { getAssets() {
this.assets = []; return this.store.getAll();
}
async importAssets(assets) {
await this.clearAssets();
for (const asset of assets) { for (const asset of assets) {
this.addAsset({ name: asset.name, bytes: asset.bytes, mime: asset.mime }); await this.addAsset({ name: asset.name, file: asset.file });
} }
this.renderList(); this.renderList();
} }
async promptUpload() { async promptUpload() {
for (const file of await promptUpload("image/*", true)) { for (const file of await promptUpload("image/*", true)) {
const rootName = file.name; const rootName = file.name;
let fullName = file.name; let fullName = file.name;
for (let i = 0; this.assets.some((x) => x.name === fullName); ++i) { for (let i = 0; await this.store.get(fullName); i++) {
const extensionIdx = rootName.split("").findLastIndex((x) => x === "."); const extensionIdx = rootName.split("").findLastIndex((x) => x === ".");
let name = rootName; let name = rootName;
let extension = ""; let extension = "";
if (extensionIdx !== -1) { if (extensionIdx !== -1) {
name = rootName.slice(0, extensionIdx); name = rootName.slice(0, extensionIdx);
extension = rootName.slice(extensionIdx); extension = rootName.slice(extensionIdx);
} }
fullName = `${name}-${i}${extension}`; fullName = `${name}-${i}${extension}`;
} }
this.addAsset({
name: fullName, await this.addAsset({ name: fullName, file });
mime: file.type,
bytes: await fetch(URL.createObjectURL(file)).then((x) => x.bytes()),
});
} }
} }
addAsset({ name, bytes, mime }) { async clearAssets() {
const id = Math.round(Math.random() * 1e6); await this.store.clear();
this.assets.push({ id, bytes, name, mime }); }
async addAsset({ name, file }) {
await this.store.add(name, file);
this.renderList(); this.renderList();
} }
deleteAsset(id) { async deleteAsset(name) {
this.assets = this.assets.filter((x) => x.id !== id); await this.store.delete(name);
this.renderList(); this.renderList();
} }
setPreview(id) { async setPreview(asset) {
this.previewedId = id; this.previewedName = asset.name;
const asset = this.assets.find((x) => x.id === id);
this.preview.title.textContent = asset.name; this.preview.title.textContent = asset.name;
this.preview.image.src = `data:${asset.mime};base64,${asset.bytes.toBase64()}`; this.preview.image.src = URL.createObjectURL(asset.file);
const sanitizedName = asset.name.replace(/</g, "&lt;").replace(/&/g, "&amp;"); const sanitizedName = asset.name.replace(/</g, "&lt;").replace(/&/g, "&amp;");
this.preview.snippet.innerHTML = "lib.drawSprite(<br>" + this.preview.snippet.innerHTML = "lib.drawSprite(<br>" +
@ -88,35 +105,47 @@ export class AssetEditor {
this.preview.image.src = ""; this.preview.image.src = "";
} }
renderList() { async renderList() {
const children = this.assets const assets = await this.store.getAll();
const children = assets
.map((asset) => { .map((asset) => {
const listItem = document.createElement("li"); const listItem = document.createElement("li");
listItem.classList.add("asset-editor-list-item"); listItem.classList.add("asset-editor-list-item");
if (this.previewedId === asset.id) {
if (this.previewedName === asset.id) {
listItem.setAttribute("previewed", true); listItem.setAttribute("previewed", true);
} }
listItem.addEventListener("click", () => { listItem.addEventListener("click", () => {
for (const li of this.list.querySelectorAll("li[previewed]")) { for (const li of this.list.querySelectorAll("li[previewed]")) {
li.removeAttribute("previewed"); li.removeAttribute("previewed");
} }
listItem.setAttribute("previewed", true); listItem.setAttribute("previewed", true);
this.setPreview(asset.id);
this.setPreview(asset);
}); });
const name = document.createElement("span"); const name = document.createElement("span");
name.textContent = asset.name; name.textContent = asset.name;
listItem.append(name); listItem.append(name);
const deleteButton = document.createElement("button"); const deleteButton = document.createElement("button");
deleteButton.innerHTML = "&times;"; deleteButton.innerHTML = "&times;";
deleteButton.classList.add("delete-button"); deleteButton.classList.add("delete-button");
deleteButton.addEventListener("click", (event) => { deleteButton.addEventListener("click", async (event) => {
event.stopPropagation(); event.stopPropagation();
if (listItem.getAttribute("previewed")) { if (listItem.getAttribute("previewed")) {
this.clearPreview(); this.clearPreview();
} }
this.deleteAsset(asset.id);
await this.deleteAsset(asset.name);
}); });
listItem.append(deleteButton); listItem.append(deleteButton);
return listItem; return listItem;
}); });

View File

@ -9,17 +9,35 @@ export class AssetProvider {
url(name) { url(name) {
const asset = this.assets.find((x) => x.name === name); const asset = this.assets.find((x) => x.name === name);
if (!asset) { if (!asset) {
throw new Error(`Asset with name '${name}' does not exist`); throw new Error(`Asset with name '${name}' does not exist`);
} }
return `data:${asset.mime};base64,${asset.bytes.toBase64()}`;
return URL.createObjectURL(asset.file);
} }
getAll() { fullUrl(name) {
return new Promise((resolve, reject) => {
const asset = this.assets.find((x) => x.name === name);
if (!asset) {
throw new Error(`Asset with name '${name}' does not exist`);
}
const reader = new FileReader();
reader.readAsDataURL(asset.file);
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
});
}
async getAll() {
const result = {}; const result = {};
for (const asset of this.assets) { for (const asset of this.assets) {
result[asset.name] = this.url(asset.name); result[asset.name] = await this.fullUrl(asset.name);
} }
return result; return result;

View File

@ -1,6 +1,5 @@
export class AssetStore { export class AssetStore {
static #isInternalConstructing = false; static #isInternalConstructing = false;
static #idb = globalThis.indexedDB;
constructor(db) { constructor(db) {
if (!AssetStore.#isInternalConstructing) { if (!AssetStore.#isInternalConstructing) {
@ -11,28 +10,66 @@ export class AssetStore {
this.db = db; this.db = db;
} }
async add(name, mime, bytes) { doTransaction(callback) {
const transaction = this.db.transaction(["asset"], "readwrite"); const transaction = this.db.transaction(["asset"], "readwrite");
return await new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
transaction.oncomplete = () => { transaction.oncomplete = () => resolve();
resolve(); transaction.onerror = () => reject(transaction.error);
};
transaction.onerror = () => {
reject();
};
const objectStore = transaction.objectStore("asset"); const objectStore = transaction.objectStore("asset");
objectStore.add({ name, mime, bytes }); callback(objectStore);
});
}
request(req) {
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
clear() {
return this.doTransaction(async (objectStore) => {
await this.request(objectStore.clear());
});
}
async get(name) {
let result;
await this.doTransaction(async (objectStore) => {
result = await this.request(objectStore.get(name));
});
return result;
}
async getAll() {
let result = [];
await this.doTransaction(async (objectStore) => {
result = await this.request(objectStore.getAll());
});
return result;
}
add(name, file) {
return this.doTransaction(async (objectStore) => {
await this.request(objectStore.add({ name, file }));
});
}
delete(name) {
return this.doTransaction(async (objectStore) => {
await this.request(objectStore.delete(name));
}); });
} }
static async load(name) { static async load(name) {
/* remove when not developing db */ const req = indexedDB.open(name);
this.#idb.deleteDatabase(name);
const req = this.#idb.open(name);
const db = await new Promise((resolve, reject) => { const db = await new Promise((resolve, reject) => {
req.onerror = () => { req.onerror = () => {
reject(req.error); reject(req.error);

View File

@ -13,7 +13,7 @@ export class HtmlExporter {
let lib = await (await fetch("./src/gamelib.js")).text(); let lib = await (await fetch("./src/gamelib.js")).text();
lib = minifyJs(lib); lib = minifyJs(lib);
const assets = this.assetProvider.getAll(); const assets = await this.assetProvider.getAll();
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>

View File

@ -14,7 +14,6 @@ import { TextCompleter } from "./text_completer.js";
import { ConsoleInput } from "./console_input.js"; import { ConsoleInput } from "./console_input.js";
import { downloadFile, slugify } from "./utils.js"; import { downloadFile, slugify } from "./utils.js";
import { HtmlExporter } from "./html_exporter.js"; import { HtmlExporter } from "./html_exporter.js";
import { AssetStore } from "./asset_store.js";
const editor = ace.edit("editor"); const editor = ace.edit("editor");
editor.setTheme("ace/theme/gruvbox"); editor.setTheme("ace/theme/gruvbox");
@ -108,12 +107,15 @@ loadButton.onclick = async () => {
editor.setValue(dec.decode(code.content)); editor.setValue(dec.decode(code.content));
}; };
runButton.onclick = () => { runButton.onclick = async () => {
const code = editor.getValue(); const code = editor.getValue();
karlkoder.lib().assetProvider.injectAssets(assetEditor.assets); const assets = await assetEditor.getAssets();
karlkoder.lib().assetProvider.injectAssets(assets);
codeRunner.setCode(code); codeRunner.setCode(code);
codeRunner.toggle(); codeRunner.toggle();
document.querySelector("canvas").focus(); document.querySelector("canvas").focus();
if (codeRunner.isRunning) { if (codeRunner.isRunning) {
@ -153,7 +155,3 @@ addEventListener("keydown", (ev) => {
}); });
toggleAssetEditorButton.addEventListener("click", () => assetEditor.toggleEditor()); toggleAssetEditorButton.addEventListener("click", () => assetEditor.toggleEditor());
const assetStore = await AssetStore.load("assetdb");
await assetStore.add("test", "image/png", []);