diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..d2c4536 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,15 @@ +# macintosh.js Credits + +This app by Felix Rieseberg. The real work was done by the people below: + +**Emulator**: Basilisk II, a 68k Macintosh emulator, by [Christian Bauer et al](http://basilisk.cebix.net), modified and compiled [with Emscripten](https://jamesfriend.com.au/basilisk-ii-classic-mac-emulator-in-the-browser) by [James Friend](https://jamesfriend.com.au). + +**Installed software** from vintage computing archives: [WinWorldPC](https://winworldpc.com), [Macintosh Garden](https://macintoshgarden.org), and [Macintosh Repository](https://www.macintoshrepository.org/). + +This software is not affiliated with nor authorized by Apple. It is provided for educational purposes only. This is an unstable toy and should not be expected to work properly. + +# Licenses + +The [source code for this app can be found on GitHub](https://github.com/felixrieseberg/macintosh). + +Basilisk II and its components are released under the GNU GPL. See [LICENSE](src/basilisk/LICENSE.txt) for details. diff --git a/README.md b/README.md new file mode 100644 index 0000000..38b554e --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# macintosh.js + +This is Mac OS 8, 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/macintosh.js/releases/download/v2.2.1/macintosh.js-2.2.1-win32-standalone-ia32.zip)
📦[Standalone, 64-bit](https://github.com/felixrieseberg/macintosh.js/releases/download/v2.2.1/macintosh.js-2.2.1-win32-standalone-x64.zip) | 📦[Standalone](https://github.com/felixrieseberg/macintosh.js/releases/download/v2.2.1/macintosh.js-macos-2.2.1.zip) | | +| Installer | 💽[Setup, 64-bit](https://github.com/felixrieseberg/macintosh.js/releases/download/v2.2.1/macintosh.js-2.2.1-setup-win32-x64.exe)
💽[Setup, 32-bit](https://github.com/felixrieseberg/macintosh.js/releases/download/v2.2.1/macintosh.js-2.2.1-setup-win32-ia32.exe) | | 💽[deb, 64-bit](https://github.com/felixrieseberg/macintosh.js/releases/download/v2.2.1/macintosh.js-linux-2.2.1_amd64.deb)
💽[rpm, 64-bit](https://github.com/felixrieseberg/macintosh.js/releases/download/v2.2.1/macintosh.js-linux-2.2.1.x86_64.rpm) | + +![Screenshot]() + +## Does it work? +Yes! Quite well, actually - on macOS, Windows, and Linux. Bear in mind that this is written entirely in JavaScript, so please adjust your expectations. The virtual machine is emulating a Motorola CPU, which Apple used before switching to IBM's PowerPC architecture in the late 1990s. + +## Should this have been a native app? +Absolutely. + +## Does it run my favorite game or app? +You'll likely be better off with an actual virtualization app, but the short answer is yes. In fact, you'll find various games and demos preinstalled, thanks to an old MacWorld Demo CD from 1997. Namely, Oregon Trail, Duke Nukem 3D, Civilization II, Alley 19 Bowling, Damage Incorporated, and Dungeons & Dragons. + +There are also various apps and trials preinstalled, including Photoshop 3, Premiere 4, Illustrator 5.5, StuffIt Expander, the Apple Web Page Construction Kit, and more. + +## Can I transfer files from and to the machine? + +Yes, you can. Click on the "Help" button at the bottom of the running app to see instructions. + +## Can I connect to the Internet? + +No. For what it's worth, the web was quite different 30 years ago - and you wouldn't be able to open even Google. However, Internet Explorer and Netscape are installed, as is the "Web Sharing Server", if you want to play around a bit. + +## License + +This project is provided for educational purposes only. It is not affiliated with and has +not been approved by Apple. diff --git a/assets/icon.icns b/assets/icon.icns new file mode 100644 index 0000000..6f01630 Binary files /dev/null and b/assets/icon.icns differ diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000..a04cba9 Binary files /dev/null and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..06acacf Binary files /dev/null and b/assets/icon.png differ diff --git a/forge.config.js b/forge.config.js new file mode 100644 index 0000000..bcc4417 --- /dev/null +++ b/forge.config.js @@ -0,0 +1,80 @@ +const path = require('path'); +const package = require('./package.json'); + +module.exports = { + hooks: { + postPackage: require('./tools/notarize') + }, + packagerConfig: { + asar: false, + icon: path.resolve(__dirname, 'assets', 'icon'), + appBundleId: 'com.felixrieseberg.macintoshjs', + appCategoryType: 'public.app-category.developer-tools', + win32metadata: { + CompanyName: 'Felix Rieseberg', + OriginalFilename: 'macintosh.js' + }, + osxSign: { + identity: 'Developer ID Application: Felix Rieseberg (LT94ZKYDCJ)', + 'hardened-runtime': true, + 'gatekeeper-assess': false, + 'entitlements': 'static/entitlements.plist', + 'entitlements-inherit': 'static/entitlements.plist', + 'signature-flags': 'library' + }, + ignore: [ + /\/assets(\/?)/, + /\/docs(\/?)/, + /\/tools(\/?)/, + /package-lock\.json/, + /README\.md/, + /CREDITS\.md/, + /issue_template\.md/, + /HELP\.md/, + ] + }, + makers: [ + { + name: '@electron-forge/maker-squirrel', + platforms: ['win32'], + config: (arch) => { + return { + name: 'macintosh.js', + authors: 'Felix Rieseberg', + exe: 'macintoshjs.exe', + noMsi: true, + remoteReleases: '', + setupExe: `macintoshjs-${package.version}-setup-${arch}.exe`, + setupIcon: path.resolve(__dirname, 'assets', 'icon.ico'), + certificateFile: process.env.WINDOWS_CERTIFICATE_FILE, + certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD + } + } + }, + { + name: '@electron-forge/maker-zip', + platforms: ['darwin', 'win32'] + }, + { + name: '@electron-forge/maker-deb', + platforms: ['linux'] + }, + { + name: '@electron-forge/maker-rpm', + platforms: ['linux'] + } + ], + publishers: [ + { + name: '@electron-forge/publisher-github', + config: { + repository: { + owner: 'felixrieseberg', + name: 'macintosh.js' + }, + draft: true, + prerelease: true + } + } + ] +}; diff --git a/package.json b/package.json index 79f74b7..5d20e57 100644 --- a/package.json +++ b/package.json @@ -19,34 +19,11 @@ }, "license": "MIT", "config": { - "forge": { - "packagerConfig": {}, - "makers": [ - { - "name": "@electron-forge/maker-squirrel", - "config": { - "name": "macintosh" - } - }, - { - "name": "@electron-forge/maker-zip", - "platforms": [ - "darwin" - ] - }, - { - "name": "@electron-forge/maker-deb", - "config": {} - }, - { - "name": "@electron-forge/maker-rpm", - "config": {} - } - ] - } + "forge": "./forge.config.js" }, "dependencies": { - "electron-squirrel-startup": "^1.0.0" + "electron-squirrel-startup": "^1.0.0", + "update-electron-app": "^1.5.0" }, "devDependencies": { "@electron-forge/cli": "6.0.0-beta.52", diff --git a/src/basilisk/BasiliskII-worker-boot.js b/src/basilisk/BasiliskII-worker-boot.js index 967ed2c..a1acfe3 100644 --- a/src/basilisk/BasiliskII-worker-boot.js +++ b/src/basilisk/BasiliskII-worker-boot.js @@ -62,10 +62,22 @@ function addAutoloader(module) { true ); } catch (error) { + postMessage("showMessageBoxSync", { + type: 'error', + title: 'Could not transfer file', + message: `We tried to transfer ${fileName} to the virtual machine, but failed. The error was: ${error}` + }); + console.error(`loadDatafiles: Failed to preload ${fileName}`, error); } }); } catch (error) { + postMessage("showMessageBoxSync", { + type: 'error', + title: 'Could not transfer files', + message: `We tried to transfer files to the virtual machine, but failed. The error was: ${error}` + }); + console.error(`loadDatafiles: Failed to copyFilesAtPath`, error); } } @@ -115,6 +127,12 @@ function writeSafely(filePath, fileData) { return new Promise((resolve) => { fs.writeFile(filePath, fileData, (error) => { if (error) { + postMessage("showMessageBoxSync", { + type: 'error', + title: 'Could not save files', + message: `We tried to save files from the virtual machine, but failed. The error was: ${error}` + }); + console.error(`Disk save: Encountered error for ${filePath}`, error); } else { console.log(`Disk save: Finished writing ${filePath}`); @@ -158,6 +176,12 @@ async function saveFilesInPath(folderPath) { ); } } catch (error) { + postMessage("showMessageBoxSync", { + type: 'error', + title: 'Could not safe file', + message: `We tried to save the file "${file}" from the virtual machine, but failed. The error was: ${error}` + }); + console.error(`Disk save: Could not write ${file}`, error); } } diff --git a/src/main/devmode.js b/src/main/devmode.js index a9d710b..83176de 100644 --- a/src/main/devmode.js +++ b/src/main/devmode.js @@ -1,23 +1,26 @@ -const fs = require('fs'); -const path = require('path'); -const { app } = require('electron'); +const fs = require("fs"); +const path = require("path"); +const { app } = require("electron"); -const appDataPath = app.getPath('userData'); -const devFilePath = path.join(appDataPath, 'developer'); +const appDataPath = app.getPath("userData"); +const devFilePath = path.join(appDataPath, "developer"); -let isDevMode = true; +let isDevMode; function getIsDevMode() { if (isDevMode !== undefined) { return isDevMode; } - return isDevMode = fs.existsSync(devFilePath); + return (isDevMode = !app.isPackaged || fs.existsSync(devFilePath)); } function setIsDevMode(set) { if (set && !getIsDevMode()) { - fs.writeFileSync(devFilePath, `So you're a developer, huh? Neat! Welcome aboard!`); + fs.writeFileSync( + devFilePath, + `So you're a developer, huh? Neat! Welcome aboard!` + ); } else if (!set && getIsDevMode()) { fs.unlinkSync(devFilePath); } @@ -27,5 +30,5 @@ function setIsDevMode(set) { module.exports = { getIsDevMode, - setIsDevMode + setIsDevMode, }; diff --git a/src/main/index.js b/src/main/index.js index d241ff7..68a978a 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,39 +1,54 @@ const { app, BrowserWindow } = require("electron"); -const path = require("path"); const { registerIpcHandlers } = require("./ipc"); const { createWindow } = require("./windows"); +const { getIsDevMode } = require("./devmode"); +const { shouldQuit } = require("./squirrel"); +const { setupUpdates } = require("./update"); -// Handle creating/removing shortcuts on Windows when installing/uninstalling. -if (require("electron-squirrel-startup")) { - // eslint-disable-line global-require - app.quit(); -} +async function onReady() { + if (!getIsDevMode()) process.env.NODE_ENV = "production"; -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.on("ready", () => { registerIpcHandlers(); createWindow(); -}); + setupUpdates(); +} -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on("window-all-closed", () => { +/** + * All windows have been closed, quit on anything but + * macOS. + */ +function onWindowsAllClosed() { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== "darwin") { app.quit(); } -}); +} -app.on("activate", () => { +function onActivate() { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } -}); +} -// In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and import them here. +function main() { + // Handle creating/removing shortcuts on Windows when + // installing/uninstalling. + if (shouldQuit()) { + app.quit(); + return; + } + + // Set the app's name + app.setName("macintosh.js"); + + // Launch + app.on("ready", onReady); + app.on("activate", onActivate); + app.on("window-all-closed", onWindowsAllClosed); +} + +main(); diff --git a/src/main/ipc.js b/src/main/ipc.js index e6d309f..5da0514 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -1,5 +1,6 @@ -const { ipcMain, app, BrowserWindow } = require("electron"); +const { ipcMain, app, BrowserWindow, dialog } = require("electron"); const { setIsDevMode, getIsDevMode } = require("./devmode"); +const { getMainWindow } = require("./windows"); function registerIpcHandlers() { ipcMain.handle("quit", () => app.quit()); @@ -12,9 +13,23 @@ function registerIpcHandlers() { ipcMain.handle("getIsDevMode", () => getIsDevMode()); - ipcMain.handle("setIsDevMode", (event, set) => { + ipcMain.handle("setIsDevMode", (_event, set) => { setIsDevMode(set); }); + + ipcMain.handle("showMessageBox", (_event, options) => { + const mainWindow = getMainWindow(); + return dialog.showMessageBox(mainWindow, options); + }); + + ipcMain.handle("showMessageBoxSync", (_event, options) => { + const mainWindow = getMainWindow(); + return dialog.showMessageBoxSync(mainWindow, options); + }); + + ipcMain.handle("getAppVersion", () => { + return app.getVersion(); + }); } module.exports = { diff --git a/src/main/squirrel.js b/src/main/squirrel.js new file mode 100644 index 0000000..d403283 --- /dev/null +++ b/src/main/squirrel.js @@ -0,0 +1,7 @@ +function shouldQuit() { + return require("electron-squirrel-startup"); +} + +module.exports = { + shouldQuit, +}; diff --git a/src/main/update.js b/src/main/update.js new file mode 100644 index 0000000..1a11172 --- /dev/null +++ b/src/main/update.js @@ -0,0 +1,14 @@ +const { app } = require("electron"); + +function setupUpdates() { + if (app.isPackaged) { + require("update-electron-app")({ + repo: "felixrieseberg/macintosh.js", + updateInterval: "1 hour", + }); + } +} + +module.exports = { + setupUpdates, +}; diff --git a/src/main/windows.js b/src/main/windows.js index 399e6f6..992e806 100644 --- a/src/main/windows.js +++ b/src/main/windows.js @@ -6,6 +6,10 @@ const { getIsDevMode } = require("./devmode"); const windowList = {}; let mainWindow; +function getMainWindow() { + return mainWindow; +} + function handleNewWindow(event, url, frameName, disposition, options) { // open window as modal event.preventDefault(); @@ -85,4 +89,5 @@ function createWindow() { module.exports = { createWindow, + getMainWindow, }; diff --git a/src/renderer/audio.js b/src/renderer/audio.js index f0876b3..dcc11c9 100644 --- a/src/renderer/audio.js +++ b/src/renderer/audio.js @@ -4,9 +4,7 @@ var audio = { channels: 1, bytesPerSample: 2, samples: 4096, - // freq: 11025, freq: 22050, - // freq: 44100, format: 0x8010, paused: false, timer: null, @@ -41,12 +39,6 @@ var audioDataBuffer = new SharedArrayBuffer(AUDIO_DATA_BUFFER_SIZE); var audioDataBufferView = new Uint8Array(audioDataBuffer); var audioContext = new AudioContext(); -// document.querySelector('#enableAudio').addEventListener('click', function() { -// audioContext.resume().then(() => { -// document.querySelector('#enableAudio').remove(); -// }); -// }); - var gainNode = audioContext.createGain(); gainNode.gain.value = 1; @@ -120,11 +112,6 @@ function openAudio() { audio.gotFirstBlock && Date.now() - getBlockBufferLastWarningTime > 5000 ) { - // console.warn( - // `UI thread tried to read audio data from worker-locked chunk ${getBlockBufferWarningCount} times` - // ); - //console.log("curChunkIndex", curChunkIndex); - // debugger getBlockBufferLastWarningTime = Date.now(); getBlockBufferWarningCount = 0; } @@ -139,10 +126,7 @@ function openAudio() { curChunkAddr + 2 + audio.bufferSize ); audio.nextChunkIndex = audioDataBufferView[curChunkAddr + 1]; - // console.assert(audio.nextChunkIndex != curChunkIndex, `curChunkIndex=${curChunkIndex} == nextChunkIndex=${audio.nextChunkIndex}`) audioDataBufferView[curChunkAddr] = LockStates.EMUL_THREAD_LOCK; - // debugger - // console.log(`got buffer=${curChunkIndex}, next=${audio.nextChunkIndex}`) return blockBuffer; }; @@ -196,7 +180,6 @@ function openAudio() { // And queue it to be played after the currently playing audio stream. audio.pushAudio(blockBuffer, audio.bufferSize); } - // console.log(`queued ${i} buffers of audio`); }; // Create a callback function that will be routinely called to ask more audio data from the user application. diff --git a/src/renderer/controls.js b/src/renderer/controls.js index 44f212a..8913b95 100644 --- a/src/renderer/controls.js +++ b/src/renderer/controls.js @@ -1,8 +1,9 @@ const { quit, devtools } = require("./ipc"); const { getIsWorkerRunning, getIsWorkerSaving } = require("./worker"); const { showCloseWarning } = require("./dialogs"); +const { getIsDevMode } = require("./ipc"); -function registerControls() { +async function registerControls() { document.querySelector("#close").addEventListener("click", () => { if (!getIsWorkerRunning()) { quit(); @@ -16,6 +17,10 @@ function registerControls() { document.querySelector("#devtools").addEventListener("click", () => { devtools(); }); + + if (await getIsDevMode()) { + document.querySelector("#devtools").classList.remove("hidden"); + } } registerControls(); diff --git a/src/renderer/credits.html b/src/renderer/credits.html index 29f4d70..6a3763f 100644 --- a/src/renderer/credits.html +++ b/src/renderer/credits.html @@ -8,6 +8,9 @@

Credits

+ + +

This app by Felix Rieseberg. The real work was done by the people below:

@@ -29,14 +32,6 @@

Basilisk II and its components are released under the GNU GPL. See LICENSE for details.

- + \ No newline at end of file diff --git a/src/renderer/credits.js b/src/renderer/credits.js new file mode 100644 index 0000000..dc8c9e2 --- /dev/null +++ b/src/renderer/credits.js @@ -0,0 +1,19 @@ +const { shell, ipcRenderer } = require("electron"); +const path = require("path"); +const { versions } = require("process"); + +const { getAppVersion } = require("./ipc.js"); + +async function credits() { + license.onclick = () => { + const licensePath = path.join(__dirname, "../basilisk/LICENSE.txt"); + shell.openPath(licensePath); + }; + + const version = await getAppVersion(); + document.querySelector( + "#versions" + ).innerHTML = `macintosh.js v${version} with Electron v${process.versions.electron}`; +} + +credits(); diff --git a/src/renderer/help.html b/src/renderer/help.html index 25af978..46a236b 100644 --- a/src/renderer/help.html +++ b/src/renderer/help.html @@ -9,21 +9,24 @@

Help

-

Passing files into the machine

+

How can I get files into the machine?

- Files will be copied over when you start your VM (virtual machine). They are not synchronized while the VM is running. However, any changes made to the "macintosh.js" folder in your VM will be saved to the corresponding folder in your home directory once you shut the VM down. + Files will be copied over when you start your VM. They are not synchronized while the VM is running. However, any changes made to the "macintosh.js" folder in your VM will be saved to the corresponding folder in your home directory once you shut the VM down. + + Be chill about it: The more files you put into that folder, + the more memory macintosh.js will consume.

-

Getting files out of machine

+

How can I get files out of the machine?