From 05f1c889ee966c73f79f010f7bb969671f99ff9d Mon Sep 17 00:00:00 2001 From: Reimar Date: Tue, 14 Oct 2025 11:24:22 +0200 Subject: [PATCH] make assets be stored in indexeddb --- src/asset_editor.js | 83 +++++++++++++++++++++++++++++-------------- src/asset_provider.js | 24 +++++++++++-- src/asset_store.js | 65 +++++++++++++++++++++++++-------- src/html_exporter.js | 2 +- src/index.js | 12 +++---- 5 files changed, 134 insertions(+), 52 deletions(-) diff --git a/src/asset_editor.js b/src/asset_editor.js index 45efb61..c465303 100644 --- a/src/asset_editor.js +++ b/src/asset_editor.js @@ -1,4 +1,5 @@ import { promptUpload } from "./prompt_upload.js"; +import { AssetStore } from "./asset_store.js"; export class AssetEditor { constructor(rootEl) { @@ -6,7 +7,8 @@ export class AssetEditor { this.editor = rootEl.querySelector("#asset-editor"); this.toggleButton = rootEl.querySelector("#toggle-asset-editor-button"); this.container = rootEl; - this.previewedId = null; + this.previewedName = null; + if (localStorage.getItem("asset-editor-expanded")) { this.editor.style.display = "block"; this.container.style.flexGrow = "1"; @@ -18,61 +20,76 @@ export class AssetEditor { this.preview.title = rootEl.querySelector("#asset-editor-preview-title"); this.preview.image = rootEl.querySelector("#asset-editor-preview-image"); this.preview.snippet = rootEl.querySelector("#asset-editor-preview-snippet"); - this.assets = []; rootEl.querySelector("#asset-editor-upload-button").addEventListener("click", () => { this.promptUpload(); }); - this.renderList(); + AssetStore.load("assetdb").then((store) => { + this.store = store; + + this.renderList(); + }); } - importAssets(assets) { - this.assets = []; + getAssets() { + return this.store.getAll(); + } + + async importAssets(assets) { + await this.clearAssets(); + 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(); } async promptUpload() { for (const file of await promptUpload("image/*", true)) { const rootName = 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 === "."); + let name = rootName; let extension = ""; + if (extensionIdx !== -1) { name = rootName.slice(0, extensionIdx); extension = rootName.slice(extensionIdx); } + fullName = `${name}-${i}${extension}`; } - this.addAsset({ - name: fullName, - mime: file.type, - bytes: await fetch(URL.createObjectURL(file)).then((x) => x.bytes()), - }); + + await this.addAsset({ name: fullName, file }); } } - addAsset({ name, bytes, mime }) { - const id = Math.round(Math.random() * 1e6); - this.assets.push({ id, bytes, name, mime }); + async clearAssets() { + await this.store.clear(); + } + + async addAsset({ name, file }) { + await this.store.add(name, file); + this.renderList(); } - deleteAsset(id) { - this.assets = this.assets.filter((x) => x.id !== id); + async deleteAsset(name) { + await this.store.delete(name); + this.renderList(); } - setPreview(id) { - this.previewedId = id; - const asset = this.assets.find((x) => x.id === id); + async setPreview(asset) { + this.previewedName = 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(/ { const listItem = document.createElement("li"); listItem.classList.add("asset-editor-list-item"); - if (this.previewedId === asset.id) { + + if (this.previewedName === asset.id) { listItem.setAttribute("previewed", true); } + listItem.addEventListener("click", () => { for (const li of this.list.querySelectorAll("li[previewed]")) { li.removeAttribute("previewed"); } + listItem.setAttribute("previewed", true); - this.setPreview(asset.id); + + this.setPreview(asset); }); + const name = document.createElement("span"); name.textContent = asset.name; listItem.append(name); + const deleteButton = document.createElement("button"); deleteButton.innerHTML = "×"; deleteButton.classList.add("delete-button"); - deleteButton.addEventListener("click", (event) => { + deleteButton.addEventListener("click", async (event) => { event.stopPropagation(); + if (listItem.getAttribute("previewed")) { this.clearPreview(); } - this.deleteAsset(asset.id); + + await this.deleteAsset(asset.name); }); + listItem.append(deleteButton); + return listItem; }); diff --git a/src/asset_provider.js b/src/asset_provider.js index 4018f78..e94b452 100644 --- a/src/asset_provider.js +++ b/src/asset_provider.js @@ -9,17 +9,35 @@ export class AssetProvider { url(name) { const asset = this.assets.find((x) => x.name === name); + if (!asset) { 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 = {}; for (const asset of this.assets) { - result[asset.name] = this.url(asset.name); + result[asset.name] = await this.fullUrl(asset.name); } return result; diff --git a/src/asset_store.js b/src/asset_store.js index c5bfbae..93ef93a 100644 --- a/src/asset_store.js +++ b/src/asset_store.js @@ -1,6 +1,5 @@ export class AssetStore { static #isInternalConstructing = false; - static #idb = globalThis.indexedDB; constructor(db) { if (!AssetStore.#isInternalConstructing) { @@ -11,28 +10,66 @@ export class AssetStore { this.db = db; } - async add(name, mime, bytes) { + doTransaction(callback) { const transaction = this.db.transaction(["asset"], "readwrite"); - return await new Promise((resolve, reject) => { - transaction.oncomplete = () => { - resolve(); - }; - - transaction.onerror = () => { - reject(); - }; + return new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); 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) { - /* remove when not developing db */ - this.#idb.deleteDatabase(name); - const req = this.#idb.open(name); + const req = indexedDB.open(name); const db = await new Promise((resolve, reject) => { req.onerror = () => { reject(req.error); diff --git a/src/html_exporter.js b/src/html_exporter.js index 1e86478..e3bd9db 100644 --- a/src/html_exporter.js +++ b/src/html_exporter.js @@ -13,7 +13,7 @@ export class HtmlExporter { let lib = await (await fetch("./src/gamelib.js")).text(); lib = minifyJs(lib); - const assets = this.assetProvider.getAll(); + const assets = await this.assetProvider.getAll(); const html = ` diff --git a/src/index.js b/src/index.js index 9fe232b..96b9aa1 100644 --- a/src/index.js +++ b/src/index.js @@ -14,7 +14,6 @@ import { TextCompleter } from "./text_completer.js"; import { ConsoleInput } from "./console_input.js"; import { downloadFile, slugify } from "./utils.js"; import { HtmlExporter } from "./html_exporter.js"; -import { AssetStore } from "./asset_store.js"; const editor = ace.edit("editor"); editor.setTheme("ace/theme/gruvbox"); @@ -108,12 +107,15 @@ loadButton.onclick = async () => { editor.setValue(dec.decode(code.content)); }; -runButton.onclick = () => { +runButton.onclick = async () => { const code = editor.getValue(); - karlkoder.lib().assetProvider.injectAssets(assetEditor.assets); + const assets = await assetEditor.getAssets(); + karlkoder.lib().assetProvider.injectAssets(assets); + codeRunner.setCode(code); codeRunner.toggle(); + document.querySelector("canvas").focus(); if (codeRunner.isRunning) { @@ -153,7 +155,3 @@ addEventListener("keydown", (ev) => { }); toggleAssetEditorButton.addEventListener("click", () => assetEditor.toggleEditor()); - -const assetStore = await AssetStore.load("assetdb"); - -await assetStore.add("test", "image/png", []);