shrooms-vb-web/App.js

316 lines
9.2 KiB
JavaScript

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 };