import Toolkit from /**/"./toolkit/Toolkit.js"; import VB from /**/"../shrooms-vb-core/web/VB.js"; import ZipFile from /**/"./util/ZipFile.js"; class App extends Toolkit.App { // Instance fields #bundle; // Packaged assets #core; // Emulation core #dateCode; // Numeric date code of bundle #theme; // ID of global color palette #themes; // Available color palettes // Components #mnuExport; // File -> Export bundle... #mnuLoadROM; // File -> Load ROM... ///////////////////////// Initialization Methods ////////////////////////// constructor() { super({ className: "tk app", style: { display : "grid", height : "100vh", gridTemplateRows: "max-content auto" } }); this.#construct.apply(this, arguments); } // Asynchronous constructor async #construct(bundle, dateCode) { // Instance fields this.#bundle = bundle; this.#dateCode = dateCode; this.#theme = null; this.#themes = new Map(); // Initialize components this.#initThemes(); await this.#initLocales(); this.title = /{{app.title}}/; this.#initMenus(); let desktop = document.createElement("div"); desktop.style.background = "var(--tk-desktop)"; this.element.append(desktop); document.body.append(this.element); VB.create(!this.#bundle.isDebug ? { audioUrl: /***/"../shrooms-vb-core/web/Audio.js", coreUrl : /***/"../shrooms-vb-core/web/Core.js", wasmUrl : /***/"../shrooms-vb-core/web/core.wasm", } : { audioUrl: import.meta.resolve("../shrooms-vb-core/web/Audio.js" ), coreUrl : import.meta.resolve("../shrooms-vb-core/web/Core.js" ), wasmUrl : import.meta.resolve("../shrooms-vb-core/web/core.wasm") }).then(c=>this.#onCoreCreate(c)); } // Display text async #initLocales() { for (let file of this.#bundle.list("shrooms-vb-web/locale/")) this.addLocale(await (await fetch(file.url)).json()); this.locale = "en-US"; } // Menu bar #initMenus() { let bar, item, sub, group; // Menu bar bar = new Toolkit.MenuBar(this); bar.ariaLabel = /{{app.menuBar}}/; this.add(bar); // File item = new Toolkit.MenuItem(this); item.text = /{{menu.file._}}/; bar.add(item); sub = this.#mnuLoadROM = new Toolkit.MenuItem(this, {disabled:true}); sub.text = /{{menu.file.loadROM}}/; sub.addEventListener("action", ()=>this.#onLoadROM()); item.add(sub); sub = new Toolkit.MenuItem(this, { type: "checkbox" }); sub.text = /{{menu.file.dualMode}}/; item.add(sub); sub = new Toolkit.MenuItem(this, { type: "checkbox" }); sub.text = /{{menu.file.debugMode}}/; item.add(sub); item.addSeparator(); sub = this.#mnuExport = new Toolkit.MenuItem(this); sub.text = /{{menu.file.exportBundle}}/; sub.addEventListener("action", ()=>this.#onExportBundle()); item.add(sub); // Emulation item = new Toolkit.MenuItem(this); item.text = /{{menu.emulation._}}/; bar.add(item); // Theme item = new Toolkit.MenuItem(this); item.text = /{{menu.theme._}}/; bar.add(item); group = new Toolkit.Group(); sub = new Toolkit.MenuItem(this, { type: "radio" }); sub.text = /{{menu.theme.auto}}/; group.add(sub, "auto"); item.add(sub); sub = new Toolkit.MenuItem(this, { type: "radio" }); sub.text = /{{menu.theme.light}}/; group.add(sub, "light"); item.add(sub); sub = new Toolkit.MenuItem(this, { type: "radio" }); sub.text = /{{menu.theme.dark}}/; group.add(sub, "dark"); item.add(sub); sub = new Toolkit.MenuItem(this, { type: "radio" }); sub.text = /{{menu.theme.virtual}}/; group.add(sub, "virtual"); item.add(sub); group.value = "auto"; group.addEventListener("action", e=>{ let theme = e[Toolkit.group].value; this.#setTheme(theme == "auto" ? null : theme); }); } // Color themes #initThemes() { let bundle = this.#bundle; // Base theme stylesheet document.head.append(Toolkit.stylesheet(this.#bundle.get( "shrooms-vb-web/theme/kiosk.css").url)); // Color set stylesheets for (let id of [ "light", "dark", "virtual" ]) { let file = bundle.get("shrooms-vb-web/theme/" + id + ".css"); let theme = Toolkit.stylesheet(file.url); theme.disabled = id != "light"; this.#themes.set(id, theme); document.head.append(theme); } // Event handling this.addEventListener("dark", e=>this.#onDark()); this.#onDark(); } ///////////////////////////// Event Handlers ////////////////////////////// // Core created #onCoreCreate(core) { this.#core = core; this.#mnuLoadROM.disabled = false; } // User agent dark mode preference changed #onDark() { // Current color theme is not auto if (this.#theme != null) return; // Working variables let active = this.#activeTheme(); let auto = this.#autoTheme(); // The active color theme matches the automatic color theme if (active == auto) return; // Activate the automatic color theme this.#themes.get(auto ).disabled = false; this.#themes.get(active).disabled = true; } // File -> Export bundle... async #onExportBundle() { this.#mnuExport.disabled = true; // Add the bundle contents to a .zip file let zip = new ZipFile(); for (let asset of this.#bundle.values()) zip.add(asset.name, asset.data); let blob = await zip.toBlob(); // Prompt the user to save the file let link = document.createElement("a"); link.download = "acid-shroom_" + this.#dateCode + ".zip"; link.href = URL.createObjectURL(blob); Object.assign(link.style, { position : "absolute", visibility: "hidden" }); document.body.append(link); link.click(); link.remove(); this.#mnuExport.disabled = false; } // File -> Load ROM... async #onLoadROM() { // Produce an invisible file picker element let picker = document.createElement("input"); picker.type = "file"; Object.assign(picker.style, { position : "absolute", visibility: "hidden" }); // Prompt the user to select a file document.body.append(picker); await new Promise(resolve=>{ picker.addEventListener("input", resolve); picker.click(); }); picker.remove(); // Select the file let file = picker.files[0] ?? null; if (file == null) return; // Read the file let rom; try { if (file.size > 0x1000000) { console.log("ROM file length safeguard"); throw 0; } rom = new Uint8Array(await file.arrayBuffer()); } catch { alert(this.translate(/{{menu.file.loadROMError}}/)); return; } // Attempt to decode as ISX rom = await this.#core.fromISX(rom) ?? rom; // Error checking if ( rom.length < 4 || rom.length > 0x1000000 || (rom.length & rom.length - 1) != 0 // Not a power of two ) { alert(this.translate(/{{menu.file.loadROMNotVB}}/)); return; } // TODO: Something with the ROM data console.log(rom.length.toString(16).toUpperCase().padStart(8, "0")); } ///////////////////////////// Private Methods ///////////////////////////// // Determine which color theme is active #activeTheme() { return [... this.#themes.entries()].find(e=>!e[1].disabled)[0]; } // Determine which color theme should be selected automatically #autoTheme() { return Toolkit.isDark() ? "dark" : "light"; } // Determine whether a ROM size is #checkROMSize(size) { return !( file.size == 0 || // Too small file.size > 0x01000000 || // Too big (file.size - 1 & file.size) != 0 // Not a power of two ); } // Specify the active color theme #setTheme(id) { // Theme is not changing if (id == this.#theme) return; // Configure instance fields this.#theme = id; // Working variables let active = this.#activeTheme(); let next = id ?? this.#autoTheme(); // Active stylesheet is not changing if (active == next) return; // Change the active stylesheet this.#themes.get(next ).disabled = false; this.#themes.get(active).disabled = true; } } export { App };