316 lines
9.2 KiB
JavaScript
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 };
|