feat: Icons, better UI
|
@ -1,7 +1,7 @@
|
|||
@import "./status.less";
|
||||
@import "./emulator.less";
|
||||
@import "./info.less";
|
||||
@import "./floppy.less";
|
||||
@import "./settings.less";
|
||||
|
||||
/* GENERAL RESETS */
|
||||
|
||||
|
@ -40,13 +40,35 @@ section {
|
|||
width: 75%;
|
||||
max-width: 700px;
|
||||
min-width: 400px;
|
||||
|
||||
.card-title {
|
||||
img {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link > img,
|
||||
.btn > img {
|
||||
height: 24px;
|
||||
margin-top: -3px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.windows95 {
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
nav .nav-link,
|
||||
nav .nav-logo {
|
||||
height: 30px;
|
||||
height: 33px;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
nav .nav-logo img {
|
||||
|
@ -61,6 +83,11 @@ section {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
border-color: #fff #000 #000 #fff;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
|
|
|
@ -13,3 +13,9 @@
|
|||
#file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings {
|
||||
legend > img {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
111
src/renderer/card-drive.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import * as React from "react";
|
||||
import { shell } from "electron";
|
||||
|
||||
interface CardDriveProps {
|
||||
showDiskImage: () => void;
|
||||
}
|
||||
|
||||
interface CardDriveState {}
|
||||
|
||||
export class CardDrive extends React.Component<CardDriveProps, CardDriveState> {
|
||||
constructor(props: CardDriveProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public render() {
|
||||
let advice: JSX.Element | null = null;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
advice = this.renderAdviceWindows();
|
||||
} else if (process.platform === "darwin") {
|
||||
advice = this.renderAdviceMac();
|
||||
} else {
|
||||
advice = this.renderAdviceLinux();
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="card settings">
|
||||
<div className="card-header">
|
||||
<h2 className="card-title">
|
||||
<img src="../../static/drive.png" />
|
||||
Modify C: Drive
|
||||
</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p>
|
||||
windows95 (this app) uses a raw disk image. Windows 95 (the
|
||||
operating system) is fragile, so adding or removing files is
|
||||
risky.
|
||||
</p>
|
||||
{advice}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
public renderAdviceWindows(): JSX.Element {
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>Changing the disk on Windows</legend>
|
||||
<p>
|
||||
Windows 10 cannot mount raw disk images (ironically, macOS and Linux
|
||||
can). However, tools exist that let you mount this drive, like the
|
||||
freeware tool{" "}
|
||||
<a
|
||||
href="#"
|
||||
onClick={() =>
|
||||
shell.openExternal(
|
||||
"https://www.osforensics.com/tools/mount-disk-images.html"
|
||||
)
|
||||
}
|
||||
>
|
||||
OSFMount
|
||||
</a>
|
||||
. I am not affiliated with it, so please use it at your own risk.
|
||||
</p>
|
||||
{this.renderMountButton("Windows Explorer")}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
public renderAdviceMac(): JSX.Element {
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>Changing the disk on macOS</legend>
|
||||
<p>
|
||||
macOS can mount the disk image directly. Click the button below to see
|
||||
the disk image in Finder. Then, double-click the image to mount it.
|
||||
</p>
|
||||
{this.renderMountButton("Finder")}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
public renderAdviceLinux(): JSX.Element {
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>Changing the disk on Linux</legend>
|
||||
<p>
|
||||
There are plenty of tools that enable Linux users to mount and modify
|
||||
disk images. The disk image used by windows95 is a raw "img" disk
|
||||
image and can probably be mounted using the <code>mount</code> tool,
|
||||
which is likely installed on your machine.
|
||||
</p>
|
||||
{this.renderMountButton("file viewer")}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMountButton(explorer: string) {
|
||||
return (
|
||||
<button className="btn" onClick={this.props.showDiskImage}>
|
||||
<img src="../../static/show-disk-image.png" />
|
||||
<span>Show disk image in {explorer}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
import * as React from "react";
|
||||
|
||||
export interface CardFloppyProps {
|
||||
setFloppyPath: (path: string) => void;
|
||||
floppyPath?: string;
|
||||
}
|
||||
|
||||
export class CardFloppy extends React.Component<CardFloppyProps, {}> {
|
||||
constructor(props: CardFloppyProps) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<section>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h2 className="card-title">Floppy Drive</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<input
|
||||
id="floppy-input"
|
||||
type="file"
|
||||
onChange={this.onChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<p>
|
||||
windows95 comes with a virtual floppy drive. If you have floppy
|
||||
disk images in the "img" format, you can mount them here.
|
||||
</p>
|
||||
<p>
|
||||
Back in the 90s and before CD-ROM became a popular format,
|
||||
software was typically distributed on floppy disks. Some
|
||||
developers have since released their apps or games for free,
|
||||
usually on virtual floppy disks using the "img" format.
|
||||
</p>
|
||||
<p>
|
||||
Once you've mounted a disk image, you might have to reboot your
|
||||
virtual windows95 machine from scratch.
|
||||
</p>
|
||||
<p id="floppy-path">
|
||||
{this.props.floppyPath
|
||||
? `Inserted Floppy Disk: ${this.props.floppyPath}`
|
||||
: `No floppy mounted`}
|
||||
</p>
|
||||
<button
|
||||
id="floppy-select"
|
||||
className="btn"
|
||||
onClick={() =>
|
||||
(document.querySelector("#floppy-input") as any).click()
|
||||
}
|
||||
>
|
||||
Mount floppy disk
|
||||
</button>
|
||||
<button id="floppy-reboot" className="btn">
|
||||
Reboot from scratch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
public onChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const floppyFile =
|
||||
event.target.files && event.target.files.length > 0
|
||||
? event.target.files[0]
|
||||
: null;
|
||||
|
||||
if (floppyFile) {
|
||||
this.props.setFloppyPath(floppyFile.path);
|
||||
} else {
|
||||
console.log(`Floppy: Input changed but no file selected`);
|
||||
}
|
||||
}
|
||||
}
|
161
src/renderer/card-settings.tsx
Normal file
|
@ -0,0 +1,161 @@
|
|||
import * as React from "react";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
interface CardSettingsProps {
|
||||
bootFromScratch: () => void;
|
||||
setFloppy: (file: File) => void;
|
||||
floppy?: File;
|
||||
}
|
||||
|
||||
interface CardSettingsState {
|
||||
isStateReset: boolean;
|
||||
}
|
||||
|
||||
export class CardSettings extends React.Component<
|
||||
CardSettingsProps,
|
||||
CardSettingsState
|
||||
> {
|
||||
constructor(props: CardSettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.onChangeFloppy = this.onChangeFloppy.bind(this);
|
||||
this.onResetState = this.onResetState.bind(this);
|
||||
|
||||
this.state = {
|
||||
isStateReset: false
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<section>
|
||||
<div className="card settings">
|
||||
<div className="card-header">
|
||||
<h2 className="card-title">
|
||||
<img src="../../static/settings.png" />
|
||||
Settings
|
||||
</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{this.renderFloppy()}
|
||||
<hr />
|
||||
{this.renderState()}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
public renderFloppy() {
|
||||
const { floppy } = this.props;
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>
|
||||
<img src="../../static/floppy.png" />
|
||||
Floppy
|
||||
</legend>
|
||||
<input
|
||||
id="floppy-input"
|
||||
type="file"
|
||||
onChange={this.onChangeFloppy}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<p>
|
||||
windows95 comes with a virtual floppy drive. It can mount floppy disk
|
||||
images in the "img" format.
|
||||
</p>
|
||||
<p>
|
||||
Back in the 90s and before CD-ROMs became a popular, software was
|
||||
typically distributed on floppy disks. Some developers have since
|
||||
released their apps or games for free, usually on virtual floppy disks
|
||||
using the "img" format.
|
||||
</p>
|
||||
<p>
|
||||
Once you've mounted a disk image, you might have to boot your virtual
|
||||
windows95 machine from scratch.
|
||||
</p>
|
||||
<p id="floppy-path">
|
||||
{floppy
|
||||
? `Inserted Floppy Disk: ${floppy.path}`
|
||||
: `No floppy mounted`}
|
||||
</p>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() =>
|
||||
(document.querySelector("#floppy-input") as any).click()
|
||||
}
|
||||
>
|
||||
<img src="../../static/select-floppy.png" />
|
||||
<span>Mount floppy disk</span>
|
||||
</button>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
public renderState() {
|
||||
const { isStateReset } = this.state;
|
||||
const { bootFromScratch } = this.props;
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>
|
||||
<img src="../../static/reset.png" />
|
||||
Reset machine state
|
||||
</legend>
|
||||
<div>
|
||||
<p>
|
||||
windows95 stores changes to your machine (like saved files) in a
|
||||
state file. If you encounter any trouble, you can reset your state
|
||||
or boot Windows 95 from scratch.{" "}
|
||||
<strong>All your changes will be lost.</strong>
|
||||
</p>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={this.onResetState}
|
||||
disabled={isStateReset}
|
||||
style={{ marginRight: "5px" }}
|
||||
>
|
||||
<img src="../../static/reset-state.png" />
|
||||
{isStateReset ? "State reset" : "Reset state"}
|
||||
</button>
|
||||
<button className="btn" onClick={bootFromScratch}>
|
||||
<img src="../../static/boot-fresh.png" />
|
||||
Boot from scratch
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a change in the floppy input
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
private onChangeFloppy(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const floppyFile =
|
||||
event.target.files && event.target.files.length > 0
|
||||
? event.target.files[0]
|
||||
: null;
|
||||
|
||||
if (floppyFile) {
|
||||
this.props.setFloppy(floppyFile);
|
||||
} else {
|
||||
console.log(`Floppy: Input changed but no file selected`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the state reset
|
||||
*/
|
||||
private async onResetState() {
|
||||
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
|
||||
await fs.remove(CONSTANTS.STATE_PATH);
|
||||
}
|
||||
|
||||
this.setState({ isStateReset: true });
|
||||
}
|
||||
}
|
|
@ -13,7 +13,9 @@ export class CardStart extends React.Component<CardStartProps, {}> {
|
|||
id="win95"
|
||||
onClick={this.props.startEmulator}
|
||||
>
|
||||
Start Windows 95
|
||||
<img src="../../static/run.png" />
|
||||
<span>Start Windows 95</span>
|
||||
<br />
|
||||
<br />
|
||||
<small>Hit ESC to lock or unlock your mouse</small>
|
||||
</div>
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import * as React from "react";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
export interface CardStateProps {
|
||||
bootFromScratch: () => void;
|
||||
}
|
||||
|
||||
export class CardState extends React.Component<CardStateProps, {}> {
|
||||
constructor(props: CardStateProps) {
|
||||
super(props);
|
||||
|
||||
this.onReset = this.onReset.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<section>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h2 className="card-title">Machine State</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p>
|
||||
windows95 stores any changes to your machine (like saved files) in
|
||||
a state file. If you encounter any trouble, you can either reset
|
||||
your state or boot Windows 95 from scratch.
|
||||
</p>
|
||||
<button className="btn" onClick={this.onReset}>
|
||||
Reset state
|
||||
</button>
|
||||
<button className="btn" onClick={this.props.bootFromScratch}>
|
||||
Reboot from scratch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
public async onReset() {
|
||||
if (fs.existsSync(CONSTANTS.STATE_PATH)) {
|
||||
await fs.remove(CONSTANTS.STATE_PATH);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +1,21 @@
|
|||
import * as React from "react";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import { ipcRenderer, remote, shell, webFrame } from "electron";
|
||||
import { ipcRenderer, remote, shell } from "electron";
|
||||
|
||||
import { CONSTANTS, IPC_COMMANDS } from "../constants";
|
||||
import { getDiskImageSize } from "../utils/disk-image-size";
|
||||
import { CardStart } from "./card-start";
|
||||
import { StartMenu } from "./start-menu";
|
||||
import { CardFloppy } from "./card-floppy";
|
||||
import { CardState } from "./card-state";
|
||||
import { CardSettings } from "./card-settings";
|
||||
import { EmulatorInfo } from "./emulator-info";
|
||||
import { CardDrive } from "./card-drive";
|
||||
|
||||
export interface EmulatorState {
|
||||
currentUiCard: string;
|
||||
emulator?: any;
|
||||
scale: number;
|
||||
floppyFile?: string;
|
||||
floppyFile?: File;
|
||||
isBootingFresh: boolean;
|
||||
isCursorCaptured: boolean;
|
||||
isInfoDisplayed: boolean;
|
||||
|
@ -33,6 +33,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
|||
this.stopEmulator = this.stopEmulator.bind(this);
|
||||
this.restartEmulator = this.restartEmulator.bind(this);
|
||||
this.resetEmulator = this.resetEmulator.bind(this);
|
||||
this.bootFromScratch = this.bootFromScratch.bind(this);
|
||||
|
||||
this.state = {
|
||||
isBootingFresh: false,
|
||||
|
@ -87,13 +88,22 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
|||
public setupUnloadListeners() {
|
||||
const handleClose = async () => {
|
||||
await this.saveState();
|
||||
|
||||
console.log(`Unload: Now done, quitting again.`);
|
||||
this.isQuitting = true;
|
||||
|
||||
setImmediate(() => {
|
||||
remote.app.quit();
|
||||
});
|
||||
};
|
||||
|
||||
window.onbeforeunload = event => {
|
||||
if (this.isQuitting) return;
|
||||
if (this.isResetting) return;
|
||||
if (this.isQuitting || this.isResetting) {
|
||||
console.log(`Unload: Not preventing`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Unload: Preventing to first save state`);
|
||||
|
||||
handleClose();
|
||||
event.preventDefault();
|
||||
|
@ -167,6 +177,10 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
|||
ipcRenderer.on(IPC_COMMANDS.ZOOM_OUT, () => {
|
||||
this.setScale(this.state.scale * 0.8);
|
||||
});
|
||||
|
||||
ipcRenderer.on(IPC_COMMANDS.ZOOM_RESET, () => {
|
||||
this.setScale(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -175,7 +189,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
|||
* 🤡
|
||||
*/
|
||||
public renderUI() {
|
||||
const { isRunning, currentUiCard } = this.state;
|
||||
const { isRunning, currentUiCard, floppyFile } = this.state;
|
||||
|
||||
if (isRunning) {
|
||||
return null;
|
||||
|
@ -183,15 +197,16 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
|||
|
||||
let card;
|
||||
|
||||
if (currentUiCard === "floppy") {
|
||||
if (currentUiCard === "settings") {
|
||||
card = (
|
||||
<CardFloppy
|
||||
setFloppyPath={floppyFile => this.setState({ floppyFile })}
|
||||
floppyPath={this.state.floppyFile}
|
||||
<CardSettings
|
||||
setFloppy={floppyFile => this.setState({ floppyFile })}
|
||||
bootFromScratch={this.bootFromScratch}
|
||||
floppy={floppyFile}
|
||||
/>
|
||||
);
|
||||
} else if (currentUiCard === "state") {
|
||||
card = <CardState bootFromScratch={this.bootFromScratch} />;
|
||||
} else if (currentUiCard === "drive") {
|
||||
card = <CardDrive showDiskImage={this.showDiskImage} />;
|
||||
} else {
|
||||
card = <CardStart startEmulator={this.startEmulator} />;
|
||||
}
|
||||
|
@ -348,16 +363,16 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
|||
private async saveState(): Promise<void> {
|
||||
const { emulator } = this.state;
|
||||
|
||||
return new Promise(resolve => {
|
||||
if (!emulator || !emulator.save_state) {
|
||||
console.log(`restoreState: No emulator present`);
|
||||
return;
|
||||
return resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
emulator.save_state(async (error: Error, newState: ArrayBuffer) => {
|
||||
if (error) {
|
||||
console.warn(`saveState: Could not save state`, error);
|
||||
return;
|
||||
return resolve();
|
||||
}
|
||||
|
||||
await fs.outputFile(CONSTANTS.STATE_PATH, Buffer.from(newState));
|
||||
|
|
|
@ -14,19 +14,23 @@ export class StartMenu extends React.Component<StartMenuProps, {}> {
|
|||
public render() {
|
||||
return (
|
||||
<nav className="nav nav-bottom">
|
||||
<a onClick={this.navigate} href="#" id="start" className="nav-logo">
|
||||
<img src="../../static/start.png" alt="" />
|
||||
<a onClick={this.navigate} href="#" id="start" className="nav-link">
|
||||
<img src="../../static/start.png" alt="Start" />
|
||||
<span>Start</span>
|
||||
</a>
|
||||
<div className="nav-menu">
|
||||
<a onClick={this.navigate} href="#" id="floppy" className="nav-link">
|
||||
Floppy Disk
|
||||
<a
|
||||
onClick={this.navigate}
|
||||
href="#"
|
||||
id="settings"
|
||||
className="nav-link"
|
||||
>
|
||||
<img src="../../static/settings.png" />
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a onClick={this.navigate} href="#" id="state" className="nav-link">
|
||||
Reset Machine
|
||||
</a>
|
||||
<a onClick={this.navigate} href="#" id="disk" className="nav-link">
|
||||
Modify C: Drive
|
||||
<a onClick={this.navigate} href="#" id="drive" className="nav-link">
|
||||
<img src="../../static/drive.png" />
|
||||
<span>Modify C: Drive</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
BIN
static/boot-fresh.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
static/drive.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
static/floppy.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
|
@ -12,5 +12,15 @@
|
|||
<body class="paused windows95">
|
||||
<div id="app"></div>
|
||||
<script src="../src/renderer/app.tsx"></script>
|
||||
<link rel="prefetch" href="../static/boot-fresh.png" />
|
||||
<link rel="prefetch" href="../static/drive.png" />
|
||||
<link rel="prefetch" href="../static/floppy.png" />
|
||||
<link rel="prefetch" href="../static/reset-state.png" />
|
||||
<link rel="prefetch" href="../static/reset.png" />
|
||||
<link rel="prefetch" href="../static/run.png" />
|
||||
<link rel="prefetch" href="../static/select-floppy.png" />
|
||||
<link rel="prefetch" href="../static/settings.png" />
|
||||
<link rel="prefetch" href="../static/show-disk-image.png" />
|
||||
<link rel="prefetch" href="../static/start.png" />
|
||||
</body>
|
||||
</html>
|
BIN
static/reset-state.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
static/reset.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
static/run.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
static/select-floppy.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
static/settings.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
static/show-disk-image.png
Normal file
After Width: | Height: | Size: 1.9 KiB |