Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94021edb61 | ||
|
|
6f2025ffc0 | ||
|
|
3a7b37fff0 | ||
|
|
16eb63e13b | ||
|
|
54fe721f4f | ||
|
|
6dee2f45a2 | ||
|
|
e7e047b0a0 | ||
|
|
5a334abb13 | ||
|
|
aacfae7ada | ||
|
|
9b87b77570 | ||
|
|
e6a0d931af | ||
|
|
973580d60b | ||
|
|
8fcf5eaed3 | ||
|
|
e15d918fb3 | ||
|
|
b442c6db08 | ||
|
|
5c946bbca4 | ||
|
|
c9e45a9f39 | ||
|
|
bc42ce3231 | ||
|
|
d91e72ccc5 | ||
|
|
bd40f00f8d | ||
|
|
1cbfca7451 | ||
|
|
7710c4b7af | ||
|
|
4cce1f0740 | ||
|
|
f8ae78f247 | ||
|
|
62f8eb2696 | ||
|
|
da4b0dd728 | ||
|
|
6cc05fa042 | ||
|
|
dda3707a23 | ||
|
|
a4bcd7fb61 | ||
|
|
17a8139346 | ||
|
|
489c7312d0 | ||
|
|
c3537ae330 | ||
|
|
c483871df9 | ||
|
|
e66cbd70db | ||
|
|
19a1bbc002 | ||
|
|
ef57e3a7fe | ||
|
|
7eae250c2a | ||
|
|
33db389d59 | ||
|
|
61f3269a45 | ||
|
|
e5d897c663 | ||
|
|
a7ae665adc | ||
|
|
bea2267f42 | ||
|
|
a55d08fafc | ||
|
|
97702cb01b | ||
|
|
12160a1ac4 | ||
|
|
3dd50db272 | ||
|
|
7b92d33584 | ||
|
|
24a1c30502 | ||
|
|
7ce0863ae8 | ||
|
|
90ec67fb16 | ||
|
|
9cab8e46f6 | ||
|
|
03b39d76b5 | ||
|
|
d8b4a139ac | ||
|
|
9f4771bf26 | ||
|
|
552b97eec5 | ||
|
|
6c0f00170c | ||
|
|
e3b9a839f5 | ||
|
|
238b07b7dd | ||
|
|
9dc1e422ff | ||
|
|
ebe7427385 | ||
|
|
3e3bee2062 | ||
|
|
c93b6878a9 | ||
|
|
d2e26ef5d1 | ||
|
|
c41befae64 |
BIN
.github/images/linux.png
vendored
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
.github/images/macos.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
.github/images/windows.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
32
.github/workflows/build.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 18.x
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -28,22 +28,38 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- name: Install
|
||||
run: yarn
|
||||
run: yarn --frozen-lockfile
|
||||
- name: lint
|
||||
run: yarn lint
|
||||
build:
|
||||
needs: lint
|
||||
name: Build (${{ matrix.os }})
|
||||
name: Build (${{ matrix.os }} - ${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# Build for supported platforms
|
||||
# https://github.com/electron/electron-packager/blob/ebcbd439ff3e0f6f92fa880ff28a8670a9bcf2ab/src/targets.js#L9
|
||||
# 32-bit Linux unsupported as of 2019: https://www.electronjs.org/blog/linux-32bit-support
|
||||
os: [ macOS-latest, ubuntu-latest, windows-latest ]
|
||||
arch: [ x64, arm64 ]
|
||||
include:
|
||||
- os: windows-latest
|
||||
arch: ia32
|
||||
- os: ubuntu-latest
|
||||
arch: armv7l
|
||||
# Publishing artifacts for multiple Windows architectures has
|
||||
# a bug which can cause the wrong architecture to be downloaded
|
||||
# for an update, so until that is fixed, only build Windows x64
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 18.x
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -83,17 +99,13 @@ jobs:
|
||||
run: yarn
|
||||
- name: Make
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: yarn make --arch=all
|
||||
run: yarn make --arch=${{ matrix.arch }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WINDOWS_CODESIGN_FILE: ${{ steps.write_file.outputs.filePath }}
|
||||
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
|
||||
# - name: Archive production artifacts
|
||||
# uses: actions/upload-artifact@v2
|
||||
# with:
|
||||
# name: ${{ matrix.os }}
|
||||
# path: out/make/**/*
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
16
.gitignore
vendored
@@ -1,6 +1,16 @@
|
||||
node_modules
|
||||
out
|
||||
src/images
|
||||
.DS_Store
|
||||
images
|
||||
dist
|
||||
|
||||
/images*/
|
||||
/helper-images/
|
||||
|
||||
dist
|
||||
!.github/images
|
||||
*.code-workspace
|
||||
*.pfx
|
||||
|
||||
Microsoft.Trusted.Signing.Client*
|
||||
trusted-signing-metadata.json
|
||||
.env
|
||||
electron-windows-sign.log
|
||||
|
||||
21
HELP.md
@@ -11,25 +11,4 @@ back to Window Mode. (Thanks to @DisplacedGamers for that wisdom)
|
||||
On the app's home screen, select "Settings" in the lower menu. Then, delete your
|
||||
machine's state before starting it again - this time hopefully without issues.
|
||||
|
||||
## I want to install additional apps or games
|
||||
|
||||
If you are running macOS, or Linux, you can probably "mount" the
|
||||
virtual hard drive used by `windows95` to add files. Hit the "Modify C: Drive"
|
||||
button, which will take you to the disk image.
|
||||
|
||||
On macOS, double-click the disk image to open it.
|
||||
|
||||
On Windows 10, Windows will _think_ that it can open up the image, but will
|
||||
actually fail to do so. Use a tool [like OSFMount][osfmount] to mount your
|
||||
disk image.
|
||||
|
||||
On Linux, search the Internet for instructions on how to mount an `img` disk
|
||||
image on your distribution. It's likely that you'll be able to run `mount`
|
||||
with the image as input.
|
||||
|
||||
[osfmount]: https://www.osforensics.com/tools/mount-disk-images.html
|
||||
|
||||
## What's the FrontPage Username and Password?
|
||||
Username: windows95
|
||||
|
||||
Password: password
|
||||
|
||||
97
README.md
@@ -3,10 +3,95 @@
|
||||
This is Windows 95, running in an [Electron](https://electronjs.org/) app. Yes, it's the full thing. I'm sorry.
|
||||
|
||||
## Downloads
|
||||
| | Windows | macOS | Linux |
|
||||
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Standalone Download | 📦[Standalone, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.2/windows95-win32-ia32-2.2.2.zip) <br /> 📦[Standalone, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.2/windows95-win32-x64-2.2.2.zip) | 📦[Standalone](https://github.com/felixrieseberg/windows95/releases/download/v2.2.2/windows95-darwin-x64-2.2.2.zip) | |
|
||||
| Installer | 💽[Setup, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.2/windows95-2.2.2-setup-x64.exe) <br /> 💽[Setup, 32-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.2/windows95-2.2.2-setup-ia32.exe) | | 💽[deb, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.2/windows95_2.2.2_amd64.deb) <br /> 💽[rpm, 64-bit](https://github.com/felixrieseberg/windows95/releases/download/v2.2.2/windows95-2.2.2-1.x86_64.rpm) |
|
||||
|
||||
<table class="is-fullwidth">
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="./.github/images/windows.png" width="24"><br />
|
||||
Windows
|
||||
</td>
|
||||
<td>
|
||||
<span>32-bit</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-setup-ia32.exe">
|
||||
💿 Installer
|
||||
</a> |
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-win32-ia32-3.1.1.zip">
|
||||
📦 Standalone Zip
|
||||
</a>
|
||||
<br />
|
||||
<span>64-bit</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-setup-x64.exe">
|
||||
💿 Installer
|
||||
</a> |
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-win32-x64-3.1.1.zip">
|
||||
📦 Standalone Zip
|
||||
</a><br />
|
||||
<span>ARM64</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-setup-arm64.exe">
|
||||
💿 Installer
|
||||
</a> |
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-win32-arm64-3.1.1.zip">
|
||||
📦 Standalone Zip
|
||||
</a><br />
|
||||
<span>
|
||||
❓ Don't know what kind of chip you have? Hit start, enter "processor" for info.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="./.github/images/macos.png" width="24"><br />
|
||||
macOS
|
||||
</td>
|
||||
<td>
|
||||
<span>Intel Processor</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-darwin-x64-3.1.1.zip">
|
||||
📦 Standalone Zip
|
||||
</a><br />
|
||||
<span>Apple M1 Processor</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-darwin-arm64-3.1.1.zip">
|
||||
📦 Standalone Zip
|
||||
</a><br />
|
||||
<span>
|
||||
❓ Don't know what kind of chip you have? Learn more at <a href="https://support.apple.com/en-us/HT211814">apple.com</a>.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="./.github/images/linux.png" width="24"><br />
|
||||
Linux
|
||||
</td>
|
||||
<td>
|
||||
<span>64-bit</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-1.x86_64.rpm">
|
||||
💿 rpm
|
||||
</a> |
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95_3.1.1_amd64.deb">
|
||||
💿 deb
|
||||
</a><br />
|
||||
<span>ARM64</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-1.arm64.rpm">
|
||||
💿 rpm
|
||||
</a> |
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95_3.1.1_arm64.deb">
|
||||
💿 deb
|
||||
</a><br />
|
||||
<span>ARMv7 (armhf)</span>
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95-3.1.1-1.armv7hl.rpm">
|
||||
💿 rpm
|
||||
</a> |
|
||||
<a href="https://github.com/felixrieseberg/windows95/releases/download/v3.1.1/windows95_3.1.1_armhf.deb">
|
||||
💿 deb
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<hr />
|
||||
|
||||

|
||||
|
||||
@@ -23,7 +108,7 @@ You'll likely be better off with an actual virtualization app, but the short ans
|
||||
|
||||
## Credits
|
||||
|
||||
99% of the work was done over at [v86](https://github.com/copy/v86/) by Copy.
|
||||
99% of the work was done over at [v86](https://github.com/copy/v86/) by Copy aka Fabian Hemmer and his contributors.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -44,6 +129,8 @@ Unpack the `images` folder into the `src` folder, creating this layout:
|
||||
|
||||
Once you've done so, run `npm install` and `npm start` to run your local build.
|
||||
|
||||
If you want to tinker with the image or make a new one, check out the [QEMU docs](./docs/qemu.md).
|
||||
|
||||
## Other Questions
|
||||
|
||||
* [MS-DOS seems to brick the screen](./HELP.md#ms-dos-seems-to-brick-the-screen)
|
||||
|
||||
BIN
assets/boot.gif
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
bios/seabios.bin
BIN
bios/vgabios.bin
@@ -35,5 +35,5 @@ xhost +
|
||||
```
|
||||
4. run
|
||||
```
|
||||
docker run -it -e DISPLAY=host.docker.internal:1 toolboc/windows95
|
||||
docker run -it -e DISPLAY=host.docker.internal:0 toolboc/windows95
|
||||
```
|
||||
|
||||
48
docs/qemu.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# QEMU Instructions
|
||||
|
||||
The image built here was made with QEMU. In this doc, I'm keeping instructions
|
||||
around.
|
||||
|
||||
Disk image creation
|
||||
|
||||
```sh
|
||||
qemu-img create -f raw windows95_v4.raw 1G
|
||||
```
|
||||
|
||||
ISO CD image creation
|
||||
|
||||
```sh
|
||||
hdiutil makehybrid -o output.iso /path/to/folder -iso -joliet
|
||||
```
|
||||
|
||||
Installation
|
||||
```sh
|
||||
qemu-system-i386 \
|
||||
-cdrom Win95_OSR25.iso \
|
||||
-m 128 \
|
||||
-hda windows95.img \
|
||||
-device sb16 \
|
||||
-nic user,model=ne2k_pci \
|
||||
-fda Win95_boot.img \
|
||||
-boot a \
|
||||
-M pc,acpi=off \
|
||||
-cpu pentium
|
||||
```
|
||||
|
||||
- Boot from floppy
|
||||
- Run `fdisk` and `format c:`
|
||||
- Run `D:\setup.exe` with `24796-OEM-0014736-66386`
|
||||
- After completing setup and restarting your computer, you might get an IOS Windows protection error
|
||||
- Use `fix95cpu.ima` as a bootable floppy to fix
|
||||
- Use `vga-driver.iso` to install different video driver
|
||||
|
||||
```sh
|
||||
qemu-system-i386 \
|
||||
-m 128 \
|
||||
-hda images/windows95.img \
|
||||
-device sb16 \
|
||||
-M pc,acpi=off \
|
||||
-cpu pentium \
|
||||
-netdev user,id=mynet0 \
|
||||
-device ne2k_isa,netdev=mynet0,irq=10
|
||||
```
|
||||
@@ -2,13 +2,32 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const package = require('./package.json');
|
||||
|
||||
if (process.env['WINDOWS_CODESIGN_FILE']) {
|
||||
const certPath = path.join(__dirname, 'win-certificate.pfx');
|
||||
const certExists = fs.existsSync(certPath);
|
||||
require('dotenv').config()
|
||||
|
||||
if (certExists) {
|
||||
process.env['WINDOWS_CODESIGN_FILE'] = certPath;
|
||||
}
|
||||
process.env.TEMP = process.env.TMP = `C:\\Users\\FelixRieseberg\\AppData\\Local\\Temp`
|
||||
|
||||
const FLAGS = {
|
||||
SIGNTOOL_PATH: process.env.SIGNTOOL_PATH,
|
||||
AZURE_CODE_SIGNING_DLIB: process.env.AZURE_CODE_SIGNING_DLIB || path.join(__dirname, 'Microsoft.Trusted.Signing.Client.1.0.60/bin/x64/Azure.CodeSigning.Dlib.dll'),
|
||||
AZURE_METADATA_JSON: process.env.AZURE_METADATA_JSON || path.resolve(__dirname, 'trusted-signing-metadata.json'),
|
||||
AZURE_TENANT_ID: process.env.AZURE_TENANT_ID,
|
||||
AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID,
|
||||
AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET,
|
||||
APPLE_ID: process.env.APPLE_ID,
|
||||
APPLE_ID_PASSWORD: process.env.APPLE_ID_PASSWORD,
|
||||
}
|
||||
|
||||
fs.writeFileSync(FLAGS.AZURE_METADATA_JSON, JSON.stringify({
|
||||
Endpoint: process.env.AZURE_CODE_SIGNING_ENDPOINT || "https://wcus.codesigning.azure.net",
|
||||
CodeSigningAccountName: process.env.AZURE_CODE_SIGNING_ACCOUNT_NAME,
|
||||
CertificateProfileName: process.env.AZURE_CODE_SIGNING_CERTIFICATE_PROFILE_NAME,
|
||||
}, null, 2));
|
||||
|
||||
const windowsSign = {
|
||||
signToolPath: FLAGS.SIGNTOOL_PATH,
|
||||
signWithParams: `/v /dlib ${FLAGS.AZURE_CODE_SIGNING_DLIB} /dmdf ${FLAGS.AZURE_METADATA_JSON}`,
|
||||
timestampServer: "http://timestamp.acs.microsoft.com",
|
||||
hashes: ["sha256"],
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -26,29 +45,38 @@ module.exports = {
|
||||
},
|
||||
osxSign: {
|
||||
identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)',
|
||||
'hardened-runtime': true,
|
||||
'gatekeeper-assess': false,
|
||||
'entitlements': 'assets/entitlements.plist',
|
||||
'entitlements-inherit': 'assets/entitlements.plist',
|
||||
'signature-flags': 'library'
|
||||
},
|
||||
osxNotarize: {
|
||||
appBundleId: 'com.felixrieseberg.macintoshjs',
|
||||
appleId: process.env['APPLE_ID'],
|
||||
appleIdPassword: process.env['APPLE_ID_PASSWORD'],
|
||||
ascProvider: 'LT94ZKYDCJ'
|
||||
appleId: FLAGS.APPLE_ID,
|
||||
appleIdPassword: FLAGS.APPLE_ID_PASSWORD,
|
||||
teamId: 'LT94ZKYDCJ'
|
||||
},
|
||||
windowsSign,
|
||||
ignore: [
|
||||
/\/assets(\/?)/,
|
||||
/\/docs(\/?)/,
|
||||
/\/tools(\/?)/,
|
||||
/\/src\/.*\.ts/,
|
||||
/\/test(\/?)/,
|
||||
/\/@types(\/?)/,
|
||||
/\/helper-images(\/?)/,
|
||||
/package-lock\.json/,
|
||||
/README\.md/,
|
||||
/tsconfig\.json/,
|
||||
/Dockerfile/,
|
||||
/issue_template\.md/,
|
||||
/HELP\.md/,
|
||||
/forge\.config\.js/,
|
||||
/\.github(\/?)/,
|
||||
/\.circleci(\/?)/,
|
||||
/\.vscode(\/?)/,
|
||||
/\.gitignore/,
|
||||
/\.gitattributes/,
|
||||
/\.eslintignore/,
|
||||
/\.eslintrc/,
|
||||
/\.prettierrc/,
|
||||
/\/Microsoft\.Trusted\.Signing\.Client.*/,
|
||||
/\/trusted-signing-metadata/,
|
||||
]
|
||||
},
|
||||
makers: [
|
||||
@@ -62,10 +90,11 @@ module.exports = {
|
||||
exe: 'windows95.exe',
|
||||
noMsi: true,
|
||||
remoteReleases: '',
|
||||
iconUrl: 'https://raw.githubusercontent.com/felixrieseberg/windows95/master/assets/icon.ico',
|
||||
loadingGif: './assets/boot.gif',
|
||||
setupExe: `windows95-${package.version}-setup-${arch}.exe`,
|
||||
setupIcon: path.resolve(__dirname, 'assets', 'icon.ico'),
|
||||
certificateFile: process.env['WINDOWS_CODESIGN_FILE'],
|
||||
certificatePassword: process.env['WINDOWS_CODESIGN_PASSWORD'],
|
||||
windowsSign
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
17729
package-lock.json
generated
Normal file
45
package.json
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"name": "windows95",
|
||||
"productName": "windows95",
|
||||
"version": "2.3.0",
|
||||
"version": "4.0.0",
|
||||
"description": "Windows 95, in an app. I'm sorry.",
|
||||
"main": "./dist/src/main/main",
|
||||
"main": "./dist/src/main/main.js",
|
||||
"scripts": {
|
||||
"start": "rimraf ./dist && electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"lint": "prettier --write src/**/*.{ts,tsx}",
|
||||
"lint": "prettier --write src/**/*.{ts,tsx} && npm run check-links",
|
||||
"less": "node ./tools/lessc.js",
|
||||
"tsc": "tsc -p tsconfig.json --noEmit"
|
||||
"tsc": "tsc -p tsconfig.json --noEmit",
|
||||
"check-links": "node tools/check-links.js",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Felix Rieseberg, felix@felixrieseberg.com",
|
||||
@@ -20,32 +22,29 @@
|
||||
"forge": "./forge.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"tslib": "^2.0.3",
|
||||
"update-electron-app": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-flatpak": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.54",
|
||||
"@electron-forge/publisher-github": "^6.0.0-beta.54",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/node": "^12.19.9",
|
||||
"@electron-forge/cli": "7.6.1",
|
||||
"@electron-forge/maker-deb": "7.6.1",
|
||||
"@electron-forge/maker-flatpak": "^7.6.1",
|
||||
"@electron-forge/maker-rpm": "^7.6.1",
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/publisher-github": "^7.6.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"electron": "11.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"electron": "34.2.0",
|
||||
"less": "^3.13.0",
|
||||
"node-abi": "^2.19.3",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^2.2.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"standard": "^16.0.3",
|
||||
"typescript": "^4.1.3"
|
||||
"parcel-bundler": "^1.12.5",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.5.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
32
patches/@electron+packager+18.3.6.patch
Normal file
@@ -0,0 +1,32 @@
|
||||
diff --git a/node_modules/@electron/packager/dist/win32.js b/node_modules/@electron/packager/dist/win32.js
|
||||
index 5399b3e..f3b6e88 100644
|
||||
--- a/node_modules/@electron/packager/dist/win32.js
|
||||
+++ b/node_modules/@electron/packager/dist/win32.js
|
||||
@@ -57,7 +57,26 @@ class WindowsApp extends platform_1.App {
|
||||
resOpts.iconPath = icon;
|
||||
}
|
||||
(0, common_1.debug)(`Running resedit with the options ${JSON.stringify(resOpts)}`);
|
||||
- await (0, resedit_1.resedit)(this.electronBinaryPath, resOpts);
|
||||
+
|
||||
+ // This causes segmentation faults for me on multiple machines
|
||||
+ // It's unclear why exactly but this spawn hack fixes it
|
||||
+ // await (0, resedit_1.resedit)(this.electronBinaryPath, resOpts);
|
||||
+
|
||||
+ const { spawnSync } = require('child_process');
|
||||
+ const resEditProcess = spawnSync('node', [
|
||||
+ 'C:\\Users\\FelixRieseberg\\Code\\windows95\\tools\\resedit.js',
|
||||
+ this.electronBinaryPath
|
||||
+ ], {
|
||||
+ stdio: 'inherit'
|
||||
+ });
|
||||
+
|
||||
+ if (resEditProcess.error) {
|
||||
+ throw resEditProcess.error;
|
||||
+ }
|
||||
+
|
||||
+ if (resEditProcess.status !== 0) {
|
||||
+ throw new Error(`Resedit process exited with code ${resEditProcess.status}`);
|
||||
+ }
|
||||
}
|
||||
async signAppIfSpecified() {
|
||||
const windowsSignOpt = this.opts.windowsSign;
|
||||
28
src/cache.ts
@@ -16,19 +16,17 @@ export async function clearStorageData() {
|
||||
return;
|
||||
}
|
||||
|
||||
await session.defaultSession.clearStorageData(
|
||||
{
|
||||
storages: [
|
||||
"appcache",
|
||||
"cookies",
|
||||
"filesystem",
|
||||
"indexdb",
|
||||
"localstorage",
|
||||
"shadercache",
|
||||
"websql",
|
||||
"serviceworkers",
|
||||
],
|
||||
quotas: ["temporary", "persistent", "syncable"],
|
||||
}
|
||||
);
|
||||
await session.defaultSession.clearStorageData({
|
||||
storages: [
|
||||
"appcache",
|
||||
"cookies",
|
||||
"filesystem",
|
||||
"indexdb",
|
||||
"localstorage",
|
||||
"shadercache",
|
||||
"websql",
|
||||
"serviceworkers",
|
||||
],
|
||||
quotas: ["temporary", "persistent", "syncable"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as path from "path";
|
||||
|
||||
const IMAGES_PATH = path.join(__dirname, "../../images");
|
||||
|
||||
export const CONSTANTS = {
|
||||
IMAGE_PATH: path.join(__dirname, "../../images/windows95.img"),
|
||||
IMAGES_PATH,
|
||||
IMAGE_PATH: path.join(IMAGES_PATH, "windows95.img"),
|
||||
IMAGE_DEFAULT_SIZE: 1073741824, // 1GB
|
||||
DEFAULT_STATE_PATH: path.join(__dirname, "../../images/default-state.bin")
|
||||
DEFAULT_STATE_PATH: path.join(IMAGES_PATH, "default-state.bin"),
|
||||
};
|
||||
|
||||
export const IPC_COMMANDS = {
|
||||
|
||||
@@ -21,4 +21,9 @@
|
||||
filter: blur(2px);
|
||||
z-index: -100;
|
||||
}
|
||||
|
||||
#emulator-text-screen {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
15
src/main/fileserver/encoding.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function encode(text: string) {
|
||||
// Convert to windows-1252 compatible string by removing unsupported chars
|
||||
let result = text.replaceAll(/[^\x00-\xFF]/g, '');
|
||||
|
||||
// If result would be empty, return original
|
||||
if (!result.trim()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getEncoding() {
|
||||
return `<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">`;
|
||||
}
|
||||
157
src/main/fileserver/fileserver.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { protocol, net } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { generateDirectoryListing } from './page-directory-listing';
|
||||
import { generateErrorPage } from './page-error';
|
||||
import { log } from '../logging';
|
||||
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
fullPath: string;
|
||||
stats: fs.Stats;
|
||||
}
|
||||
|
||||
export const APP_INTERCEPT = 'http://windows95/';
|
||||
export const MY_COMPUTER_INTERCEPT = 'http://my-computer/';
|
||||
|
||||
const interceptedUrls = [
|
||||
MY_COMPUTER_INTERCEPT,
|
||||
APP_INTERCEPT
|
||||
];
|
||||
|
||||
export function setupFileServer() {
|
||||
// Register protocol handler for our custom schema
|
||||
protocol.handle('http', async (request) => {
|
||||
if (!interceptedUrls.some(url => request.url.startsWith(url))) {
|
||||
return fetch(request.url, {
|
||||
headers: request.headers,
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { fullPath, decodedPath } = getFilePath(request.url);
|
||||
|
||||
log(`FileServer: Handling request for ${request.url}`, { fullPath, decodedPath });
|
||||
|
||||
// Check if path exists
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return new Response(generateErrorPage(
|
||||
'File or Directory Not Found',
|
||||
decodedPath
|
||||
), {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'text/html'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a directory
|
||||
const stats = await fs.promises.stat(fullPath);
|
||||
if (stats.isDirectory()) {
|
||||
// If we're in an app-intercept, check if there's an index.htm file in the directory
|
||||
if (request.url.startsWith(APP_INTERCEPT)) {
|
||||
const indexHtmlPath = path.join(fullPath, 'index.htm');
|
||||
if (fs.existsSync(indexHtmlPath)) {
|
||||
return serveFile(indexHtmlPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate directory listing
|
||||
const files = await fs.promises.readdir(fullPath);
|
||||
const listing = generateDirectoryListing(fullPath, files);
|
||||
return new Response(listing, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/html'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
return await serveFile(fullPath);
|
||||
} catch (error) {
|
||||
// Handle specific file read errors
|
||||
if (error.code === 'EACCES') {
|
||||
return new Response(generateErrorPage(
|
||||
'Access Denied',
|
||||
'You do not have permission to access this file'
|
||||
), {
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'text/html'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Re-throw other errors to be caught by outer try-catch
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorPage = generateErrorPage(
|
||||
'Internal Server Error',
|
||||
`An error occurred while processing your request: ${error.message}`
|
||||
);
|
||||
return new Response(errorPage, {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'text/html'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getFilePath(url: string) {
|
||||
let urlPath: string;
|
||||
let fullPath: string;
|
||||
let decodedPath: string;
|
||||
|
||||
if (url.startsWith(APP_INTERCEPT)) {
|
||||
fullPath = path.resolve(__dirname, '../../../static/www', url.replace(APP_INTERCEPT, ''));
|
||||
decodedPath = '.';
|
||||
} else if (url.startsWith(MY_COMPUTER_INTERCEPT)) {
|
||||
urlPath = url.replace(MY_COMPUTER_INTERCEPT, '');
|
||||
decodedPath = decodeURIComponent(urlPath);
|
||||
fullPath = path.join('/', decodedPath);
|
||||
} else {
|
||||
throw new Error('Invalid URL');
|
||||
}
|
||||
|
||||
return { fullPath, decodedPath };
|
||||
}
|
||||
|
||||
async function serveFile(fullPath: string): Promise<Response> {
|
||||
const fileData = await fs.promises.readFile(fullPath);
|
||||
|
||||
// Determine content type based on file extension
|
||||
const ext = path.extname(fullPath).toLowerCase();
|
||||
let contentType = 'application/octet-stream';
|
||||
|
||||
// Common content types
|
||||
const contentTypes: Record<string, string> = {
|
||||
'.htm': 'text/html',
|
||||
'.html': 'text/html',
|
||||
'.txt': 'text/plain',
|
||||
'.css': 'text/css',
|
||||
'.js': 'text/javascript',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif'
|
||||
};
|
||||
|
||||
if (ext in contentTypes) {
|
||||
contentType = contentTypes[ext];
|
||||
}
|
||||
|
||||
return new Response(fileData, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentType
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
72
src/main/fileserver/hide-files.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Stats } from "fs";
|
||||
import { settings } from "../settings";
|
||||
import { FileEntry } from "./fileserver";
|
||||
|
||||
const FILES_TO_HIDE_ON_DARWIN: string[] = [
|
||||
'.DS_Store',
|
||||
'.localized',
|
||||
'.Trashes',
|
||||
'.fseventsd',
|
||||
'.Spotlight-V100',
|
||||
'.file',
|
||||
'.hotfiles.btree',
|
||||
'.DocumentRevisions-V100',
|
||||
'.TemporaryItems',
|
||||
'.file (resource fork files)',
|
||||
'.VolumeIcon.icns',
|
||||
];
|
||||
|
||||
const FILES_TO_HIDE_ON_WINDOWS: string[] = [
|
||||
'desktop.ini',
|
||||
'Thumbs.db',
|
||||
'ehthumbs.db',
|
||||
'ehthumbs.db-shm',
|
||||
'ehthumbs.db-wal',
|
||||
];
|
||||
|
||||
const FILES_TO_HIDE_ON_LINUX: string[] = [];
|
||||
|
||||
export function shouldHideFile(file: FileEntry) {
|
||||
if (isHiddenFile(file) && !settings.get('isFileServerShowingHiddenFiles')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isSystemHiddenFile(file) && !settings.get('isFileServerShowingSystemHiddenFiles')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isHiddenFile(file: FileEntry) {
|
||||
if (process.platform === 'win32') {
|
||||
return (file.stats.mode & 0x2) === 0x2;
|
||||
} else {
|
||||
return file.name.startsWith('.');
|
||||
}
|
||||
}
|
||||
|
||||
export function isSystemHiddenFile(file: FileEntry) {
|
||||
return getFilesToHide().some(hiddenFile => file.name.endsWith(hiddenFile));
|
||||
}
|
||||
|
||||
let _filesToHide: string[];
|
||||
|
||||
function getFilesToHide() {
|
||||
if (_filesToHide) {
|
||||
return _filesToHide;
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
_filesToHide = FILES_TO_HIDE_ON_DARWIN;
|
||||
} else if (process.platform === 'win32') {
|
||||
_filesToHide = FILES_TO_HIDE_ON_WINDOWS;
|
||||
} else {
|
||||
_filesToHide = FILES_TO_HIDE_ON_LINUX;
|
||||
}
|
||||
|
||||
return _filesToHide;
|
||||
}
|
||||
|
||||
|
||||
|
||||
120
src/main/fileserver/page-directory-listing.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
import { APP_INTERCEPT, FileEntry, MY_COMPUTER_INTERCEPT } from "./fileserver";
|
||||
import { shouldHideFile } from "./hide-files";
|
||||
import { encode, getEncoding } from "./encoding";
|
||||
import { log } from "console";
|
||||
import { app } from "electron";
|
||||
|
||||
export function generateDirectoryListing(currentPath: string, files: string[]): string {
|
||||
const parentPath = path.dirname(currentPath || '/');
|
||||
const title = currentPath === '/' ? 'My Host Computer' : `Directory: ${encode(currentPath)}`;
|
||||
|
||||
// Get file info and sort (directories first, then alphabetically)
|
||||
const items = files
|
||||
.map(name => {
|
||||
const fullPath = path.join(currentPath, name);
|
||||
let stats: fs.Stats;
|
||||
try {
|
||||
stats = fs.statSync(fullPath);
|
||||
} catch (error) {
|
||||
log(`FileServer: Failed to get stats for ${fullPath}: ${error}`);
|
||||
stats = new fs.Stats();
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
fullPath,
|
||||
stats
|
||||
} as FileEntry;
|
||||
})
|
||||
.filter(entry => entry.stats && !shouldHideFile(entry))
|
||||
.sort((a, b) => {
|
||||
if (a.stats.isDirectory() !== b.stats.isDirectory()) {
|
||||
return a.stats.isDirectory() ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map(getFileLiHtml)
|
||||
.join('')
|
||||
|
||||
// Generate very simple HTML that works in IE 5.5
|
||||
return `
|
||||
<html>
|
||||
<head>
|
||||
${getEncoding()}
|
||||
<title>${title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>${title}</h2>
|
||||
<p>${getParentFolderLinkHtml(parentPath)} | ${getDesktopLinkHtml()} | ${getDownloadsLinkHtml()}</p>
|
||||
<p>
|
||||
<ul>
|
||||
${items}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
function getParentFolderLinkHtml(parentPath: string) {
|
||||
return `
|
||||
${getIconHtml('folder.gif')}
|
||||
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(parentPath)}">
|
||||
[Parent Directory]
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
function getDesktopLinkHtml() {
|
||||
const desktopPath = app.getPath('desktop');
|
||||
|
||||
return `
|
||||
${getIconHtml('desktop.gif')}
|
||||
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(desktopPath)}">
|
||||
Desktop
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
function getDownloadsLinkHtml() {
|
||||
const downloadsPath = app.getPath('downloads');
|
||||
|
||||
return `
|
||||
${getIconHtml('network.gif')}
|
||||
<a href="${MY_COMPUTER_INTERCEPT}${encodeURI(downloadsPath)}">
|
||||
Downloads
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
function getIconHtml(icon: string) {
|
||||
return `<img src="${APP_INTERCEPT}images/${icon}" style="vertical-align: middle; margin-right: 5px;" width="16" height="16">`;
|
||||
}
|
||||
|
||||
function getFileLiHtml(entry: FileEntry) {
|
||||
const encodedPath = encodeURI(entry.fullPath);
|
||||
const sizeDisplay = entry.stats.isDirectory() ? '' : ` (${formatFileSize(entry.stats.size)})`;
|
||||
const icon = entry.stats.isDirectory() ? getIconHtml('folder.gif') : getIconHtml('doc.gif');
|
||||
|
||||
return `<li>
|
||||
${icon}
|
||||
<a href="${MY_COMPUTER_INTERCEPT}${encodedPath}">
|
||||
${getDisplayName(entry)}
|
||||
</a>
|
||||
${sizeDisplay}
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function getDisplayName(entry: FileEntry) {
|
||||
return encode(entry.stats.isDirectory() ? `[${entry.name}]` : entry.name);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
22
src/main/fileserver/page-error.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getEncoding } from "./encoding";
|
||||
import { MY_COMPUTER_INTERCEPT } from "./fileserver";
|
||||
|
||||
export function generateErrorPage(errorMessage: string, requestedPath: string): string {
|
||||
return `
|
||||
<html>
|
||||
<head>
|
||||
${getEncoding()}
|
||||
<title>Error - File Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Error: ${errorMessage}</h2>
|
||||
<p>windows95 failed to find the file or directory on your host computer: <code>${requestedPath}</code></p>
|
||||
<p>Options:</p>
|
||||
<ul>
|
||||
<li><a href="${MY_COMPUTER_INTERCEPT}">Return to root directory</a></li>
|
||||
<li><a href="javascript:history.back()">Go back to previous page</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ipcMain, app } from 'electron';
|
||||
import { ipcMain, app } from "electron";
|
||||
import * as path from "path";
|
||||
|
||||
import { IPC_COMMANDS } from '../constants';
|
||||
import { IPC_COMMANDS } from "../constants";
|
||||
|
||||
export function setupIpcListeners() {
|
||||
ipcMain.handle(IPC_COMMANDS.GET_STATE_PATH, () => {
|
||||
return path.join(app.getPath("userData"), "state-v2.bin")
|
||||
return path.join(app.getPath("userData"), "state-v4.bin");
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_COMMANDS.APP_QUIT, () => {
|
||||
|
||||
3
src/main/logging.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function log(message: string, ...args: unknown[]) {
|
||||
console.log(`[${new Date().toLocaleString()}] ${message}`, ...args);
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import { shouldQuit } from "./squirrel";
|
||||
import { setupUpdates } from "./update";
|
||||
import { getOrCreateWindow } from "./windows";
|
||||
import { setupMenu } from "./menu";
|
||||
import { setupIpcListeners } from './ipc';
|
||||
import { setupIpcListeners } from "./ipc";
|
||||
import { setupSession } from "./session";
|
||||
import { setupFileServer } from './fileserver/fileserver';
|
||||
|
||||
/**
|
||||
* Handle the app's "ready" event. This is essentially
|
||||
@@ -15,11 +17,13 @@ import { setupIpcListeners } from './ipc';
|
||||
export async function onReady() {
|
||||
if (!isDevMode()) process.env.NODE_ENV = "production";
|
||||
|
||||
setupSession();
|
||||
setupIpcListeners();
|
||||
getOrCreateWindow();
|
||||
setupAboutPanel();
|
||||
setupMenu();
|
||||
setupUpdates();
|
||||
setupFileServer();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { app, shell, Menu, BrowserWindow, ipcMain } from "electron";
|
||||
import { app, shell, Menu, BrowserWindow, ipcMain, dialog } from "electron";
|
||||
|
||||
import { clearCaches } from "../cache";
|
||||
import { IPC_COMMANDS } from "../constants";
|
||||
import { isDevMode } from "../utils/devmode";
|
||||
import { log } from "./logging";
|
||||
|
||||
const LINKS = {
|
||||
homepage: "https://www.twitter.com/felixrieseberg",
|
||||
homepage: "https://www.felixrieseberg.com",
|
||||
repo: "https://github.com/felixrieseberg/windows95",
|
||||
credits: "https://github.com/felixrieseberg/windows95/blob/master/CREDITS.md",
|
||||
help: "https://github.com/felixrieseberg/windows95/blob/master/HELP.md",
|
||||
@@ -15,10 +16,10 @@ export async function setupMenu() {
|
||||
await createMenu();
|
||||
|
||||
ipcMain.on(IPC_COMMANDS.MACHINE_STARTED, () =>
|
||||
createMenu({ isRunning: true })
|
||||
createMenu({ isRunning: true }),
|
||||
);
|
||||
ipcMain.on(IPC_COMMANDS.MACHINE_STOPPED, () =>
|
||||
createMenu({ isRunning: false })
|
||||
createMenu({ isRunning: false }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,10 +27,10 @@ function send(cmd: string) {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
|
||||
if (windows[0]) {
|
||||
console.log(`Sending "${cmd}"`);
|
||||
log(`Sending "${cmd}"`);
|
||||
windows[0].webContents.send(cmd);
|
||||
} else {
|
||||
console.log(`Tried to send "${cmd}", but could not find window`);
|
||||
log(`Tried to send "${cmd}", but could not find window`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +164,20 @@ async function createMenu({ isRunning } = { isRunning: false }) {
|
||||
},
|
||||
{
|
||||
label: "Reset",
|
||||
click: () => send(IPC_COMMANDS.MACHINE_RESET),
|
||||
click: async () => {
|
||||
const result = await dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['Reset', 'Cancel'],
|
||||
defaultId: 1,
|
||||
title: 'Reset Machine',
|
||||
message: 'Are you sure you want to reset the machine?',
|
||||
detail: 'This will delete the machine state, including all changes you have made.',
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
send(IPC_COMMANDS.MACHINE_RESET);
|
||||
}
|
||||
},
|
||||
enabled: isRunning,
|
||||
},
|
||||
{
|
||||
|
||||
20
src/main/session.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { session } from "electron";
|
||||
|
||||
export function setupSession() {
|
||||
const s = session.defaultSession;
|
||||
|
||||
s.webRequest.onBeforeSendHeaders(
|
||||
(details, callback) => {
|
||||
callback({ requestHeaders: { Origin: '*', ...details.requestHeaders } });
|
||||
},
|
||||
);
|
||||
|
||||
s.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
'Access-Control-Allow-Origin': ['*'],
|
||||
...details.responseHeaders,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
72
src/main/settings.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { app } from 'electron';
|
||||
|
||||
export interface Settings {
|
||||
isFileServerEnabled: boolean;
|
||||
isFileServerShowingHiddenFiles: boolean;
|
||||
isFileServerShowingSystemHiddenFiles: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
isFileServerEnabled: true,
|
||||
isFileServerShowingHiddenFiles: false,
|
||||
isFileServerShowingSystemHiddenFiles: false,
|
||||
};
|
||||
|
||||
class SettingsManager {
|
||||
private filePath: string;
|
||||
private data: Settings;
|
||||
|
||||
constructor() {
|
||||
this.filePath = path.join(app.getPath('userData'), 'settings.json');
|
||||
this.data = this.load();
|
||||
}
|
||||
|
||||
private load(): Settings {
|
||||
try {
|
||||
if (fs.existsSync(this.filePath)) {
|
||||
const fileContent = fs.readFileSync(this.filePath, 'utf8');
|
||||
const parsed = JSON.parse(fileContent);
|
||||
|
||||
return {
|
||||
...DEFAULT_SETTINGS,
|
||||
...parsed,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
}
|
||||
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
try {
|
||||
fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
get(key: keyof Settings): any {
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
set(key: keyof Settings, value: any): void {
|
||||
this.data[key] = value;
|
||||
this.save();
|
||||
}
|
||||
|
||||
delete(key: keyof Settings): void {
|
||||
delete this.data[key];
|
||||
this.save();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.data = DEFAULT_SETTINGS;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
export const settings = new SettingsManager();
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { BrowserWindow, shell } from "electron";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
@@ -14,14 +14,27 @@ export function getOrCreateWindow(): BrowserWindow {
|
||||
nodeIntegration: true,
|
||||
sandbox: false,
|
||||
webviewTag: false,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
|
||||
// mainWindow.webContents.toggleDevTools();
|
||||
mainWindow.loadFile("./dist/static/index.html");
|
||||
|
||||
mainWindow.webContents.on("will-navigate", (event, url) =>
|
||||
handleNavigation(event, url),
|
||||
);
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function handleNavigation(event: Electron.Event, url: string) {
|
||||
if (url.startsWith("http")) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export interface Win95Window extends Window {
|
||||
emulator: any;
|
||||
win95: {
|
||||
app: App;
|
||||
};
|
||||
}
|
||||
|
||||
declare let window: Win95Window;
|
||||
|
||||
/**
|
||||
* The top-level class controlling the whole app. This is *not* a React component,
|
||||
* but it does eventually render all components.
|
||||
@@ -27,8 +36,8 @@ export class App {
|
||||
}
|
||||
}
|
||||
|
||||
window["win95"] = window["win95"] || {
|
||||
window.win95 = window.win95 || {
|
||||
app: new App(),
|
||||
};
|
||||
|
||||
window["win95"].app.setup();
|
||||
window.win95.app.setup();
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
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="https://google.com">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,12 +1,13 @@
|
||||
import * as React from "react";
|
||||
import * as fs from "fs-extra";
|
||||
|
||||
import { getStatePath } from './utils/get-state-path';
|
||||
import { resetState } from "./utils/reset-state";
|
||||
|
||||
interface CardSettingsProps {
|
||||
bootFromScratch: () => void;
|
||||
setFloppy: (file: File) => void;
|
||||
setCdrom: (cdrom: File) => void;
|
||||
floppy?: File;
|
||||
cdrom?: File;
|
||||
}
|
||||
|
||||
interface CardSettingsState {
|
||||
@@ -21,6 +22,7 @@ export class CardSettings extends React.Component<
|
||||
super(props);
|
||||
|
||||
this.onChangeFloppy = this.onChangeFloppy.bind(this);
|
||||
this.onChangeCdrom = this.onChangeCdrom.bind(this);
|
||||
this.onResetState = this.onResetState.bind(this);
|
||||
|
||||
this.state = {
|
||||
@@ -39,6 +41,8 @@ export class CardSettings extends React.Component<
|
||||
</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{this.renderCdrom()}
|
||||
<hr />
|
||||
{this.renderFloppy()}
|
||||
<hr />
|
||||
{this.renderState()}
|
||||
@@ -48,6 +52,44 @@ export class CardSettings extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
public renderCdrom() {
|
||||
// CD is currently not working, so.. let's return nothing.
|
||||
return null;
|
||||
|
||||
const { cdrom } = this.props;
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>
|
||||
<img src="../../static/cdrom.png" />
|
||||
CD-ROM
|
||||
</legend>
|
||||
<input
|
||||
id="cdrom-input"
|
||||
type="file"
|
||||
onChange={this.onChangeCdrom}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<p>
|
||||
windows95 comes with a virtual CD drive. It can mount images in the
|
||||
"iso" format.
|
||||
</p>
|
||||
<p id="floppy-path">
|
||||
{cdrom ? `Inserted CD: ${cdrom?.path}` : `No CD mounted`}
|
||||
</p>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() =>
|
||||
(document.querySelector("#cdrom-input") as any).click()
|
||||
}
|
||||
>
|
||||
<img src="../../static/select-cdrom.png" />
|
||||
<span>Mount CD</span>
|
||||
</button>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
public renderFloppy() {
|
||||
const { floppy } = this.props;
|
||||
|
||||
@@ -79,7 +121,7 @@ export class CardSettings extends React.Component<
|
||||
</p>
|
||||
<p id="floppy-path">
|
||||
{floppy
|
||||
? `Inserted Floppy Disk: ${floppy.path}`
|
||||
? `Inserted Floppy Disk: ${floppy.name}`
|
||||
: `No floppy mounted`}
|
||||
</p>
|
||||
<button
|
||||
@@ -148,16 +190,29 @@ export class CardSettings extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a change in the cdrom input
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
private onChangeCdrom(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const CdromFile =
|
||||
event.target.files && event.target.files.length > 0
|
||||
? event.target.files[0]
|
||||
: null;
|
||||
|
||||
if (CdromFile) {
|
||||
this.props.setCdrom(CdromFile);
|
||||
} else {
|
||||
console.log(`Cdrom: Input changed but no file selected`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the state reset
|
||||
*/
|
||||
private async onResetState() {
|
||||
const statePath = await getStatePath();
|
||||
|
||||
if (fs.existsSync(statePath)) {
|
||||
await fs.remove(statePath);
|
||||
}
|
||||
|
||||
await resetState();
|
||||
this.setState({ isStateReset: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export class EmulatorInfo extends React.Component<
|
||||
|
||||
if (!emulator) {
|
||||
console.log(
|
||||
`Emulator info: Tried to install listeners, but emulator not defined yet.`
|
||||
`Emulator info: Tried to install listeners, but emulator not defined yet.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export class EmulatorInfo extends React.Component<
|
||||
}
|
||||
|
||||
// TypeScript think's we're using a Node.js setInterval. We're not.
|
||||
this.cpuInterval = (setInterval(this.cpuCount, 500) as unknown) as number;
|
||||
this.cpuInterval = setInterval(this.cpuCount, 500) as unknown as number;
|
||||
|
||||
// Disk
|
||||
emulator.add_listener("ide-read-start", this.onIDEReadStart);
|
||||
@@ -104,7 +104,7 @@ export class EmulatorInfo extends React.Component<
|
||||
|
||||
if (!emulator) {
|
||||
console.log(
|
||||
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`
|
||||
`Emulator info: Tried to uninstall listeners, but emulator not defined yet.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as fs from "fs-extra";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { ipcRenderer, shell } from "electron";
|
||||
import { ipcRenderer, shell, webUtils } from "electron";
|
||||
|
||||
import { CONSTANTS, IPC_COMMANDS } from "../constants";
|
||||
import { getDiskImageSize } from "../utils/disk-image-size";
|
||||
@@ -9,14 +9,18 @@ import { CardStart } from "./card-start";
|
||||
import { StartMenu } from "./start-menu";
|
||||
import { CardSettings } from "./card-settings";
|
||||
import { EmulatorInfo } from "./emulator-info";
|
||||
import { CardDrive } from "./card-drive";
|
||||
import { getStatePath } from './utils/get-state-path';
|
||||
import { getStatePath } from "./utils/get-state-path";
|
||||
import { Win95Window } from "./app";
|
||||
import { resetState } from "./utils/reset-state";
|
||||
|
||||
declare let window: Win95Window;
|
||||
|
||||
export interface EmulatorState {
|
||||
currentUiCard: string;
|
||||
currentUiCard: "start" | "settings";
|
||||
emulator?: any;
|
||||
scale: number;
|
||||
floppyFile?: File;
|
||||
cdromFile?: File;
|
||||
isBootingFresh: boolean;
|
||||
isCursorCaptured: boolean;
|
||||
isInfoDisplayed: boolean;
|
||||
@@ -42,16 +46,14 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
isRunning: false,
|
||||
currentUiCard: "start",
|
||||
isInfoDisplayed: true,
|
||||
scale: 1,
|
||||
// We can start pretty large
|
||||
// If it's too large, it'll just grow until it hits borders
|
||||
scale: 2,
|
||||
};
|
||||
|
||||
this.setupInputListeners();
|
||||
this.setupIpcListeners();
|
||||
this.setupUnloadListeners();
|
||||
|
||||
if (document.location.hash.includes("AUTO_START")) {
|
||||
this.startEmulator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +181,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
* 🤡
|
||||
*/
|
||||
public renderUI() {
|
||||
const { isRunning, currentUiCard, floppyFile } = this.state;
|
||||
const { isRunning, currentUiCard, floppyFile, cdromFile } = this.state;
|
||||
|
||||
if (isRunning) {
|
||||
return null;
|
||||
@@ -191,12 +193,12 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
card = (
|
||||
<CardSettings
|
||||
setFloppy={(floppyFile) => this.setState({ floppyFile })}
|
||||
setCdrom={(cdromFile) => this.setState({ cdromFile })}
|
||||
bootFromScratch={this.bootFromScratch}
|
||||
floppy={floppyFile}
|
||||
cdrom={cdromFile}
|
||||
/>
|
||||
);
|
||||
} else if (currentUiCard === "drive") {
|
||||
card = <CardDrive showDiskImage={this.showDiskImage} />;
|
||||
} else {
|
||||
card = <CardStart startEmulator={this.startEmulator} />;
|
||||
}
|
||||
@@ -205,7 +207,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
<>
|
||||
{card}
|
||||
<StartMenu
|
||||
navigate={(target) => this.setState({ currentUiCard: target })}
|
||||
navigate={(target) => this.setState({ currentUiCard: target as "start" | "settings" })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -220,8 +222,8 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
{this.renderInfo()}
|
||||
{this.renderUI()}
|
||||
<div id="emulator">
|
||||
<div></div>
|
||||
<canvas></canvas>
|
||||
<div id="emulator-text-screen"></div>
|
||||
<canvas id="emulator-canvas"></canvas>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -258,12 +260,9 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
*/
|
||||
public showDiskImage() {
|
||||
// Contents/Resources/app/dist/static
|
||||
const imagePath = path.join(__dirname, "../../images/windows95.img");
|
||||
console.log(`Showing disk image in ${CONSTANTS.IMAGE_PATH}`);
|
||||
|
||||
console.log(`Showing disk image in ${imagePath}`);
|
||||
``;
|
||||
|
||||
shell.showItemInFolder(imagePath);
|
||||
shell.showItemInFolder(CONSTANTS.IMAGE_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,31 +271,52 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
private async startEmulator() {
|
||||
document.body.classList.remove("paused");
|
||||
|
||||
const imageSize = await getDiskImageSize();
|
||||
const cdromPath = this.state.cdromFile
|
||||
? webUtils.getPathForFile(this.state.cdromFile)
|
||||
: null;
|
||||
|
||||
const options = {
|
||||
wasm_path: path.join(__dirname, "build/v86.wasm"),
|
||||
memory_size: 128 * 1024 * 1024,
|
||||
video_memory_size: 32 * 1024 * 1024,
|
||||
screen_container: document.getElementById("emulator"),
|
||||
vga_memory_size: 64 * 1024 * 1024,
|
||||
screen: {
|
||||
container: document.getElementById("emulator"),
|
||||
scale: 0
|
||||
},
|
||||
preserve_mac_from_state_image: true,
|
||||
net_device: {
|
||||
relay_url: "fetch",
|
||||
type: "ne2k",
|
||||
},
|
||||
bios: {
|
||||
url: "../../bios/seabios.bin",
|
||||
url: path.join(__dirname, "../../bios/seabios.bin"),
|
||||
},
|
||||
vga_bios: {
|
||||
url: "../../bios/vgabios.bin",
|
||||
url: path.join(__dirname, "../../bios/vgabios.bin"),
|
||||
},
|
||||
hda: {
|
||||
url: "../../images/windows95.img",
|
||||
url: CONSTANTS.IMAGE_PATH,
|
||||
async: true,
|
||||
size: imageSize,
|
||||
},
|
||||
fda: {
|
||||
buffer: this.state.floppyFile,
|
||||
size: await getDiskImageSize(CONSTANTS.IMAGE_PATH),
|
||||
},
|
||||
fda: this.state.floppyFile
|
||||
? {
|
||||
buffer: this.state.floppyFile,
|
||||
}
|
||||
: undefined,
|
||||
cdrom: cdromPath
|
||||
? {
|
||||
url: cdromPath,
|
||||
async: true,
|
||||
size: await getDiskImageSize(cdromPath),
|
||||
}
|
||||
: undefined,
|
||||
boot_order: 0x132,
|
||||
};
|
||||
|
||||
console.log(`🚜 Starting emulator with options`, options);
|
||||
|
||||
window["emulator"] = new V86Starter(options);
|
||||
window["emulator"] = new V86(options);
|
||||
|
||||
// New v86 instance
|
||||
this.setState({
|
||||
@@ -316,6 +336,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
|
||||
this.lockMouse();
|
||||
this.state.emulator.run();
|
||||
this.state.emulator.screen_set_scale(this.state.scale);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@@ -345,19 +366,23 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
|
||||
await this.saveState();
|
||||
this.unlockMouse();
|
||||
emulator.stop();
|
||||
await emulator.stop();
|
||||
this.setState({ isRunning: false });
|
||||
this.resetCanvas();
|
||||
|
||||
document.body.classList.add("paused");
|
||||
ipcRenderer.send(IPC_COMMANDS.MACHINE_STOPPED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the emulator by reloading the whole page (lol)
|
||||
* Reset the emulator by reloading the whole page
|
||||
*/
|
||||
private async resetEmulator() {
|
||||
this.isResetting = true;
|
||||
document.location.hash = `#AUTO_START`;
|
||||
|
||||
await this.stopEmulator();
|
||||
await resetState();
|
||||
|
||||
document.location.reload();
|
||||
}
|
||||
|
||||
@@ -369,49 +394,45 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
const { emulator } = this.state;
|
||||
const statePath = await getStatePath();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (!emulator || !emulator.save_state) {
|
||||
console.log(`restoreState: No emulator present`);
|
||||
return resolve();
|
||||
}
|
||||
if (!emulator || !emulator.save_state) {
|
||||
console.log(`restoreState: No emulator present`);
|
||||
return;
|
||||
}
|
||||
|
||||
emulator.save_state(async (error: Error, newState: ArrayBuffer) => {
|
||||
if (error) {
|
||||
console.warn(`saveState: Could not save state`, error);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
await fs.outputFile(statePath, Buffer.from(newState));
|
||||
|
||||
console.log(`saveState: Saved state to ${statePath}`);
|
||||
|
||||
resolve();
|
||||
try {
|
||||
const newState = await emulator.save_state();
|
||||
await fs.promises.writeFile(statePath, Buffer.from(newState), {
|
||||
flush: true
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`saveState: Could not save state`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores state to the emulator.
|
||||
*/
|
||||
private async restoreState() {
|
||||
const { emulator } = this.state;
|
||||
const { emulator, isBootingFresh } = this.state;
|
||||
const state = await this.getState();
|
||||
|
||||
// Nothing to do with if we don't have a state
|
||||
if (!state) {
|
||||
if (isBootingFresh) {
|
||||
console.log(`restoreState: Booting fresh, not restoring.`);
|
||||
return;
|
||||
} else if (!state) {
|
||||
console.log(`restoreState: No state present, not restoring.`);
|
||||
}
|
||||
|
||||
if (!emulator) {
|
||||
return;
|
||||
} else if (!emulator) {
|
||||
console.log(`restoreState: No emulator present`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.state.emulator.restore_state(state);
|
||||
await this.state.emulator.restore_state(state);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`State: Could not read state file. Maybe none exists?`,
|
||||
error
|
||||
`restoreState: Could not read state file. Maybe none exists?`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -430,6 +451,8 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
|
||||
if (fs.existsSync(statePath)) {
|
||||
return fs.readFileSync(statePath).buffer;
|
||||
} else {
|
||||
console.log(`getState: No state file found at ${statePath}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -456,7 +479,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
emulator.lock_mouse();
|
||||
} else {
|
||||
console.warn(
|
||||
`Emulator: Tried to lock mouse, but no emulator or not running`
|
||||
`Emulator: Tried to lock mouse, but no emulator or not running`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -493,4 +516,16 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
this.state.emulator.keyboard_send_scancodes(scancodes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the canvas
|
||||
*/
|
||||
private resetCanvas() {
|
||||
const canvas = document.getElementById("emulator-canvas");
|
||||
|
||||
if (canvas instanceof HTMLCanvasElement) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
src/renderer/global.d.ts
vendored
@@ -1,2 +1,2 @@
|
||||
declare const V86Starter: any;
|
||||
declare const V86: any;
|
||||
declare const win95: any;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2012-2018, Fabian Hemmer
|
||||
Copyright (c) 2012, The v86 contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
@@ -19,8 +19,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those
|
||||
of the authors and should not be interpreted as representing official policies,
|
||||
either expressed or implied, of the FreeBSD Project.
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
BIN
src/renderer/lib/build/v86.wasm
Executable file
@@ -28,10 +28,6 @@ export class StartMenu extends React.Component<StartMenuProps, {}> {
|
||||
<img src="../../static/settings.png" />
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a onClick={this.navigate} href="#" id="drive" className="nav-link">
|
||||
<img src="../../static/drive.png" />
|
||||
<span>Modify C: Drive</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { IPC_COMMANDS } from '../../constants';
|
||||
import { ipcRenderer } from "electron";
|
||||
import { IPC_COMMANDS } from "../../constants";
|
||||
|
||||
let _statePath = '';
|
||||
let _statePath = "";
|
||||
|
||||
export async function getStatePath(): Promise<string> {
|
||||
if (_statePath) {
|
||||
@@ -9,6 +9,5 @@ export async function getStatePath(): Promise<string> {
|
||||
}
|
||||
|
||||
const statePath = await ipcRenderer.invoke(IPC_COMMANDS.GET_STATE_PATH);
|
||||
return _statePath = statePath;
|
||||
return (_statePath = statePath);
|
||||
}
|
||||
|
||||
|
||||
14
src/renderer/utils/reset-state.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import fs from "fs";
|
||||
import { getStatePath } from "./get-state-path";
|
||||
|
||||
export async function resetState() {
|
||||
const statePath = await getStatePath();
|
||||
|
||||
if (fs.existsSync(statePath)) {
|
||||
try {
|
||||
await fs.promises.unlink(statePath);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete state file: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "fs-extra";
|
||||
import * as fs from "fs";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
@@ -7,9 +7,9 @@ import { CONSTANTS } from "../constants";
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
export async function getDiskImageSize() {
|
||||
export async function getDiskImageSize(path: string) {
|
||||
try {
|
||||
const stats = await fs.stat(CONSTANTS.IMAGE_PATH);
|
||||
const stats = await fs.promises.stat(path);
|
||||
|
||||
if (stats) {
|
||||
return stats.size;
|
||||
|
||||
BIN
static/cdrom.png
Normal file
|
After Width: | Height: | Size: 672 B |
BIN
static/drive.png
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -7,7 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="../src/less/vendor/95css.css">
|
||||
<link rel="stylesheet" href="../src/less/root.less">
|
||||
<script src="../src/renderer/lib/libv86.js"></script>
|
||||
<!-- libv86 -->
|
||||
</head>
|
||||
<body class="paused windows95">
|
||||
<div id="app"></div>
|
||||
|
||||
BIN
static/select-cdrom.png
Normal file
|
After Width: | Height: | Size: 519 B |
20
static/www/apps.htm
Normal file
@@ -0,0 +1,20 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>windows95 Help</title>
|
||||
</head>
|
||||
<body bgcolor="#C0C0C0">
|
||||
<table width="100%" cellpadding="10" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<font face="Arial" color="#000000">
|
||||
<font size="5"><b>windows95 Apps & Games</b></font>
|
||||
<hr>
|
||||
|
||||
<p>I've installed a few apps and games for you to try out. Check out the Games folder on the desktop!</p>
|
||||
<p>If you want to try other games, I recommend trying to find them on the Internet Archive. On your host computer, visit https://archive.org, then find the "Classic PC Games" category. Once downloaded, you can import them into windows95 from <a href="http://my-computer">your host's Download folder</a>.</p>
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/www/buttons/macos.gif
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
static/www/buttons/madewithelectron.gif
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/www/buttons/msie.gif
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
31
static/www/credits.htm
Normal file
@@ -0,0 +1,31 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Windows 95 Credits</title>
|
||||
</head>
|
||||
<body bgcolor="#C0C0C0">
|
||||
<table width="100%" cellpadding="10" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<font face="Arial" color="#000000">
|
||||
<font size="5"><b>windows95 Credits</b></font>
|
||||
<hr>
|
||||
|
||||
<h3>Emulation Engine</h3>
|
||||
<p>
|
||||
None of this would be possible without the people working on v86, in particular Fabian Hemmer aka copy.
|
||||
You can visit his website at <a href="http://copy.sh" target="_blank">copy.sh</a>. It also wouldn't be
|
||||
possible without the QEMU project. If you enjoy running old systems, you probably want QEMU - windows95
|
||||
is merely a toy, QEMU lets you actually run old systems in a stable manner.
|
||||
</p>
|
||||
|
||||
<h3>Electron</h3>
|
||||
<p>
|
||||
Electron is a framework for building desktop applications using web technologies. It's what powers windows95.
|
||||
You can visit the project's website at electronjs.org (in a modern browser, not in windows95).
|
||||
</p>
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
34
static/www/help.htm
Normal file
@@ -0,0 +1,34 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>windows95 Help</title>
|
||||
</head>
|
||||
<body bgcolor="#C0C0C0">
|
||||
<table width="100%" cellpadding="10" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<font face="Arial" color="#000000">
|
||||
<font size="5"><b>windows95 Help</b></font>
|
||||
<hr>
|
||||
|
||||
<h3>MS-DOS Display Issues</h3>
|
||||
<p>If MS-DOS seems to mess up the screen:</p>
|
||||
<ol>
|
||||
<li>Hit <code>Alt + Enter</code> to make the command screen "Full Screen" (as far as Windows 95 is concerned)</li>
|
||||
<li>This should restore the display from the garbled mess you see and allow you to access the Command Prompt</li>
|
||||
<li>Press Alt-Enter again to leave Full Screen and go back to Window Mode</li>
|
||||
</ol>
|
||||
<p><i>(Thanks to @DisplacedGamers for that wisdom)</i></p>
|
||||
|
||||
<h3>windows95 Stuck in Bad State</h3>
|
||||
<p>If windows95 becomes unresponsive or stuck:</p>
|
||||
<ol>
|
||||
<li>On the app's home screen, select "Settings" in the lower menu</li>
|
||||
<li>Delete your machine's state before starting it again</li>
|
||||
<li>This should resolve the issue when you restart</li>
|
||||
</ol>
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
37
static/www/home.htm
Normal file
@@ -0,0 +1,37 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to Windows 95!</title>
|
||||
</head>
|
||||
<body bgcolor="#C0C0C0">
|
||||
<table width="100%" cellpadding="10" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<center>
|
||||
<marquee scrollamount="3">
|
||||
<font face="Arial" size="6" color="#000000">
|
||||
<blink>Welcome to Windows 95!</blink>
|
||||
</font>
|
||||
</marquee>
|
||||
</center>
|
||||
|
||||
<font face="Arial" color="#000000">
|
||||
<p>Hi, I'm Felix, the maker behind windows95. I hope you're having fun!</p>
|
||||
|
||||
<p>Reach out to me in a modern browser (as in: not in windows95) on <font color="#0000FF">felixrieseberg.com</font> or find me on Bluesky at <font color="#0000FF">@felixrieseberg</font>.</p>
|
||||
|
||||
<hr width="75%">
|
||||
<a name="internet"></a>
|
||||
<font size="5" color="#000000"><img src="images/ie.gif" width="16" height="16" border="0" align="absmiddle"> <b>The Internet!</b></font>
|
||||
<hr width="75%">
|
||||
|
||||
<p>In a major update since the last version, windows95 now has working Internet! That said, most modern websites will not work, so brace yourself. I recommend using <a href="http://theoldnet.com/" target="_blank">The Old Net</a> to travel back in time.</p>
|
||||
</font>
|
||||
|
||||
<center>
|
||||
<font size="2" color="#000000">Last updated: 2025</font>
|
||||
</center>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/www/images/bg.gif
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
static/www/images/desktop.gif
Normal file
|
After Width: | Height: | Size: 500 B |
BIN
static/www/images/doc.gif
Normal file
|
After Width: | Height: | Size: 103 B |
BIN
static/www/images/folder.gif
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/www/images/help.gif
Normal file
|
After Width: | Height: | Size: 265 B |
BIN
static/www/images/ie.gif
Normal file
|
After Width: | Height: | Size: 661 B |
BIN
static/www/images/network.gif
Normal file
|
After Width: | Height: | Size: 605 B |
BIN
static/www/images/programs.gif
Normal file
|
After Width: | Height: | Size: 361 B |
21
static/www/index.htm
Normal file
@@ -0,0 +1,21 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Welcome to Windows 95!</title>
|
||||
</head>
|
||||
|
||||
<frameset cols="200,*" border="0" framespacing="0" frameborder="NO">
|
||||
<frame src="navigation.htm" name="nav" scrolling="auto" noresize>
|
||||
<frame src="home.htm" name="main" scrolling="auto" noresize>
|
||||
<noframes>
|
||||
<body bgcolor="#000080">
|
||||
<font face="Arial" color="#FFFFFF">
|
||||
<h2>Frame Alert!</h2>
|
||||
<p>This page uses frames, but your browser doesn't support them.</p>
|
||||
<p>Please upgrade to Netscape Navigator 2.0 or Internet Explorer 3.0!</p>
|
||||
</font>
|
||||
</body>
|
||||
</noframes>
|
||||
</frameset>
|
||||
|
||||
</html>
|
||||
46
static/www/navigation.htm
Normal file
@@ -0,0 +1,46 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Navigation</title>
|
||||
</head>
|
||||
<body bgcolor="#C0C0C0" background="images/bg.gif">
|
||||
<table width="100%" cellpadding="4" cellspacing="1" bgcolor="#000000">
|
||||
<tr><td bgcolor="#C0C0C0">
|
||||
<font face="Arial" size="2" color="#000000">
|
||||
<img src="images/folder.gif" width="16" height="16" border="0" align="absmiddle"> <b>Navigation</b>
|
||||
</font>
|
||||
</td></tr>
|
||||
<tr><td bgcolor="#C0C0C0">
|
||||
<font face="Arial" size="2" color="#000000">
|
||||
<img src="images/desktop.gif" width="16" height="16" border="0" align="absmiddle"> <a href="home.htm" target="main">Home</a>
|
||||
</font>
|
||||
</td></tr>
|
||||
<tr><td bgcolor="#C0C0C0">
|
||||
<font face="Arial" size="2" color="#000000">
|
||||
<img src="images/programs.gif" width="16" height="16" border="0" align="absmiddle"> <a href="apps.htm" target="main">Apps & Games</a>
|
||||
</font>
|
||||
</td></tr>
|
||||
<tr><td bgcolor="#C0C0C0">
|
||||
<font face="Arial" size="2" color="#000000">
|
||||
<img src="images/help.gif" width="16" height="16" border="0" align="absmiddle"> <a href="help.htm" target="main">Help</a>
|
||||
</font>
|
||||
</td></tr>
|
||||
<tr><td bgcolor="#C0C0C0">
|
||||
<font face="Arial" size="2" color="#000000">
|
||||
<img src="images/doc.gif" width="16" height="16" border="0" align="absmiddle"> <a href="credits.htm" target="main">Credits</a>
|
||||
</font>
|
||||
</td></tr>
|
||||
</table>
|
||||
<br>
|
||||
<center>
|
||||
<p>
|
||||
<font face="Arial" size="1" color="#000000">
|
||||
Best viewed with<br>
|
||||
Internet Explorer 5.5 and windows95
|
||||
</font>
|
||||
</p>
|
||||
<img src="buttons/madewithelectron.gif" width="88" height="31" border="0" align="absmiddle">
|
||||
<img src="buttons/macos.gif" width="88" height="31" border="0" align="absmiddle">
|
||||
<img src="buttons/msie.gif" width="88" height="31" border="0" align="absmiddle">
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
38
tools/check-links.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const fs = require('fs/promises')
|
||||
const path = require('path')
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
const LINK_RGX = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/g;
|
||||
|
||||
async function main() {
|
||||
const readmePath = path.join(__dirname, '../README.md')
|
||||
const readme = await fs.readFile(readmePath, 'utf-8')
|
||||
const links = readme.match(LINK_RGX)
|
||||
let failed = false
|
||||
|
||||
for (const link of links) {
|
||||
try {
|
||||
const response = await fetch(link, { method: 'HEAD' })
|
||||
|
||||
if (!response.ok) {
|
||||
// If we're inside GitHub's release asset server, we just ran into AWS not allowing
|
||||
// HEAD requests, which is different from a 404.
|
||||
if (!response.url.startsWith('https://github-production-release-asset')) {
|
||||
throw new Error (`HTTP Error Response: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ ${link}`);
|
||||
} catch (error) {
|
||||
failed = true
|
||||
|
||||
console.log(`❌ ${link}\n${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
process.exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
const Bundler = require('parcel-bundler')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
async function copyLib() {
|
||||
const target = path.join(__dirname, '../dist/static')
|
||||
const lib = path.join(__dirname, '../src/renderer/lib')
|
||||
const index = path.join(target, 'index.html')
|
||||
|
||||
// Copy in lib
|
||||
await fs.promises.cp(lib, target, { recursive: true });
|
||||
|
||||
// Patch so that fs.read is used
|
||||
const libv86path = path.join(target, 'libv86.js')
|
||||
const libv86 = fs.readFileSync(libv86path, 'utf-8')
|
||||
|
||||
let patchedLibv86 = libv86.replace('k.load_file="undefined"===typeof XMLHttpRequest?pa:qa', 'k.load_file=pa')
|
||||
patchedLibv86 = patchedLibv86.replace('H.exportSymbol=function(a,b){"undefined"!==typeof module&&"undefined"!==typeof module.exports?module.exports[a]=b:"undefined"!==typeof window?window[a]=b:"function"===typeof importScripts&&(self[a]=b)}', 'H.exportSymbol=function(a,b){"undefined"!==typeof window?window[a]=b:"undefined"!==typeof module&&"undefined"!==typeof module.exports?module.exports[a]=b:"function"===typeof importScripts&&(self[a]=b)}')
|
||||
patchedLibv86 = patchedLibv86.replace('this.fetch=fetch;', 'this.fetch=(...args)=>fetch(...args);')
|
||||
|
||||
fs.writeFileSync(libv86path, patchedLibv86)
|
||||
|
||||
// Overwrite
|
||||
const indexContents = fs.readFileSync(index, 'utf-8');
|
||||
const replacedContents = indexContents.replace('<!-- libv86 -->', '<script src="libv86.js"></script>')
|
||||
fs.writeFileSync(index, replacedContents)
|
||||
}
|
||||
|
||||
async function compileParcel (options = {}) {
|
||||
const entryFiles = [
|
||||
@@ -27,7 +52,7 @@ async function compileParcel (options = {}) {
|
||||
logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
|
||||
hmr: false, // Enable or disable HMR while watching
|
||||
hmrPort: 0, // The port the HMR socket runs on, defaults to a random free port (0 in node.js resolves to a random free port)
|
||||
sourceMaps: true, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
|
||||
sourceMaps: false, // Enable or disable sourcemaps, defaults to enabled (minified builds currently always create sourcemaps)
|
||||
hmrHostname: '', // A hostname for hot module reload, default to ''
|
||||
detailedReport: false, // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled,
|
||||
...options
|
||||
@@ -38,6 +63,8 @@ async function compileParcel (options = {}) {
|
||||
// Run the bundler, this returns the main bundle
|
||||
// Use the events if you're using watch mode as this promise will only trigger once and not for every rebuild
|
||||
await bundler.bundle()
|
||||
|
||||
await copyLib();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
27
tools/resedit.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const path = require('path');
|
||||
|
||||
const resedit = require('../node_modules/@electron/packager/dist/resedit.js')
|
||||
const package = require('../package.json');
|
||||
|
||||
const exePath = process.argv[process.argv.length - 1]
|
||||
|
||||
console.log(exePath)
|
||||
|
||||
async function main() {
|
||||
|
||||
await resedit.resedit(exePath, {
|
||||
"productVersion": package.version,
|
||||
"fileVersion": package.version,
|
||||
"productName": package.productName,
|
||||
"icon": path.join(__dirname, "../assets/icon.ico"),
|
||||
"win32Metadata": {
|
||||
"FileDescription": package.productName,
|
||||
"InternalName": package.name,
|
||||
"OriginalFilename": `${package.name}.exe`,
|
||||
"ProductName": package.productName,
|
||||
"CompanyName": package.author
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -8,22 +8,20 @@
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"lib": [
|
||||
"es2017",
|
||||
"es2023",
|
||||
"dom"
|
||||
],
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedParameters": true,
|
||||
"importHelpers": true,
|
||||
"noEmitHelpers": false,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"pretty": true,
|
||||
"target": "es2017",
|
||||
"target": "es2023",
|
||||
"jsx": "react",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
|
||||