import React from "react"; import { Output } from "@/components/output"; import { Emulator, EmulationState, isFinalState } from "./emulator"; import { Filesystem, setupFilesystem, windowsToInternalPath, } from "./filesystem"; import { memory64 } from "wasm-feature-detect"; import "./App.css"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Settings, loadSettings, saveSettings } from "./settings"; import { SettingsMenu } from "@/components/settings-menu"; import { PlayFill, StopFill, GearFill, PauseFill, HouseFill, } from "react-bootstrap-icons"; import { StatusIndicator } from "@/components/status-indicator"; import { Header } from "./Header"; import { Button } from "@/components/ui/button"; import { Drawer, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { FilesystemExplorer } from "./filesystem-explorer"; import { EmulationStatus } from "./emulator"; import { EmulationSummary } from "./components/emulation-summary"; import { downloadBinaryFilePercent } from "./download"; export interface PlaygroundFile { file: string; storage: string; } export interface PlaygroundProps {} export interface PlaygroundState { settings: Settings; filesystemPromise?: Promise; filesystem?: Filesystem; emulator?: Emulator; emulationStatus?: EmulationStatus; application?: string; drawerOpen: boolean; allowWasm64: boolean; file?: PlaygroundFile; } function decodeFileData(data: string | null): PlaygroundFile | undefined { if (!data) { return undefined; } try { const jsonData = JSON.parse(atob(data)); return { file: jsonData.file, storage: jsonData.storage, }; } catch (e) { console.log(e); } return undefined; } interface GlobalThisExt { emulateCache?: string | null; } function getGlobalThis() { return globalThis as GlobalThisExt; } export function storeEmulateData(data?: string) { getGlobalThis().emulateCache = undefined; if (data) { localStorage.setItem("emulate", data); } else { localStorage.removeItem("emulate"); } } function getEmulateData() { const gt = getGlobalThis(); if (gt.emulateCache) { return gt.emulateCache; } const emulateData = localStorage.getItem("emulate"); localStorage.removeItem("emulate"); gt.emulateCache = emulateData; return emulateData; } export class Playground extends React.Component< PlaygroundProps, PlaygroundState > { private output: React.RefObject; private iconCache: Map = new Map(); constructor(props: PlaygroundProps) { super(props); this.output = React.createRef(); this.start = this.start.bind(this); this.resetFilesys = this.resetFilesys.bind(this); this.startEmulator = this.startEmulator.bind(this); this.toggleEmulatorState = this.toggleEmulatorState.bind(this); this.state = { settings: loadSettings(), drawerOpen: false, allowWasm64: false, file: decodeFileData(getEmulateData()), }; } componentDidMount(): void { memory64().then((allowWasm64) => { this.setState({ allowWasm64 }); }); if (this.state.file) { this.emulateRemoteFile(this.state.file); } } async resetFilesys() { if (!this.state.filesystem) { return; } await this.state.filesystem.delete(); this.setState({ filesystemPromise: undefined, filesystem: undefined, drawerOpen: false, }); this.output.current?.clear(); location.reload(); } _onEmulatorStatusChanged(s: EmulationStatus) { this.setState({ emulationStatus: s }); } _onEmulatorStateChanged(s: EmulationState, persistFs: boolean) { if (isFinalState(s) && persistFs) { this.setState({ filesystemPromise: undefined, filesystem: undefined }); this.initFilesys(true); } else { this.forceUpdate(); } } initFilesys(force: boolean = false) { if (!force && this.state.filesystemPromise) { return this.state.filesystemPromise; } const promise = new Promise((resolve) => { if (!force) { this.logLine("Loading filesystem..."); } setupFilesystem( (current, total, file) => { this.logLine(`Processing filesystem (${current}/${total}): ${file}`); }, (percent) => { this.logLine(`Downloading filesystem: ${percent}%`); }, ).then(resolve); }); promise.then((filesystem) => this.setState({ filesystem })); this.setState({ filesystemPromise: promise }); return promise; } setDrawerOpen(drawerOpen: boolean) { this.setState({ drawerOpen }); } async downloadFileToFilesystem(file: PlaygroundFile) { const fs = await this.initFilesys(); const fileData = await downloadBinaryFilePercent( file.storage, (percent) => { this.logLine(`Downloading binary: ${percent}%`); }, ); await fs.storeFiles([ { name: windowsToInternalPath(file.file), data: fileData, }, ]); } async emulateRemoteFile(file: PlaygroundFile) { await this.downloadFileToFilesystem(file); await this.startEmulator(file.file); } async start() { await this.initFilesys(); this.setDrawerOpen(true); } logLine(line: string) { this.output.current?.logLine(line); } logLines(lines: string[]) { this.output.current?.logLines(lines); } isEmulatorPaused() { return ( this.state.emulator && this.state.emulator.getState() == EmulationState.Paused ); } toggleEmulatorState() { if (this.isEmulatorPaused()) { this.state.emulator?.resume(); } else { this.state.emulator?.pause(); } } async startEmulator(userFile: string) { this.state.emulator?.stop(); this.output.current?.clear(); this.setDrawerOpen(false); this.logLine("Starting emulation..."); if (this.state.filesystemPromise) { await this.state.filesystemPromise; } const persistFs = this.state.settings.persist; const new_emulator = new Emulator( (l) => this.logLines(l), (s) => this._onEmulatorStateChanged(s, persistFs), (s) => this._onEmulatorStatusChanged(s), ); //new_emulator.onTerminate().then(() => this.setState({ emulator: null })); this.setState({ emulator: new_emulator, application: userFile }); new_emulator.start(this.state.settings, userFile); } render() { return ( <>
{ saveSettings(s); this.setState({ settings: s }); }} /> {!this.state.filesystem ? ( <> ) : ( this.setState({ drawerOpen: o })} > Filesystem Explorer Filesystem Explorer )} {/* Separator */}
); } }