mirror of
https://github.com/mileswolfallen2/OmniEmu2.0.git
synced 2026-09-08 11:23:17 +00:00
still workign to make it this is just a back up
This commit is contained in:
@@ -36,6 +36,9 @@ jobs:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Generate icons
|
||||
run: python3 scripts/generate-icons.py
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -43,7 +46,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Package
|
||||
run: npx electron-builder --${{ matrix.target == 'win-x64' && 'win' || matrix.target == 'mac-x64' && 'mac' || matrix.target == 'mac-arm64' && 'mac' || 'linux' }} --${{ matrix.arch }}
|
||||
run: npx electron-builder --${{ matrix.target == 'win-x64' && 'win' || contains(matrix.target, 'mac') && 'mac' || 'linux' }} --${{ matrix.arch }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -51,7 +54,8 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: omniemu-${{ matrix.target }}
|
||||
path: release/*
|
||||
path: release/*.dmg
|
||||
if-no-files-found: ignore
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Allow electron's postinstall script to download the binary
|
||||
ignore-scripts=false
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Generated
+73
-362
@@ -17,7 +17,7 @@
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"concurrently": "^9.1.0",
|
||||
"electron": "^33.0.0",
|
||||
"electron": "^43.1.0",
|
||||
"electron-builder": "^25.1.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
@@ -323,6 +323,16 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-internal/extract-zip": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.4.tgz",
|
||||
"integrity": "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/asar": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||
@@ -373,25 +383,50 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz",
|
||||
"integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"got": "^11.8.5",
|
||||
"env-paths": "^3.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"progress": "^2.0.3",
|
||||
"semver": "^6.2.0",
|
||||
"semver": "^7.6.3",
|
||||
"sumchecker": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"global-agent": "^3.0.0"
|
||||
"undici": "^7.24.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/node_modules/env-paths": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/notarize": {
|
||||
@@ -1966,13 +2001,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
|
||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
@@ -2025,17 +2060,6 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@@ -2514,15 +2538,6 @@
|
||||
"bluebird": "^3.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
@@ -2601,6 +2616,7 @@
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@@ -3340,44 +3356,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -3405,14 +3383,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/dir-compare": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
|
||||
@@ -3606,22 +3576,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "33.4.11",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz",
|
||||
"integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==",
|
||||
"version": "43.1.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-43.1.0.tgz",
|
||||
"integrity": "sha512-DPfxpQLd4NL3BJ8DBxYAfmLUKKesF5Rx9dQx5FyczAP8bhOPScjHE48GArVeXu68LlAainuwkmQTQvdZwpIIAQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"extract-zip": "^2.0.1"
|
||||
"@electron-internal/extract-zip": "^1.0.1",
|
||||
"@electron/get": "^5.0.0",
|
||||
"@types/node": "^24.9.0"
|
||||
},
|
||||
"bin": {
|
||||
"electron": "cli.js"
|
||||
"electron": "cli.js",
|
||||
"install-electron": "install.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20.55"
|
||||
"node": ">= 22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder": {
|
||||
@@ -3898,14 +3868,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
@@ -3958,20 +3920,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/exponential-backoff": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
||||
@@ -3979,27 +3927,6 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extsprintf": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
|
||||
@@ -4025,16 +3952,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -4148,21 +4065,6 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
@@ -4357,57 +4259,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/globalthis": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-properties": "^1.2.1",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -4464,20 +4315,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -4899,14 +4736,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -4920,16 +4749,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -5161,20 +4980,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -5590,17 +5395,6 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -5769,13 +5563,6 @@
|
||||
"url": "https://github.com/sponsors/jet2jet"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -6105,25 +5892,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
||||
@@ -6243,31 +6011,6 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -6445,14 +6188,6 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ssri": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
|
||||
@@ -6740,20 +6475,6 @@
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -6768,10 +6489,21 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
||||
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6801,16 +6533,6 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
@@ -7093,17 +6815,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
+47
-12
@@ -4,7 +4,7 @@
|
||||
"description": "Cross-platform emulator manager, game launcher and ROM manager",
|
||||
"main": "dist/main/index.js",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"",
|
||||
"dev": "concurrently -k \"npm run dev:main\" \"npm run dev:renderer\"",
|
||||
"dev:main": "tsc -p tsconfig.main.json --watch",
|
||||
"dev:renderer": "vite",
|
||||
"build": "npm run build:renderer && npm run build:main",
|
||||
@@ -12,10 +12,14 @@
|
||||
"build:renderer": "vite build",
|
||||
"start": "electron dist/main/index.js",
|
||||
"preview": "npm run build && npm run start",
|
||||
"package:mac": "npm run build && electron-builder --mac",
|
||||
"prepare": "python3 scripts/generate-icons.py",
|
||||
"postinstall": "node node_modules/electron/install.js",
|
||||
"package:mac": "npm run build && electron-builder --mac --universal",
|
||||
"package:mac:x64": "npm run build && electron-builder --mac --x64",
|
||||
"package:mac:arm64": "npm run build && electron-builder --mac --arm64",
|
||||
"package:win": "npm run build && electron-builder --win",
|
||||
"package:linux": "npm run build && electron-builder --linux",
|
||||
"package:all": "npm run build && electron-builder --mac --win --linux",
|
||||
"package:all": "npm run build && electron-builder --mac --universal --win --linux",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.main.json && tsc --noEmit -p tsconfig.renderer.json"
|
||||
},
|
||||
"author": "",
|
||||
@@ -25,7 +29,7 @@
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"concurrently": "^9.1.0",
|
||||
"electron": "^33.0.0",
|
||||
"electron": "^43.1.0",
|
||||
"electron-builder": "^25.1.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
@@ -38,30 +42,61 @@
|
||||
"appId": "com.omniemu.app",
|
||||
"productName": "OmniEmu",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
"output": "release",
|
||||
"buildResources": "assets"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "assets",
|
||||
"to": "assets",
|
||||
"filter": [
|
||||
"*.png",
|
||||
"*.icns",
|
||||
"*.ico"
|
||||
]
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.utilities",
|
||||
"target": ["dmg", "zip"],
|
||||
"icon": "assets/icon.png",
|
||||
"target": [
|
||||
"dmg",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "${productName}-${version}-mac-${arch}.${ext}",
|
||||
"hardenedRuntime": true
|
||||
"hardenedRuntime": false
|
||||
},
|
||||
"win": {
|
||||
"target": ["nsis", "zip"],
|
||||
"icon": "assets/icon.png",
|
||||
"target": [
|
||||
"nsis",
|
||||
"zip"
|
||||
],
|
||||
"artifactName": "${productName}-${version}-win-${arch}.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
"oneClick": true,
|
||||
"allowToChangeInstallationDirectory": false,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true
|
||||
},
|
||||
"linux": {
|
||||
"target": ["AppImage", "deb"],
|
||||
"category": "Utility",
|
||||
"icon": "assets/icon.png",
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb"
|
||||
],
|
||||
"category": "Game",
|
||||
"artifactName": "${productName}-${version}-linux-${arch}.${ext}"
|
||||
}
|
||||
},
|
||||
"allowScripts": {
|
||||
"[email protected]": true,
|
||||
"[email protected]": true,
|
||||
"[email protected]": true
|
||||
}
|
||||
}
|
||||
|
||||
+22
-11
@@ -3,27 +3,38 @@ set -euo pipefail
|
||||
|
||||
# Build OmniEmu for macOS
|
||||
# Usage: ./scripts/build-mac.sh [arch]
|
||||
# arch: x64 | arm64 (default: current arch)
|
||||
# arch: x64 | arm64 | universal (default: universal)
|
||||
# Requires: Node.js 20+, npm
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
ARCH="${1:-$(uname -m)}"
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH_FLAG="x64" ;;
|
||||
arm64) ARCH_FLAG="arm64" ;;
|
||||
*) echo "Unknown arch: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
ARCH="${1:-universal}"
|
||||
|
||||
echo "==> Installing dependencies..."
|
||||
npm install
|
||||
|
||||
echo "==> Building for macOS ($ARCH_FLAG)..."
|
||||
echo "==> Building..."
|
||||
npm run build
|
||||
|
||||
echo "==> Packaging for macOS ($ARCH_FLAG)..."
|
||||
npx electron-builder --mac --arm64="$([ "$ARCH_FLAG" = "arm64" ] && echo true || echo false)" --x64="$([ "$ARCH_FLAG" = "x64" ] && echo true || echo false)"
|
||||
echo "==> Packaging for macOS ($ARCH)..."
|
||||
case "$ARCH" in
|
||||
universal)
|
||||
npx electron-builder --mac --universal
|
||||
;;
|
||||
x64)
|
||||
npx electron-builder --mac --x64
|
||||
;;
|
||||
arm64)
|
||||
npx electron-builder --mac --arm64
|
||||
;;
|
||||
*)
|
||||
echo "Unknown arch: $ARCH (use: x64, arm64, universal)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "==> Done! Artifacts in ./release/"
|
||||
ls -lh release/*.dmg release/*.zip 2>/dev/null || true
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generates minimal placeholder icons for all platforms.
|
||||
* Creates a simple app icon: purple gradient on transparent.
|
||||
* Outputs: assets/icon.png (1024x1024)
|
||||
* Uses pure Node.js — no dependencies.
|
||||
*/
|
||||
|
||||
const { writeFileSync, mkdirSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const { createCanvas } = (() => {
|
||||
// Minimal PNG encoder — no canvas library needed
|
||||
class Canvas {
|
||||
constructor(w, h) { this.w = w; this.h = h; this.pixels = Buffer.alloc(w * h * 4, 0); }
|
||||
fillRect(x, y, w, h, r, g, b, a) {
|
||||
for (let py = y; py < y + h; py++) {
|
||||
for (let px = x; px < x + w; px++) {
|
||||
const i = (py * this.w + px) * 4;
|
||||
this.pixels[i] = r; this.pixels[i+1] = g; this.pixels[i+2] = b; this.pixels[i+3] = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
fillCircle(cx, cy, rad, r, g, b, a) {
|
||||
for (let py = cy - rad; py <= cy + rad; py++) {
|
||||
for (let px = cx - rad; px <= cx + rad; px++) {
|
||||
if ((px-cx)**2 + (py-cy)**2 <= rad*rad) {
|
||||
const i = (py * this.w + px) * 4;
|
||||
if (i >= 0 && i < this.pixels.length) {
|
||||
this.pixels[i] = r; this.pixels[i+1] = g; this.pixels[i+2] = b; this.pixels[i+3] = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
toPNG() {
|
||||
const { w, h, pixels } = this;
|
||||
const data = [];
|
||||
// PNG signature
|
||||
data.push(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
||||
// IHDR chunk
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 6; // color type RGBA
|
||||
data.push(this._chunk('IHDR', ihdr));
|
||||
// IDAT chunk (raw pixel data with filter bytes)
|
||||
const raw = Buffer.alloc(h * (1 + w * 4));
|
||||
for (let y = 0; y < h; y++) {
|
||||
raw[y * (1 + w * 4)] = 0; // no filter
|
||||
pixels.copy(raw, y * (1 + w * 4) + 1, y * w * 4, (y + 1) * w * 4);
|
||||
}
|
||||
const zlib = require('zlib');
|
||||
const compressed = zlib.deflateSync(raw);
|
||||
data.push(this._chunk('IDAT', compressed));
|
||||
// IEND chunk
|
||||
data.push(this._chunk('IEND', Buffer.alloc(0)));
|
||||
return Buffer.concat(data);
|
||||
}
|
||||
_chunk(type, buf) {
|
||||
const len = Buffer.alloc(4); len.writeUInt32BE(buf.length);
|
||||
const typeB = Buffer.from(type, 'ascii');
|
||||
const crcData = Buffer.concat([typeB, buf]);
|
||||
const crc = require('zlib').crc32(crcData);
|
||||
const crcB = Buffer.alloc(4); crcB.writeUInt32BE(crc >>> 0);
|
||||
return Buffer.concat([len, typeB, buf, crcB]);
|
||||
}
|
||||
}
|
||||
return { createCanvas: (w, h) => new Canvas(w, h) };
|
||||
})();
|
||||
|
||||
const SIZE = 1024;
|
||||
const canvas = createCanvas(SIZE, SIZE);
|
||||
const c = SIZE / 2;
|
||||
const r = SIZE * 0.42;
|
||||
|
||||
// Background circle
|
||||
canvas.fillCircle(c, c, r, 108, 99, 255, 255);
|
||||
// Inner circle (slightly darker)
|
||||
canvas.fillCircle(c, c, r * 0.7, 90, 80, 220, 255);
|
||||
// Letter "O" representation — two circles to make a ring
|
||||
canvas.fillCircle(c, c, r * 0.5, 108, 99, 255, 255);
|
||||
canvas.fillCircle(c, c, r * 0.3, 90, 80, 220, 255);
|
||||
|
||||
const outDir = join(__dirname, '..', 'assets');
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
writeFileSync(join(outDir, 'icon.png'), canvas.toPNG());
|
||||
console.log('Generated assets/icon.png');
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate app icon for all platforms using Pillow."""
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
SIZE = 1024
|
||||
img = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
cx = cy = SIZE // 2
|
||||
r = int(SIZE * 0.42)
|
||||
|
||||
# Outer circle (purple)
|
||||
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(108, 99, 255, 255))
|
||||
|
||||
# Inner circle (slightly darker)
|
||||
ir = int(r * 0.7)
|
||||
draw.ellipse([cx - ir, cy - ir, cx + ir, cy + ir], fill=(90, 80, 220, 255))
|
||||
|
||||
# Ring hole
|
||||
hr = int(r * 0.28)
|
||||
draw.ellipse([cx - hr, cy - hr, cx + hr, cy + hr], fill=(108, 99, 255, 255))
|
||||
|
||||
# Center hole (transparent center of 'O')
|
||||
cr = int(r * 0.17)
|
||||
draw.ellipse([cx - cr, cy - cr, cx + cr, cy + cr], fill=(90, 80, 220, 255))
|
||||
|
||||
os.makedirs('assets', exist_ok=True)
|
||||
img.save('assets/icon.png')
|
||||
print(f'Generated assets/icon.png ({os.path.getsize("assets/icon.png")} bytes)')
|
||||
@@ -288,6 +288,13 @@ function alternativePaths(emulatorId: string): string[] {
|
||||
const userData = app.getPath('userData');
|
||||
const omniEmuDir = join(userData, 'emulators', emulatorId);
|
||||
|
||||
// Check marker file first (written after a successful install)
|
||||
const markerFile = join(omniEmuDir, '.installed');
|
||||
try {
|
||||
const markerPath = require('fs').readFileSync(markerFile, 'utf-8').trim();
|
||||
if (markerPath && existsSync(markerPath)) return [markerPath];
|
||||
} catch { /* no marker */ }
|
||||
|
||||
const common: Record<string, string[]> = {
|
||||
dolphin: [
|
||||
join(omniEmuDir, 'Dolphin.exe'),
|
||||
|
||||
+49
-7
@@ -1,15 +1,40 @@
|
||||
import { app, BrowserWindow, Tray, Menu, nativeImage } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { registerIpcHandlers } from './ipc';
|
||||
import { settings } from './settings';
|
||||
import { isMacOS, isWindows } from './platform';
|
||||
import { isMacOS } from './platform';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let tray: Tray | null = null;
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
|
||||
|
||||
function createAppIcon() {
|
||||
const iconPaths = [
|
||||
join(__dirname, '../../assets/icon.png'),
|
||||
join(__dirname, '../renderer/assets/icon.png'),
|
||||
join(__dirname, '../../build/icon.png'),
|
||||
];
|
||||
for (const p of iconPaths) {
|
||||
try {
|
||||
if (existsSync(p)) return nativeImage.createFromPath(p);
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
// Fallback: 16x16 purple pixel
|
||||
const size = 16;
|
||||
const buf = Buffer.alloc(size * size * 4 + 8);
|
||||
buf.writeUInt32LE(size, 0);
|
||||
buf.writeUInt32LE(size, 4);
|
||||
for (let i = 8; i < buf.length; i += 4) {
|
||||
buf[i] = 108; buf[i + 1] = 99; buf[i + 2] = 255; buf[i + 3] = 255;
|
||||
}
|
||||
return nativeImage.createFromBuffer(buf, { width: size, height: size });
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
const icon = createAppIcon();
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
@@ -18,6 +43,7 @@ function createWindow(): void {
|
||||
title: 'OmniEmu',
|
||||
backgroundColor: '#1a1a2e',
|
||||
show: false,
|
||||
icon,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
@@ -26,9 +52,11 @@ function createWindow(): void {
|
||||
},
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.loadURL('http://localhost:5173');
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(VITE_DEV_SERVER_URL);
|
||||
if (process.env.OMNIEMU_DEVTOOLS) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||
}
|
||||
@@ -51,10 +79,10 @@ function createWindow(): void {
|
||||
}
|
||||
|
||||
function createTray(): void {
|
||||
const iconSize = isMacOS() ? 16 : 24;
|
||||
const icon = nativeImage.createEmpty();
|
||||
const icon = createAppIcon();
|
||||
const trayIcon = icon.resize({ width: 22, height: 22 });
|
||||
|
||||
tray = new Tray(icon);
|
||||
tray = new Tray(trayIcon);
|
||||
tray.setToolTip('OmniEmu');
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
@@ -67,6 +95,20 @@ function createTray(): void {
|
||||
tray.on('click', () => mainWindow?.show());
|
||||
}
|
||||
|
||||
// Prevent multiple instances
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
registerIpcHandlers();
|
||||
createWindow();
|
||||
|
||||
+104
-56
@@ -1,10 +1,9 @@
|
||||
import { createWriteStream, existsSync, mkdirSync, createReadStream } from 'fs';
|
||||
import { join, dirname, basename, extname } from 'path';
|
||||
import { get } from 'https';
|
||||
import { get as getHttp } from 'http';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
||||
import { join, basename, extname, dirname } from 'path';
|
||||
import { get as httpsGet, RequestOptions } from 'https';
|
||||
import { get as httpGet } from 'http';
|
||||
import { execSync } from 'child_process';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { createGunzip, createBrotliDecompress, createInflate } from 'zlib';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { app } from 'electron';
|
||||
import { EmulatorDownload, InstallProgress, Platform, Arch } from '../shared/types';
|
||||
@@ -24,113 +23,158 @@ function tempName(suffix: string): string {
|
||||
|
||||
function downloadFile(url: string, dest: string, onProgress: (pct: number) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? get : getHttp;
|
||||
protocol(url, (response) => {
|
||||
const doRequest = (currentUrl: string) => {
|
||||
const protocol = currentUrl.startsWith('https') ? httpsGet : httpGet;
|
||||
const opts: RequestOptions = {
|
||||
headers: { 'User-Agent': 'OmniEmu/0.1.0' },
|
||||
timeout: 30000,
|
||||
};
|
||||
protocol(currentUrl, opts, (response) => {
|
||||
const code = response.statusCode ?? 500;
|
||||
if (code >= 300 && code < 400 && response.headers.location) {
|
||||
downloadFile(response.headers.location, dest, onProgress).then(resolve).catch(reject);
|
||||
doRequest(response.headers.location);
|
||||
return;
|
||||
}
|
||||
if (code !== 200) {
|
||||
reject(new Error(`HTTP ${code} downloading ${url}`));
|
||||
reject(new Error(`HTTP ${code} — ${currentUrl}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const total = parseInt(response.headers['content-length'] ?? '0', 10);
|
||||
let downloaded = 0;
|
||||
const file = createWriteStream(dest);
|
||||
|
||||
const fileStream = createWriteStream(dest);
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
downloaded += chunk.length;
|
||||
if (total > 0) onProgress(Math.round((downloaded / total) * 100));
|
||||
});
|
||||
|
||||
pipeline(response, file)
|
||||
.then(() => {
|
||||
onProgress(100);
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
}).on('error', reject);
|
||||
pipeline(response, fileStream).then(resolve).catch(reject);
|
||||
}).on('error', reject).on('timeout', function (this: any) { this.destroy(); reject(new Error('Download timed out')); });
|
||||
};
|
||||
doRequest(url);
|
||||
});
|
||||
}
|
||||
|
||||
function execOrThrow(cmd: string): string {
|
||||
try {
|
||||
return execSync(cmd, { stdio: 'pipe', timeout: 120000 }).toString();
|
||||
} catch (e: any) {
|
||||
throw new Error(`Command failed: ${cmd}\n${e.stderr?.toString() || e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function findAppInDir(dir: string): string | undefined {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const full = join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
if (e.name.endsWith('.app')) {
|
||||
const macosBin = join(full, 'Contents', 'MacOS', basename(e.name, '.app'));
|
||||
if (existsSync(macosBin)) return macosBin;
|
||||
}
|
||||
const found = findAppInDir(full);
|
||||
if (found) return found;
|
||||
} else if (e.isFile()) {
|
||||
const isExec = e.name.endsWith('.exe') || e.name.endsWith('.AppImage')
|
||||
|| e.name === 'retroarch' || e.name === 'dolphin-emu'
|
||||
|| e.name === 'rpcs3' || e.name === 'Ryujinx'
|
||||
|| e.name === 'mame' || e.name === 'mame64' || e.name === 'PCSX2';
|
||||
if (isExec) return full;
|
||||
// Check if it's executable
|
||||
try {
|
||||
if (statSync(full).mode & 0o111) return full;
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractArchive(archivePath: string, destDir: string, format: string, emulatorId: string, onProgress: (msg: string) => void): void {
|
||||
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
||||
|
||||
onProgress(`Extracting ${basename(archivePath)}...`);
|
||||
|
||||
switch (format) {
|
||||
case 'zip': {
|
||||
execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: 'pipe' });
|
||||
case 'zip':
|
||||
execOrThrow(`unzip -o "${archivePath}" -d "${destDir}"`);
|
||||
break;
|
||||
}
|
||||
case 'tar.gz':
|
||||
case 'tar.bz2': {
|
||||
const decompress = format === 'tar.bz2' ? '-j' : '-z';
|
||||
execSync(`tar -x${decompress}f "${archivePath}" -C "${destDir}"`, { stdio: 'pipe' });
|
||||
execOrThrow(`tar -xzf "${archivePath}" -C "${destDir}"`);
|
||||
break;
|
||||
case 'tar.bz2':
|
||||
execOrThrow(`tar -xjf "${archivePath}" -C "${destDir}"`);
|
||||
break;
|
||||
}
|
||||
case '7z': {
|
||||
execSync(`7z x "${archivePath}" -o"${destDir}" -y`, { stdio: 'pipe' });
|
||||
try {
|
||||
execOrThrow(`7z x "${archivePath}" -o"${destDir}" -y`);
|
||||
} catch {
|
||||
// Try unzip as fallback (some 7z files are actually zip)
|
||||
execOrThrow(`unzip -o "${archivePath}" -d "${destDir}"`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'dmg': {
|
||||
// Attach DMG and copy .app out
|
||||
const mountPoint = `/tmp/omniemu_${emulatorId}`;
|
||||
execSync(`hdiutil attach "${archivePath}" -mountpoint "${mountPoint}" -nobrowse -quiet`, { stdio: 'pipe' });
|
||||
execSync(`cp -R "${mountPoint}"/*.app "${destDir}/" 2>/dev/null; cp -R "${mountPoint}"/*/*.app "${destDir}/" 2>/dev/null; true`, { stdio: 'pipe' });
|
||||
execSync(`hdiutil detach "${mountPoint}" -quiet 2>/dev/null; true`, { stdio: 'pipe' });
|
||||
if (!isMacOS()) throw new Error('DMG files can only be extracted on macOS');
|
||||
const mountPoint = `/tmp/omniemu_${emulatorId}_${randomBytes(4).toString('hex')}`;
|
||||
try {
|
||||
execOrThrow(`hdiutil attach "${archivePath}" -mountpoint "${mountPoint}" -nobrowse -quiet`);
|
||||
// Copy any .app bundles found in the mounted volume
|
||||
const result = execSync(`ls "${mountPoint}" 2>/dev/null || true`).toString();
|
||||
for (const item of result.split('\n').filter(Boolean)) {
|
||||
const src = join(mountPoint, item);
|
||||
if (item.endsWith('.app') || (statSync(src).isDirectory() && !item.startsWith('.'))) {
|
||||
execOrThrow(`cp -R "${src}" "${destDir}/"`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
execOrThrow(`hdiutil detach "${mountPoint}" -quiet 2>/dev/null; true`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'exe':
|
||||
case 'msi':
|
||||
case 'appimage':
|
||||
case 'pkg': {
|
||||
// These are direct installers, not archives
|
||||
case 'pkg':
|
||||
// These are installers, not archives
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown archive format: ${format}`);
|
||||
}
|
||||
|
||||
onProgress('Extraction complete');
|
||||
}
|
||||
|
||||
function runInstaller(installerPath: string, format: string, emulatorId: string, installDir: string, onProgress: (msg: string) => void): void {
|
||||
onProgress(`Running installer for ${emulatorId}...`);
|
||||
onProgress(`Running installer...`);
|
||||
|
||||
switch (format) {
|
||||
case 'exe': {
|
||||
if (isWindows()) {
|
||||
execSync(`"${installerPath}" /S /D="${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||
execOrThrow(`"${installerPath}" /S /D="${installDir}"`);
|
||||
} else {
|
||||
execSync(`chmod +x "${installerPath}" && "${installerPath}" -- "${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||
execOrThrow(`chmod +x "${installerPath}" && "${installerPath}"`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'msi': {
|
||||
execSync(`msiexec /i "${installerPath}" /quiet /norestart INSTALLDIR="${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||
execOrThrow(`msiexec /i "${installerPath}" /quiet /norestart INSTALLDIR="${installDir}"`);
|
||||
break;
|
||||
}
|
||||
case 'appimage': {
|
||||
execSync(`chmod +x "${installerPath}"`, { stdio: 'pipe' });
|
||||
// Copy AppImage to install dir
|
||||
execSync(`cp "${installerPath}" "${installDir}/${emulatorId}.AppImage"`, { stdio: 'pipe' });
|
||||
execOrThrow(`chmod +x "${installerPath}"`);
|
||||
execOrThrow(`cp "${installerPath}" "${installDir}/${emulatorId}.AppImage"`);
|
||||
break;
|
||||
}
|
||||
case 'pkg': {
|
||||
execSync(`installer -pkg "${installerPath}" -target /`, { stdio: 'pipe', timeout: 300000 });
|
||||
execOrThrow(`installer -pkg "${installerPath}" -target /`);
|
||||
break;
|
||||
}
|
||||
case 'dmg': {
|
||||
case 'dmg':
|
||||
case 'zip':
|
||||
case 'tar.gz':
|
||||
case 'tar.bz2':
|
||||
case '7z':
|
||||
// Already handled in extractArchive
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Cannot install format: ${format}`);
|
||||
}
|
||||
|
||||
onProgress('Installer finished');
|
||||
}
|
||||
|
||||
@@ -143,26 +187,24 @@ export async function installEmulator(
|
||||
arch: Arch,
|
||||
onProgress: ProgressCallback
|
||||
): Promise<string> {
|
||||
// Pick the right download for this platform + arch
|
||||
const candidates = downloads.filter((d) => !d.arch || d.arch === arch);
|
||||
const download = candidates[0];
|
||||
if (!download) throw new Error(`No download available for ${platform}/${arch}`);
|
||||
if (!download) throw new Error(`No download available for ${emulatorId} on ${platform} (${arch})`);
|
||||
|
||||
const report = (stage: InstallProgress['stage'], percent: number, message: string) => {
|
||||
onProgress({ emulatorId, stage, percent, message });
|
||||
};
|
||||
|
||||
report('downloading', 0, `Downloading ${download.url}...`);
|
||||
|
||||
report('downloading', 0, `Downloading ${emulatorId}...`);
|
||||
const downloadPath = tempName(`.${download.format}`);
|
||||
await downloadFile(download.url, downloadPath, (pct) => {
|
||||
report('downloading', pct, `Downloading ${emulatorId}... ${pct}%`);
|
||||
});
|
||||
report('downloading', 100, 'Download complete');
|
||||
|
||||
const installDir = join(app.getPath('userData'), 'emulators', emulatorId);
|
||||
if (!existsSync(installDir)) mkdirSync(installDir, { recursive: true });
|
||||
|
||||
// Determine if this is an archive or an installer
|
||||
const archiveFormats = ['zip', 'tar.gz', 'tar.bz2', '7z', 'dmg'];
|
||||
const installerFormats = ['exe', 'msi', 'pkg', 'appimage'];
|
||||
|
||||
@@ -185,7 +227,13 @@ export async function installEmulator(
|
||||
// Cleanup
|
||||
try { execSync(`rm -f "${downloadPath}"`); } catch { /* ignore */ }
|
||||
|
||||
report('done', 100, `${emulatorId} installed successfully`);
|
||||
report('done', 100, `${emulatorId} installed`);
|
||||
|
||||
return installDir;
|
||||
}
|
||||
|
||||
/** After install, find the executable in the install dir */
|
||||
export function findInstalledBinary(emulatorId: string, installDir: string): string | undefined {
|
||||
if (!existsSync(installDir)) return undefined;
|
||||
return findAppInDir(installDir);
|
||||
}
|
||||
|
||||
+11
-2
@@ -1,4 +1,6 @@
|
||||
import { ipcMain, shell, dialog, BrowserWindow } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
import {
|
||||
getAllEmulatorStates,
|
||||
checkEmulator,
|
||||
@@ -9,7 +11,7 @@ import {
|
||||
getRomsDirectory,
|
||||
getEmulatorsDirectory,
|
||||
} from './emulators';
|
||||
import { installEmulator } from './installer';
|
||||
import { installEmulator, findInstalledBinary } from './installer';
|
||||
import { applyRecommendedConfig, getPresets, checkConfigured } from './configurator';
|
||||
import { settings } from './settings';
|
||||
import { getSystemInfo, platformName, getPlatform, getArch } from './platform';
|
||||
@@ -44,7 +46,14 @@ export function registerIpcHandlers(): void {
|
||||
win?.webContents.send('emulators:install-progress', p);
|
||||
};
|
||||
|
||||
await installEmulator(emulatorId, downloads, platform, arch, sendProgress);
|
||||
const installDir = await installEmulator(emulatorId, downloads, platform, arch, sendProgress);
|
||||
|
||||
// Try to find the binary and create a symlink or record it
|
||||
const binary = findInstalledBinary(emulatorId, installDir);
|
||||
if (binary) {
|
||||
const marker = join(installDir, '.installed');
|
||||
writeFileSync(marker, binary);
|
||||
}
|
||||
|
||||
// Re-check state after install
|
||||
return checkEmulator(emulatorId);
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import type { EmulatorState } from '../../shared/types';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import type { EmulatorState, InstallProgress } from '../../shared/types';
|
||||
|
||||
interface ProgressMap {
|
||||
[emulatorId: string]: InstallProgress;
|
||||
}
|
||||
|
||||
export function EmulatorsPage() {
|
||||
const [states, setStates] = useState<EmulatorState[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [progress, setProgress] = useState<ProgressMap>({});
|
||||
const [actioning, setActioning] = useState<string | null>(null);
|
||||
const cleanups = useRef<(() => void)[]>([]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -12,16 +19,74 @@ export function EmulatorsPage() {
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
useEffect(() => {
|
||||
load();
|
||||
|
||||
const unsub = window.omni.emulators.onInstallProgress((p: InstallProgress) => {
|
||||
setProgress((prev) => ({ ...prev, [p.emulatorId]: p }));
|
||||
if (p.stage === 'done' || p.stage === 'error') {
|
||||
setActioning(null);
|
||||
}
|
||||
});
|
||||
cleanups.current.push(unsub);
|
||||
|
||||
return () => {
|
||||
cleanups.current.forEach((fn) => fn());
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
const handleInstall = async (id: string) => {
|
||||
await window.omni.emulators.openInstallUrl(id);
|
||||
setActioning(id);
|
||||
setProgress((prev) => ({
|
||||
...prev,
|
||||
[id]: { emulatorId: id, stage: 'downloading', percent: 0, message: 'Starting...' },
|
||||
}));
|
||||
await window.omni.emulators.install(id);
|
||||
await load();
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
load();
|
||||
const handleInstallAndConfigure = async (id: string) => {
|
||||
setActioning(id);
|
||||
setProgress((prev) => ({
|
||||
...prev,
|
||||
[id]: { emulatorId: id, stage: 'downloading', percent: 0, message: 'Starting...' },
|
||||
}));
|
||||
const result = await window.omni.emulators.install(id);
|
||||
if (result.installed && result.path) {
|
||||
await window.omni.emulators.configure(id, result.path);
|
||||
}
|
||||
await load();
|
||||
};
|
||||
|
||||
const handleConfigure = async (state: EmulatorState) => {
|
||||
if (!state.path) return;
|
||||
setActioning(state.config.id);
|
||||
setProgress((prev) => ({
|
||||
...prev,
|
||||
[state.config.id]: {
|
||||
emulatorId: state.config.id,
|
||||
stage: 'configuring', percent: 0, message: 'Applying recommended settings...',
|
||||
},
|
||||
}));
|
||||
await window.omni.emulators.configure(state.config.id, state.path);
|
||||
setProgress((prev) => ({
|
||||
...prev,
|
||||
[state.config.id]: {
|
||||
emulatorId: state.config.id,
|
||||
stage: 'done', percent: 100,
|
||||
message: 'Configuration applied',
|
||||
},
|
||||
}));
|
||||
setActioning(null);
|
||||
await load();
|
||||
};
|
||||
|
||||
const handleOpenWebsite = async (id: string) => {
|
||||
await window.omni.emulators.openWebsite(id);
|
||||
};
|
||||
|
||||
const handleRefresh = () => load();
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Checking emulators...</div>;
|
||||
}
|
||||
@@ -31,21 +96,34 @@ export function EmulatorsPage() {
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon">🕹️</div>
|
||||
<h3>No emulators configured</h3>
|
||||
<p>Add emulator definitions to get started.</p>
|
||||
<p>Ready to install and configure emulators automatically.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const currentProgress = (id: string) => progress[id];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
|
||||
<div className="info-bar">
|
||||
<span>
|
||||
{states.filter((s) => s.installed).length} installed
|
||||
{' · '}
|
||||
{states.filter((s) => s.configured).length} configured
|
||||
{' · '}
|
||||
{Object.keys(progress).length > 0 && ' Installing...'}
|
||||
</span>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleRefresh}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="card-grid">
|
||||
{states.map((state) => (
|
||||
{states.map((state) => {
|
||||
const prog = currentProgress(state.config.id);
|
||||
const isActioning = actioning === state.config.id;
|
||||
|
||||
return (
|
||||
<div className="card" key={state.config.id}>
|
||||
<div className="card-header">
|
||||
<h3>{state.config.name}</h3>
|
||||
@@ -54,14 +132,18 @@ export function EmulatorsPage() {
|
||||
!state.config.supported
|
||||
? 'badge-unsupported'
|
||||
: state.installed
|
||||
? state.configured
|
||||
? 'badge-installed'
|
||||
: 'badge-installed'
|
||||
: 'badge-missing'
|
||||
}`}
|
||||
>
|
||||
{!state.config.supported
|
||||
? 'Unsupported'
|
||||
: state.installed
|
||||
? 'Installed'
|
||||
? state.configured
|
||||
? 'Configured'
|
||||
: 'Installed'
|
||||
: 'Not installed'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -77,28 +159,101 @@ export function EmulatorsPage() {
|
||||
</div>
|
||||
|
||||
{state.version && (
|
||||
<p className="text-sm text-muted mt-2">
|
||||
Version: {state.version}
|
||||
</p>
|
||||
<p className="text-sm text-muted mt-2">Version: {state.version}</p>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
|
||||
{!state.installed && state.config.installUrl && (
|
||||
{prog && (
|
||||
<div className="mt-2">
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{
|
||||
width: `${prog.percent}%`,
|
||||
background:
|
||||
prog.stage === 'error'
|
||||
? 'var(--error)'
|
||||
: 'var(--accent)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
{prog.stage === 'error'
|
||||
? `Error: ${prog.error || prog.message}`
|
||||
: prog.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 6,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{!state.installed && state.config.supported && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstallAndConfigure(state.config.id)}
|
||||
>
|
||||
{isActioning ? 'Working...' : 'Install & Configure'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstall(state.config.id)}
|
||||
>
|
||||
Install
|
||||
Install Only
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!state.installed && !state.config.supported && (
|
||||
<span className="text-sm text-muted">Not supported on this platform</span>
|
||||
)}
|
||||
|
||||
{state.installed && !state.configured && (
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleConfigure(state)}
|
||||
>
|
||||
Apply Recommended Settings
|
||||
</button>
|
||||
)}
|
||||
{state.installed && state.path && (
|
||||
<span className="text-sm text-muted" style={{ padding: '4px 0' }}>
|
||||
{state.path}
|
||||
|
||||
{state.installed && state.configured && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleConfigure(state)}
|
||||
>
|
||||
Re-apply Settings
|
||||
</button>
|
||||
<span className="badge badge-installed" style={{ fontSize: 11 }}>
|
||||
Configured
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.config.websiteUrl && (
|
||||
<button
|
||||
className="btn-icon"
|
||||
title="Open website"
|
||||
onClick={() => handleOpenWebsite(state.config.id)}
|
||||
>
|
||||
↗
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,79 +2,97 @@ import React, { useEffect, useState, useCallback } from 'react';
|
||||
import type { GameEntry } from '../../shared/types';
|
||||
|
||||
const platformIcons: Record<string, string> = {
|
||||
nes: '🕹️',
|
||||
snes: '🕹️',
|
||||
n64: '🎮',
|
||||
gba: '🎮',
|
||||
gbc: '🎮',
|
||||
ps1: '💿',
|
||||
ps2: '💿',
|
||||
ps3: '💿',
|
||||
wii: '📀',
|
||||
gc: '📀',
|
||||
switch: '🕹️',
|
||||
arcade: '🕹️',
|
||||
nes: '🕹️', snes: '🕹️', n64: '🎮',
|
||||
gba: '🎮', gbc: '🎮',
|
||||
ps1: '💿', ps2: '💿', ps3: '💿',
|
||||
wii: '📀', gc: '📀',
|
||||
switch: '🕹️', arcade: '🕹️',
|
||||
};
|
||||
|
||||
export function LibraryPage() {
|
||||
const [games, setGames] = useState<GameEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedDir, setSelectedDir] = useState<string | null>(null);
|
||||
const [romsDir, setRomsDir] = useState<string>('');
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
const dir = await window.omni.roms.selectDirectory();
|
||||
if (!dir) return;
|
||||
setSelectedDir(dir);
|
||||
// Load saved ROM directory on mount
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const settings = await window.omni.settings.get();
|
||||
if (settings.romsDirectory) {
|
||||
setRomsDir(settings.romsDirectory);
|
||||
setLoading(true);
|
||||
const results = await window.omni.roms.scan(settings.romsDirectory);
|
||||
setGames(results);
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const scanDirectory = useCallback(async (dir: string) => {
|
||||
setRomsDir(dir);
|
||||
setLoading(true);
|
||||
// Save to persistent settings
|
||||
await window.omni.settings.save({ romsDirectory: dir });
|
||||
const results = await window.omni.roms.scan(dir);
|
||||
setGames(results);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
const dir = await window.omni.roms.selectDirectory();
|
||||
if (!dir) return;
|
||||
await scanDirectory(dir);
|
||||
}, [scanDirectory]);
|
||||
|
||||
const handleLaunch = async (game: GameEntry) => {
|
||||
await window.omni.game.launch(game.emulatorId, game.romPath);
|
||||
};
|
||||
|
||||
const handleOpenDirectory = async () => {
|
||||
const dir = await window.omni.roms.selectDirectory();
|
||||
if (!dir) return;
|
||||
setSelectedDir(dir);
|
||||
setLoading(true);
|
||||
const results = await window.omni.roms.scan(dir);
|
||||
setGames(results);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16, alignItems: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={handleScan}>
|
||||
Scan for ROMs
|
||||
Select ROM Folder
|
||||
</button>
|
||||
{selectedDir && (
|
||||
<span className="text-sm text-muted" style={{ padding: '8px 0' }}>
|
||||
{selectedDir}
|
||||
{romsDir && (
|
||||
<span className="text-sm text-muted" style={{ padding: '8px 0', maxWidth: 400, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{romsDir}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <div className="loading">Scanning for games...</div>}
|
||||
|
||||
{!loading && games.length === 0 && (
|
||||
<div className="scan-area" onClick={handleOpenDirectory}>
|
||||
{!loading && games.length === 0 && !romsDir && (
|
||||
<div className="scan-area" onClick={handleScan}>
|
||||
<div style={{ fontSize: 48 }}>📂</div>
|
||||
<p>Click to select a ROM directory</p>
|
||||
<p>Select a folder to scan for ROMs</p>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
Supported formats: .nes, .sfc, .n64, .gba, .iso, .wbfs, .nsp, .ps2, and more
|
||||
Your ROM directory will be saved for next time
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && games.length === 0 && romsDir && (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon">🔍</div>
|
||||
<p>No ROMs found in that folder</p>
|
||||
<p className="text-sm text-muted mt-2">
|
||||
Supported: .nes, .sfc, .n64, .gba, .iso, .wbfs, .nsp, .ps2, .chd, and more
|
||||
</p>
|
||||
<button className="btn btn-secondary mt-4" onClick={handleScan}>
|
||||
Choose another folder
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{games.length > 0 && (
|
||||
<div>
|
||||
<div className="info-bar">
|
||||
<span>{games.length} games found</span>
|
||||
<span>{(games.length / 10).toFixed(1)} GB estimated</span>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleScan}>
|
||||
Rescan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="library-grid">
|
||||
@@ -91,9 +109,7 @@ export function LibraryPage() {
|
||||
<div className="game-card-info">
|
||||
<div className="game-card-title">{game.title}</div>
|
||||
<div className="game-card-platform">
|
||||
{game.platform.toUpperCase()}
|
||||
{' · '}
|
||||
{game.emulatorId}
|
||||
{game.platform.toUpperCase()} · {game.emulatorId}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,6 +84,25 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Presets</h3>
|
||||
|
||||
<div className="setting-row">
|
||||
<div>
|
||||
<div className="setting-label">Preset Source URL</div>
|
||||
<div className="setting-desc">
|
||||
URL to fetch recommended emulator config presets from
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.presetSourceUrl}
|
||||
onChange={(e) => update({ presetSourceUrl: e.target.value })}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Behavior</h3>
|
||||
|
||||
|
||||
@@ -447,6 +447,21 @@ select:focus, input[type="text"]:focus {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 300ms ease, background 300ms ease;
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
.mt-2 { margin-top: 8px; }
|
||||
.mt-4 { margin-top: 16px; }
|
||||
|
||||
+42
-2
@@ -1,8 +1,48 @@
|
||||
import type { OmniApi } from '../main/preload';
|
||||
import type {
|
||||
EmulatorConfig,
|
||||
EmulatorState,
|
||||
GameEntry,
|
||||
SystemInfo,
|
||||
AppSettings,
|
||||
InstallProgress,
|
||||
ConfigPreset,
|
||||
} from '../shared/types';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
omni: OmniApi;
|
||||
omni: {
|
||||
system: {
|
||||
info: () => Promise<SystemInfo>;
|
||||
platformName: () => Promise<string>;
|
||||
};
|
||||
emulators: {
|
||||
list: () => Promise<EmulatorConfig[]>;
|
||||
states: () => Promise<EmulatorState[]>;
|
||||
check: (id: string) => Promise<EmulatorState>;
|
||||
install: (id: string) => Promise<EmulatorState>;
|
||||
configure: (id: string, installPath: string) => Promise<{ success: boolean; state: EmulatorState }>;
|
||||
presets: (id: string) => Promise<ConfigPreset[]>;
|
||||
configured: (id: string, installPath?: string) => Promise<boolean>;
|
||||
openWebsite: (id: string) => Promise<boolean>;
|
||||
onInstallProgress: (cb: (p: InstallProgress) => void) => () => void;
|
||||
};
|
||||
roms: {
|
||||
scan: (directory?: string) => Promise<GameEntry[]>;
|
||||
selectDirectory: () => Promise<string | null>;
|
||||
};
|
||||
game: {
|
||||
launch: (emulatorId: string, romPath: string) => Promise<boolean>;
|
||||
};
|
||||
settings: {
|
||||
get: () => Promise<AppSettings>;
|
||||
save: (s: Partial<AppSettings>) => Promise<AppSettings>;
|
||||
reset: () => Promise<AppSettings>;
|
||||
};
|
||||
paths: {
|
||||
romsDirectory: () => Promise<string>;
|
||||
emulatorsDirectory: () => Promise<string>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/renderer",
|
||||
"rootDir": "src/renderer",
|
||||
"types": ["vite/client"],
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/renderer/**/*", "src/shared/**/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user