feat: release v0.2.0 with beta features toggle and new frontend support

- Added support for experimental emulators (ES-DE, NeoStation, Pegasus) behind a beta toggle in settings.
- Implemented manual installation prompts for ES-DE and NeoStation on macOS.
- Updated user agent to v0.1.3 across various modules.
- Enhanced the Emulators page with conditional install buttons for beta emulators.
- Introduced a new settings section for enabling/disabling beta features.
- Updated release notes for v0.2.0 detailing new features and improvements.
- Added project specification document for OmniEmu 2.0.
This commit is contained in:
2026-07-13 01:44:51 -05:00
parent 630deea100
commit 89446e4f96
19 changed files with 1609 additions and 38 deletions
+180
View File
@@ -0,0 +1,180 @@
# **OmniEmu v0.2.0 — Fourth Pre-Release**
Cross-platform emulator manager, game launcher, and ROM manager.
---
## **What's New**
### **Frontend Support**
OmniEmu now supports multiple emulation frontends — install and configure them alongside your emulators.
**ES-DE (EmulationStation Desktop Edition)** — available in Beta
- Multi-system frontend with a polished UI for browsing and launching games
- Auto-configured with your ROM directory and installed emulators
- Supports all 20+ systems
- macOS: requires manual install (OmniEmu guides you through it)
**NeoStation** — available in Beta
- Modern Flutter-based frontend with ScreenScraper integration
- Auto-discovers emulators and ROMs
- RetroAchievements built in
- macOS: requires manual install (OmniEmu guides you through it)
**EmuBuddy**
- Cross-platform frontend with automated setup and game downloading
- One-click controller configuration
- Supports 14 systems
- Available on Windows, macOS, and Linux
**Pegasus Frontend** — available in Beta
- Lightweight, themeable game launcher
- OmniEmu auto-generates collection files (`metadata.pegasus.txt`) in each ROM subdirectory
- Writes Pegasus settings to point at your ROM library
---
### **Beta Features Toggle**
A new **Settings > Experimental** section lets you opt into beta features.
Enable **Beta Features** to unlock ES-DE, NeoStation, and Pegasus in the Emulators page. EmuBuddy is available without the toggle.
---
### **Pegasus Collection File Generation**
When Pegasus is configured (or on app startup with beta features enabled), OmniEmu now writes `metadata.pegasus.txt` files directly into each ROM subdirectory (e.g. `roms/nes/metadata.pegasus.txt`). This is where Pegasus discovers collections — not in a central config folder.
Each collection file includes:
- Collection name and shortname
- File extensions for that system
- Launch command pointing to the correct emulator
- Individual game entries with filenames
---
### **ROM Library Auto-Scan**
The Library page now scans for games automatically on startup — no need to manually select a folder first. If your ROMs directory is set (defaults to `~/Documents/roms`), games appear immediately.
---
## **Bug Fixes & Improvements**
### **macOS Frontend Installation**
- ES-DE and NeoStation require manual DMG installation on macOS due to license dialogs
- OmniEmu now shows a guided install flow: detects the DMG, opens it for you, then detects the installed app
- **"Install Manually"** button in the Emulators page lets you skip the automated flow and install at your own pace
### **Emulator Detection**
- Added alternative install paths for all emulators (Homebrew, snap, Flatpak, common directories)
- Improved binary discovery with recursive directory scanning
### **ROM Scanning**
- ROMs are automatically organized into the correct subdirectory on scan
- Platform guessing improved: `.iso` files in a `ps2/` folder are classified as PS2, not PS1
- Supported extensions now include `.cdi` (Dreamcast), `.pbp` (PSP), `.chd` (multiple systems)
---
## **Supported Emulators**
Emulator | Systems | Platforms
--- | --- | ---
Dolphin | GameCube, Wii | Win, macOS, Linux
RPCS3 | PlayStation 3 | Win, Linux
Eden | Nintendo Switch | Win, macOS, Linux
PCSX2 | PlayStation 2 | Win, macOS, Linux
MAME | Arcade | Win, Linux
DuckStation | PlayStation 1 | Win, macOS, Linux
RetroArch | NES, SNES, N64, GB, GBC, GBA, PS1, PCE, Sega MD, Sega Saturn, Dreamcast | Win, macOS, Linux
PPSSPP | PlayStation Portable | Win, macOS, Linux
melonDS | Nintendo DS | Win, macOS, Linux
Flycast | Dreamcast, Naomi, Naomi 2, Atomiswave | Win, macOS, Linux
### **Frontends (Beta)**
Frontend | Type | Platforms
--- | --- | ---
ES-DE | Multi-system frontend | Win, macOS, Linux
NeoStation | Flutter-based frontend | Win, macOS, Linux
Pegasus | Themeable game launcher | Win, macOS, Linux
EmuBuddy | Automated emulation frontend | Win, macOS, Linux
---
## **Auto-Update**
If you're downloading OmniEmu from GitHub, you can safely ignore the `.yml` files included with each release.
These are used internally by OmniEmu's built-in updater and are **not required** for manual installation.
Simply download the installer for your platform.
---
# **Installation**
## **macOS**
Download:
- `OmniEmu-0.2.0-mac-universal.dmg`
Install:
1. Drag **OmniEmu** into **Applications**.
2. Right-click **OmniEmu** > **Open** on first launch.
If macOS blocks the app, run:
```bash
sudo xattr -rd com.apple.quarantine /Applications/OmniEmu.app
```
## **Windows**
Download:
- `OmniEmu-0.2.0-win-x64.exe` *(Intel / AMD)*
- `OmniEmu-0.2.0-win-arm64.exe` *(ARM64)*
Portable ZIP builds are also available.
If Windows SmartScreen appears:
**More info > Run anyway**
## **Linux**
Download:
- `OmniEmu-0.2.0-linux-x86_64.AppImage`
- `OmniEmu-0.2.0-linux-arm64.AppImage`
Make it executable:
```bash
chmod +x OmniEmu*.AppImage
./OmniEmu*.AppImage
```
If the AppImage doesn't launch, install **libfuse2**.
---
## **Thanks for Testing!**
OmniEmu is still in **pre-release**, so bug reports, feature requests, and feedback are greatly appreciated.
Thank you for helping improve OmniEmu, and enjoy the update!
+566
View File
@@ -0,0 +1,566 @@
# OmniEmu 2.0 — Project Specification
> All your games. One place.
OmniEmu is a cross-platform desktop application for managing emulators, launching games, and organizing ROM libraries. Built with Electron, React, and TypeScript.
**Repository:** [github.com/mileswolfallen2/OmniEmu2.0](https://github.com/mileswolfallen2/OmniEmu2.0)
**Version:** 0.2.0
**License:** MIT (Copyright 2026 mileswa1q22)
---
## Table of Contents
1. [Tech Stack](#tech-stack)
2. [Licenses](#licenses)
3. [Architecture](#architecture)
4. [Supported Emulators](#supported-emulators)
5. [Supported Frontends](#supported-frontends)
6. [Supported Systems](#supported-systems)
7. [ROM Format Support](#rom-format-support)
8. [Configuration Presets](#configuration-presets)
9. [IPC API Reference](#ipc-api-reference)
10. [File Structure](#file-structure)
11. [Build & Development](#build--development)
12. [Release Artifacts](#release-artifacts)
13. [Security](#security)
14. [Contributing](#contributing)
---
## Tech Stack
| Technology | Version | Purpose | License |
|---|---|---|---|
| Electron | 43.1.0 | Desktop app shell | MIT |
| React | 19.2.7 | UI framework | MIT |
| React DOM | 19.2.7 | DOM rendering | MIT |
| TypeScript | 5.9.3 | Type-safe language | Apache-2.0 |
| Vite | 6.4.3 | Renderer bundler | MIT |
| electron-builder | 25.1.8 | Packaging & distribution | MIT |
| electron-updater | 6.8.9 | Auto-update support | MIT |
| @vitejs/plugin-react | 4.7.0 | React Fast Refresh for Vite | MIT |
| concurrently | 9.2.3 | Parallel dev scripts | MIT |
| 7zip-bin | 5.2.0 | Archive extraction (7z) | MIT |
All dependencies are MIT-licensed except TypeScript (Apache-2.0).
---
## Licenses
### OmniEmu
```
MIT License
Copyright (c) 2026 mileswa1q22
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
### Third-Party Licenses
| Component | License | SPDX |
|---|---|---|
| Electron | MIT | MIT |
| React | MIT | MIT |
| React DOM | MIT | MIT |
| TypeScript | Apache-2.0 | Apache-2.0 |
| Vite | MIT | MIT |
| electron-builder | MIT | MIT |
| electron-updater | MIT | MIT |
| @vitejs/plugin-react | MIT | MIT |
| concurrently | MIT | MIT |
| 7zip-bin | MIT | MIT |
### External Software (Not Bundled)
OmniEmu can install and configure the following third-party software. Each has its own license:
| Software | License | Website |
|---|---|---|
| RetroArch | GPLv3 | https://retroarch.com |
| Dolphin | GPLv2+ | https://dolphin-emu.org |
| RPCS3 | GPLv2 | https://rpcs3.net |
| Eden | GPLv3 | https://eden-emu.dev |
| PCSX2 | GPLv2+ | https://pcsx2.net |
| MAME | GPLv2+ | https://mamedev.org |
| DuckStation | GPLv3 | https://github.com/stenzek/duckstation |
| PPSSPP | GPLv2+ | https://ppsspp.org |
| melonDS | GPLv2 | https://melonds.kuribo64.net |
| Flycast | GPLv2 | https://github.com/flyinghead/flycast |
| ES-DE | GPLv3 | https://es-de.org |
| NeoStation | MIT | https://neostation.dev |
| Pegasus Frontend | MIT | https://pegasus-frontend.org |
| EmuBuddy | Unknown | https://github.com/computerex/EmuBuddy |
---
## Architecture
OmniEmu uses a standard Electron two-process architecture:
```
+------------------+ IPC (ipcMain/ipcRenderer) +------------------+
| | <---------------------------------> | |
| Main Process | | Renderer Process |
| (Node.js) | | (Chromium) |
| | | |
| - Emulator mgmt | | - React UI |
| - ROM scanning | | - Pages |
| - Installation | | - Components |
| - Config writing | | - Gamepad nav |
| - Auto-updater | | - Modals |
| - Settings | | |
| - RetroAchieve. | | |
| - BIOS checks | | |
+------------------+ +------------------+
| |
v v
File system window.omni API
(settings, configs, (contextBridge)
ROMs, cache, emulators)
```
### Main Process (`src/main/`)
| File | Purpose |
|---|---|
| `index.ts` | App entry point. Creates window (1200x800, min 900x600), system tray, single-instance lock, startup tasks |
| `emulators.ts` | Core emulator registry. 14 emulators with download URLs, detection, version checking, ROM scanning, game launching |
| `configurator.ts` | Configuration engine. Presets, config file writing, ES-DE XML generation, NeoStation JSON generation, Pegasus collection generation, controller bindings, RetroAchievements setup |
| `ipc.ts` | IPC handler registry. 35+ channels exposed to the renderer |
| `preload.ts` | Context bridge. Exposes `window.omni` API with typed wrappers |
| `platform.ts` | Platform detection (`win32`/`darwin`/`linux`, `x64`/`arm64`) |
| `settings.ts` | Settings persistence (`settings.json` in userData) |
| `installer.ts` | Download, extract, and install emulators (zip, 7z, tar.gz, DMG, NSIS, AppImage, Homebrew, snap) |
| `scraper.ts` | Cover art (libretro-thumbnails) and metadata (MobyGames) scraping with local cache |
| `updater.ts` | Auto-update via electron-updater |
| `bios.ts` | BIOS file detection and management |
| `ra.ts` | RetroAchievements integration (game lookup, achievement data) |
### Renderer Process (`src/renderer/`)
| File | Purpose |
|---|---|
| `App.tsx` | Root component. Page routing, theme application, gamepad nav |
| `main.tsx` | React entry point |
| `index.html` | HTML shell with CSP |
| `styles.css` | Complete CSS design system (dark/light themes) |
| `types.ts` | `window.omni` type declarations |
### Pages
| Page | Purpose |
|---|---|
| `Dashboard.tsx` | System info, emulator overview, recent games, quick actions |
| `EmulatorsPage.tsx` | Emulator cards with install/configure/launch/uninstall actions |
| `LibraryPage.tsx` | ROM library grid with cover art, auto-scan |
| `ControllerPage.tsx` | Gamepad detection, button states, controller config |
| `SettingsPage.tsx` | Directories, BIOS, emulator assignments, appearance, beta features, updates, about |
| `UtilitiesPage.tsx` | ROM folder structure, RetroAchievements credentials |
### Components
| Component | Purpose |
|---|---|
| `Sidebar.tsx` | Navigation sidebar (6 items + version) |
| `BiosCheckPanel.tsx` | BIOS file scanner |
| `GameDetailModal.tsx` | Game detail view with metadata, achievements, screenshots |
| `ReleaseNotesModal.tsx` | Markdown release notes display |
### Hooks
| Hook | Purpose |
|---|---|
| `useGamepadNav.ts` | 60fps gamepad polling, D-Pad/button navigation, controller detection |
---
## Supported Emulators
### Core Emulators
| Emulator | ID | Description | Systems | Download Formats |
|---|---|---|---|---|
| RetroArch | `retroarch` | Multi-system emulator frontend | NES, SNES, N64, GB, GBC, GBA, PS1, PCE, Sega MD, Sega Saturn, Dreamcast | 7z |
| Dolphin | `dolphin` | GameCube & Wii emulator | GC, Wii | 7z, DMG, AppImage |
| RPCS3 | `rpcs3` | PlayStation 3 emulator | PS3 | 7z, AppImage (no macOS) |
| Eden | `eden` | Nintendo Switch emulator | Switch | ZIP, DMG, AppImage |
| PCSX2 | `pcsx2` | PlayStation 2 emulator | PS2 | 7z, tar.xz, AppImage |
| DuckStation | `duckstation` | PlayStation 1 emulator | PS1 | ZIP, AppImage |
| MAME | `mame` | Multi Arcade Machine Emulator | Arcade | EXE, ZIP (no macOS download) |
| PPSSPP | `ppsspp` | PlayStation Portable emulator | PSP | 7z, ZIP, AppImage |
| melonDS | `melonds` | Nintendo DS emulator | NDS | ZIP, AppImage |
| Flycast | `flycast` | Dreamcast/Naomi/Atomiswave emulator | Dreamcast | ZIP, AppImage |
### Frontends
| Frontend | ID | Description | Gated |
|---|---|---|---|
| ES-DE | `esde` | EmulationStation Desktop Edition | Beta |
| NeoStation | `neostation` | Flutter-based frontend with ScreenScraper | Beta |
| Pegasus | `pegasus` | Themeable game launcher | Beta |
| EmuBuddy | `emubuddy` | Automated emulation frontend | No |
### Detection
OmniEmu detects installed emulators via:
1. Default hardcoded paths per platform
2. Marker file (`userData/emulators/<id>/.installed`)
3. Alternative common paths (Homebrew, snap, Flatpak, etc.)
4. System `which`/`where` lookup
---
## Supported Systems
| System ID | Name | Primary Emulator | Alternatives |
|---|---|---|---|
| `nes` | Nintendo Entertainment System | RetroArch (Nestopia) | RetroArch (Mesen, FCEUX) |
| `snes` | Super Nintendo | RetroArch (Snes9x) | RetroArch (bsnes HD) |
| `n64` | Nintendo 64 | RetroArch (Mupen64Plus-Next) | RetroArch (Parallel) |
| `gb` | Game Boy | RetroArch (Gambatte) | RetroArch (SameBoy) |
| `gbc` | Game Boy Color | RetroArch (Gambatte) | RetroArch (SameBoy) |
| `gba` | Game Boy Advance | RetroArch (mGBA) | RetroArch (VBA-M) |
| `nds` | Nintendo DS | melonDS | RetroArch (melonDS) |
| `ps1` | PlayStation 1 | DuckStation | RetroArch (SwanStation) |
| `ps2` | PlayStation 2 | PCSX2 | — |
| `ps3` | PlayStation 3 | RPCS3 | — |
| `psp` | PlayStation Portable | PPSSPP | — |
| `gc` | Nintendo GameCube | Dolphin | — |
| `wii` | Nintendo Wii | Dolphin | — |
| `switch` | Nintendo Switch | Eden | — |
| `arcade` | Arcade | MAME | RetroArch (MAME) |
| `dreamcast` | Sega Dreamcast | Flycast | RetroArch (Flycast) |
| `sega-md` | Sega Mega Drive / Genesis | RetroArch (Genesis Plus GX) | — |
| `sega-saturn` | Sega Saturn | RetroArch (Yaba Sanshiro) | — |
| `pce` | PC Engine / TurboGrafx-16 | RetroArch (Mednafen PCE) | — |
---
## ROM Format Support
| Extension | System(s) |
|---|---|
| `.nes` | NES |
| `.sfc`, `.smc` | SNES |
| `.n64`, `.z64`, `.v64` | N64 |
| `.gb` | Game Boy |
| `.gbc` | Game Boy Color |
| `.gba` | Game Boy Advance |
| `.nds` | Nintendo DS |
| `.iso`, `.bin`, `.cue` | PS1, PS2, PS3, PSP, GC, Wii, Saturn |
| `.chd` | PS1, PS2, PSP, Dreamcast, Saturn |
| `.pbp` | PSP |
| `.cso` | PSP |
| `.gcm`, `.gcz`, `.rvz` | GameCube |
| `.wbfs`, `.wad` | Wii |
| `.nsp`, `.xci` | Switch |
| `.pkg` | PS3 |
| `.m3u` | Multi-disc games |
| `.zip`, `.7z` | Arcade (MAME), various |
| `.gdi`, `.cdi` | Dreamcast |
| `.rom` | Various |
Platform is guessed from file extension, with directory name as override (e.g. `.iso` in `ps2/` is classified as PS2).
---
## Configuration Presets
### Built-In Presets
| Emulator | Preset Name | Config Files |
|---|---|---|
| RetroArch | OmniEmu Recommended | `retroarch.cfg` |
| RetroArch | OmniEmu Performance | `retroarch.cfg` |
| Dolphin | OmniEmu Recommended | `Config/Dolphin.ini`, `Config/GFX.ini` |
| RPCS3 | OmniEmu Recommended | `config.yml` |
| Eden | OmniEmu Recommended | `Config.json` |
| PCSX2 | OmniEmu Recommended | `inis/PCSX2.ini` |
| DuckStation | OmniEmu Recommended | `settings.ini` |
| MAME | OmniEmu Recommended | `mame.ini` |
| melonDS | OmniEmu Recommended | `melonDS.ini` |
| Flycast | OmniEmu Recommended | `emu.cfg` |
| ES-DE | OmniEmu Recommended | Generates `es_settings.xml`, `es_find_rules.xml` |
| NeoStation | OmniEmu Recommended | Generates `omniemu_config.json` |
| Pegasus | OmniEmu Recommended | Generates `metadata.pegasus.txt` per ROM dir |
### Preset Loading Priority
1. Remote URL (`https://raw.githubusercontent.com/mileswolfallen2/OmniEmu2.0/main/presets.json`)
2. Local file (`userData/presets.json`)
3. Built-in presets (compiled into the app)
### Remote Preset Source
Configurable in **Settings > Presets**. Point to any JSON file following the `presets.json` schema.
---
## IPC API Reference
### System
| Channel | Arguments | Returns |
|---|---|---|
| `system:info` | — | `SystemInfo` |
| `system:platform-name` | — | `string` |
### Emulators
| Channel | Arguments | Returns |
|---|---|---|
| `emulators:list` | — | `EmulatorConfig[]` |
| `emulators:states` | — | `EmulatorState[]` |
| `emulators:check` | `id: string` | `EmulatorState` |
| `emulators:install` | `id: string` | `EmulatorState` |
| `emulators:configure` | `id, installPath` | `{ success, state }` |
| `emulators:presets` | `id: string` | `ConfigPreset[]` |
| `emulators:configured` | `id, installPath?` | `boolean` |
| `emulators:launch` | `id: string` | `boolean` |
| `emulators:uninstall` | `id: string` | `{ removed, state }` |
| `emulators:open-website` | `id: string` | `boolean` |
| `emulators:system-assignments` | — | `Record<string, string[]>` |
| `emulators:update-controller-config` | `id, installPath, controllerName?` | `boolean` |
### ROMs / Games
| Channel | Arguments | Returns |
|---|---|---|
| `roms:scan` | `directory?: string` | `GameEntry[]` |
| `roms:select-directory` | — | `string \| null` |
| `game:launch` | `emulatorId, romPath` | `boolean` |
| `games:recent` | — | `GameEntry[]` |
| `games:clear-recent` | — | `boolean` |
| `games:scrape-art` | `title, platform` | `string \| undefined` |
| `games:cache-covers` | `{ romPath, coverUrl }[]` | `boolean` |
| `games:scrape-metadata` | `romPath, title, platform` | `GameMetadata` |
| `games:achievements` | `romPath, title, platform` | `AchievementInfo \| null` |
### Settings
| Channel | Arguments | Returns |
|---|---|---|
| `settings:get` | — | `AppSettings` |
| `settings:save` | `Partial<AppSettings>` | `AppSettings` |
| `settings:reset` | — | `AppSettings` |
### BIOS
| Channel | Arguments | Returns |
|---|---|---|
| `bios:list-known` | — | `BiosEntry[]` |
| `bios:scan` | `directory?: string` | `BiosCheckResult[]` |
| `bios:select-directory` | — | `string \| null` |
| `bios:configure-retroarch` | `configDir, biosDir` | `boolean` |
### RetroAchievements
| Channel | Arguments | Returns |
|---|---|---|
| `retroachievements:save` | `username, password` | `Record<string, boolean>` |
### Utilities
| Channel | Arguments | Returns |
|---|---|---|
| `utilities:regenerate-roms-structure` | — | `boolean` |
### Updates
| Channel | Arguments | Returns |
|---|---|---|
| `updates:check` | — | `boolean` |
| `updates:download` | — | `boolean` |
| `updates:quit-and-install` | — | `boolean` |
### Outbound Events (Main -> Renderer)
| Event | Data |
|---|---|
| `emulators:install-progress` | `InstallProgress` |
| `updates:status` | `{ status, version, releaseNotes, message, manualLink }` |
| `updates:download-progress` | `{ percent, bytesPerSecond, transferred, total }` |
---
## File Structure
```
OmniEmu2.0/
├── src/
│ ├── main/ # Electron main process
│ │ ├── index.ts # Entry point
│ │ ├── emulators.ts # Emulator registry & ROM scanning
│ │ ├── configurator.ts # Config presets & emulator setup
│ │ ├── ipc.ts # IPC handler registration
│ │ ├── preload.ts # Context bridge (window.omni)
│ │ ├── platform.ts # OS/arch detection
│ │ ├── settings.ts # Settings persistence
│ │ ├── installer.ts # Download & install emulators
│ │ ├── scraper.ts # Art & metadata scraping
│ │ ├── updater.ts # Auto-update
│ │ ├── bios.ts # BIOS detection
│ │ └── ra.ts # RetroAchievements
│ ├── renderer/ # React UI
│ │ ├── App.tsx # Root component
│ │ ├── main.tsx # React entry point
│ │ ├── index.html # HTML shell
│ │ ├── styles.css # Design system
│ │ ├── types.ts # window.omni types
│ │ ├── pages/ # Page components
│ │ ├── components/ # Shared components
│ │ ├── hooks/ # Custom hooks
│ │ └── utils/ # Utilities (markdown)
│ └── shared/
│ └── types.ts # Shared TypeScript types
├── assets/ # App icons
├── scripts/ # Build & install scripts
├── presets.json # Remote preset source
├── package.json # Project config & electron-builder
├── vite.config.ts # Vite build config
├── tsconfig.json # Base TS config
├── tsconfig.main.json # Main process TS config
├── tsconfig.renderer.json # Renderer TS config
├── LICENSE # MIT
└── README.md # Project docs
```
### Runtime Directories
| Path | Platform | Contents |
|---|---|---|
| `userData/settings.json` | All | App settings |
| `userData/cache/scrape-cache.json` | All | Cover art cache |
| `userData/cache/metadata-cache.json` | All | Game metadata cache |
| `userData/cache/ra-game-list-cache.json` | All | RetroAchievements game lists |
| `userData/emulators/<id>/.installed` | All | Installed emulator marker |
| `userData/configs/` | All | Config state markers |
| `~/Documents/roms/` | Default | ROM library root |
| `~/Documents/roms/<system>/` | Default | System-specific ROM folders |
---
## Build & Development
### Prerequisites
- Node.js 18+
- npm
- Python 3 (for icon generation)
- Platform-specific tools:
- macOS: Xcode Command Line Tools
- Windows: NSIS (for installer)
- Linux: libfuse2 (for AppImage)
### Commands
```bash
# Install dependencies
npm install
# Development (hot reload)
npm run dev
# Build renderer + main
npm run build
# Type checking
npm run typecheck
# Package for current platform
npm run package:mac # macOS universal
npm run package:win # Windows x64
npm run package:linux # Linux x64
# Package all platforms
npm run build:all # macOS universal + Win x64/arm64 + Linux x64/arm64
```
### Build Configuration
- **App ID:** `com.omniemu.app`
- **Product Name:** `OmniEmu`
- **Output:** `release/`
- **Renderer:** Vite builds to `dist/renderer/`
- **Main:** TypeScript compiles to `dist/main/`
- **Publish:** GitHub Releases
---
## Release Artifacts
| Platform | Format | Architecture | Filename Pattern |
|---|---|---|---|
| macOS | DMG | Universal (x64 + ARM64) | `OmniEmu-{ver}-mac-universal.dmg` |
| macOS | ZIP | Universal | `OmniEmu-{ver}-mac-universal.zip` |
| Windows | NSIS EXE | x64 | `OmniEmu-{ver}-win-x64.exe` |
| Windows | NSIS EXE | ARM64 | `OmniEmu-{ver}-win-arm64.exe` |
| Windows | ZIP | x64 | `OmniEmu-{ver}-win-x64.zip` |
| Windows | ZIP | ARM64 | `OmniEmu-{ver}-win-arm64.zip` |
| Linux | AppImage | x86_64 | `OmniEmu-{ver}-linux-x86_64.AppImage` |
| Linux | AppImage | ARM64 | `OmniEmu-{ver}-linux-arm64.AppImage` |
`.yml` files in releases are for electron-updater and can be ignored for manual installs.
---
## Security
- **Context Isolation:** Enabled
- **Node Integration:** Disabled
- **Sandbox:** Disabled (required for native module access)
- **CSP:** `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'`
- **Single Instance:** Enforced via `app.requestSingleInstanceLock()`
- **No secrets or API keys** are committed to the repository
---
## Contributing
### Code Style
- TypeScript strict mode
- React functional components with hooks
- No comments unless requested
- Follow existing patterns and naming conventions
### Key Conventions
- Emulators are defined in `knownEmulators` array in `src/main/emulators.ts`
- Config presets are in `builtInPresets` in `src/main/configurator.ts`
- All IPC channels are registered in `src/main/ipc.ts`
- All renderer API calls go through `window.omni` (defined in `src/main/preload.ts`)
- Shared types are in `src/shared/types.ts`
### Testing Changes
1. Run `npm run typecheck` to verify types
2. Run `npm run build` to verify compilation
3. Run `npm run dev` to test in development mode
4. Test on all target platforms if possible
---
*Last updated: July 2026*
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "omniemu2",
"version": "0.1.1",
"version": "0.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniemu2",
"version": "0.1.1",
"version": "0.1.2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "omniemu2",
"version": "0.1.2",
"version": "0.1.3",
"description": "Cross-platform emulator manager, game launcher and ROM manager",
"main": "dist/main/index.js",
"scripts": {
@@ -44,7 +44,6 @@
"build": {
"appId": "com.omniemu.app",
"productName": "OmniEmu",
"afterPack": "./scripts/adhoc-sign.cjs",
"publish": ["github"],
"directories": {
"output": "release",
+7
View File
@@ -96,5 +96,12 @@
"emu.cfg": "[config]\nrenderer = vulkan\nfullscreen = yes\nvsync = yes\nauto_region = yes\ncable_type = vga\nbroadcast = ntsc\nframeskip = 0\n[network]\nenable = no\n[input]\nenable_mouse = no\n"
}
}
],
"esde": [
{
"name": "OmniEmu Recommended",
"description": "Pre-configures ES-DE with your ROM directory and installed emulators",
"files": {}
}
]
}
+432 -2
View File
@@ -1,8 +1,10 @@
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { app } from 'electron';
import { ConfigPreset, InstallProgress, Platform } from '../shared/types';
import { getPlatform, isWindows, isMacOS } from './platform';
import { knownEmulators, checkEmulator } from './emulators';
import { settings } from './settings';
const platform = getPlatform();
@@ -256,6 +258,27 @@ volume = 100
},
},
],
esde: [
{
name: 'OmniEmu Recommended',
description: 'Pre-configures ES-DE with your ROM directory and installed emulators',
files: {},
},
],
neostation: [
{
name: 'OmniEmu Recommended',
description: 'Pre-configures NeoStation with your ROM directory and installed emulators',
files: {},
},
],
pegasus: [
{
name: 'OmniEmu Recommended',
description: 'Generates Pegasus collection files for all your ROMs with correct launch commands',
files: {},
},
],
};
// ---- Remote presets URL ----
@@ -267,7 +290,7 @@ async function fetchRemotePresets(): Promise<Record<string, ConfigPreset[]> | nu
const { get } = await import('https');
const data = await new Promise<string>((resolve, reject) => {
get(presetSourceUrl, {
headers: { 'User-Agent': 'OmniEmu/0.1.2' },
headers: { 'User-Agent': 'OmniEmu/0.1.3' },
timeout: 10000,
}, (res) => {
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
@@ -367,6 +390,21 @@ function getConfigDir(emulatorId: string, installPath: string): string {
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'melonDS'),
linux: join(require('os').homedir(), '.config', 'melonDS'),
},
esde: {
win32: join(require('os').homedir(), 'ES-DE'),
darwin: join(require('os').homedir(), 'ES-DE'),
linux: join(require('os').homedir(), 'ES-DE'),
},
neostation: {
win32: join(process.env.LOCALAPPDATA || join(require('os').homedir(), 'AppData', 'Local'), 'neostation'),
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'neostation'),
linux: join(require('os').homedir(), '.local', 'share', 'neostation'),
},
pegasus: {
win32: join(process.env.LOCALAPPDATA || join(require('os').homedir(), 'AppData', 'Local'), 'pegasus-frontend'),
darwin: join(require('os').homedir(), 'Library', 'Preferences', 'pegasus-frontend'),
linux: join(require('os').homedir(), '.config', 'pegasus-frontend'),
},
};
return platformDirs[emulatorId]?.[platform] || dirname(installPath);
}
@@ -419,12 +457,404 @@ export async function applyRecommendedConfig(
installPath: string,
onProgress?: (p: InstallProgress) => void
): Promise<boolean> {
if (emulatorId === 'esde') {
return configureEsde(installPath, onProgress);
}
if (emulatorId === 'neostation') {
return configureNeostation(installPath, onProgress);
}
if (emulatorId === 'pegasus') {
return configurePegasus(installPath, onProgress);
}
const presets = await getPresets(emulatorId);
if (presets.length === 0) return false;
await applyPreset(emulatorId, presets[0], installPath, onProgress);
return true;
}
// ---- ES-DE Configuration ----
/** Map OmniEmu emulator IDs to ES-DE find-rules emulator names */
const esdeEmulatorNames: Record<string, string> = {
retroarch: 'RETROARCH',
dolphin: 'DOLPHIN',
rpcs3: 'RPCS3',
pcsx2: 'PCSX2',
duckstation: 'DUCKSTATION',
mame: 'MAME',
ppsspp: 'PPSSPP',
melonds: 'MELONDS',
flycast: 'FLYCAST',
eden: 'EDEN',
};
function getEsdeEmulatorName(emulatorId: string): string | undefined {
return esdeEmulatorNames[emulatorId];
}
function generateEsFindRules(): string {
const lines: string[] = [
'<?xml version="1.0"?>',
'<!-- ES-DE find rules generated by OmniEmu -->',
'<ruleList>',
];
for (const emu of knownEmulators) {
if (emu.id === 'esde') continue;
const esdeName = getEsdeEmulatorName(emu.id);
if (!esdeName) continue;
const state = checkEmulator(emu.id);
if (!state.installed || !state.path) continue;
lines.push(` <emulator name="${esdeName}">`);
lines.push(' <rule type="staticpath">');
lines.push(` <entry>${state.path}</entry>`);
lines.push(' </rule>');
lines.push(' </emulator>');
}
lines.push('</ruleList>');
return lines.join('\n');
}
function generateEsSettingsXml(romDir: string, mediaDir: string): string {
return [
'<?xml version="1.0"?>',
'<!-- ES-DE settings generated by OmniEmu -->',
'<viewSettings>',
` <string name="ROMDirectory" value="${romDir}" />`,
` <string name="MediaDirectory" value="${mediaDir}" />`,
'</viewSettings>',
].join('\n');
}
async function configureEsde(
installPath: string,
onProgress?: (p: InstallProgress) => void
): Promise<boolean> {
const report = onProgress
? (percent: number, message: string) =>
onProgress({ emulatorId: 'esde', stage: 'configuring', percent, message })
: () => {};
report(0, 'Configuring ES-DE...');
const esdeConfigDir = getConfigDir('esde', installPath);
const settingsDir = join(esdeConfigDir, 'settings');
const customSystemsDir = join(esdeConfigDir, 'custom_systems');
if (!existsSync(settingsDir)) mkdirSync(settingsDir, { recursive: true });
if (!existsSync(customSystemsDir)) mkdirSync(customSystemsDir, { recursive: true });
report(25, 'Writing ES-DE settings...');
const romDir = settings.get().romsDirectory || join(require('os').homedir(), 'Documents', 'roms');
const mediaDir = join(esdeConfigDir, 'downloaded_media');
const settingsXml = generateEsSettingsXml(romDir, mediaDir);
writeFileSync(join(settingsDir, 'es_settings.xml'), settingsXml, 'utf-8');
report(50, 'Writing emulator find rules...');
const findRules = generateEsFindRules();
writeFileSync(join(customSystemsDir, 'es_find_rules.xml'), findRules, 'utf-8');
report(75, 'Creating media directory...');
if (!existsSync(mediaDir)) mkdirSync(mediaDir, { recursive: true });
report(100, 'ES-DE configured');
const markerDir = join(app.getPath('userData'), 'configs');
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
writeFileSync(join(markerDir, 'esde.configured'), new Date().toISOString(), 'utf-8');
return true;
}
// ---- NeoStation Configuration ----
function generateNeostationConfig(romDir: string): string {
const emulatorsDir = settings.get().emulatorsDirectory || join(app.getPath('userData'), 'emulators');
// Build emulator paths map from installed emulators
const emulatorPaths: Record<string, string> = {};
for (const emu of knownEmulators) {
if (emu.id === 'neostation' || emu.id === 'esde') continue;
const state = checkEmulator(emu.id);
if (state.installed && state.path) {
emulatorPaths[emu.id] = state.path;
}
}
return JSON.stringify({
roms_directory: romDir,
emulators_directory: emulatorsDir,
emulators: emulatorPaths,
}, null, 2);
}
async function configureNeostation(
installPath: string,
onProgress?: (p: InstallProgress) => void
): Promise<boolean> {
const report = onProgress
? (percent: number, message: string) =>
onProgress({ emulatorId: 'neostation', stage: 'configuring', percent, message })
: () => {};
report(0, 'Configuring NeoStation...');
const configDir = getConfigDir('neostation', installPath);
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
report(25, 'Writing NeoStation configuration...');
const romDir = settings.get().romsDirectory || join(require('os').homedir(), 'Documents', 'roms');
const configContent = generateNeostationConfig(romDir);
writeFileSync(join(configDir, 'omniemu_config.json'), configContent, 'utf-8');
report(50, 'Discovering installed emulators...');
// Create symlinks to ROM directories for NeoStation to discover
const romsLink = join(configDir, 'roms');
try {
if (!existsSync(romsLink)) {
const { symlinkSync } = require('fs');
symlinkSync(romDir, romsLink);
}
} catch { /* ignore if symlink fails */ }
report(75, 'Finalizing...');
report(100, 'NeoStation configured');
const markerDir = join(app.getPath('userData'), 'configs');
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
writeFileSync(join(markerDir, 'neostation.configured'), new Date().toISOString(), 'utf-8');
return true;
}
// ---- Pegasus Frontend Configuration ----
interface PegasusSystemMeta {
name: string;
shortname: string;
extensions: string[];
}
const pegasusSystemMeta: Record<string, PegasusSystemMeta> = {
nes: { name: 'Nintendo Entertainment System', shortname: 'nes', extensions: ['nes', 'fds'] },
snes: { name: 'Super Nintendo Entertainment System', shortname: 'snes', extensions: ['smc', 'sfc', 'fig', 'swc'] },
n64: { name: 'Nintendo 64', shortname: 'n64', extensions: ['n64', 'z64', 'v64'] },
gb: { name: 'Game Boy', shortname: 'gb', extensions: ['gb'] },
gbc: { name: 'Game Boy Color', shortname: 'gbc', extensions: ['gbc'] },
gba: { name: 'Game Boy Advance', shortname: 'gba', extensions: ['gba'] },
nds: { name: 'Nintendo DS', shortname: 'nds', extensions: ['nds'] },
ps1: { name: 'PlayStation', shortname: 'psx', extensions: ['bin', 'cue', 'iso', 'chd', 'pbp'] },
ps2: { name: 'PlayStation 2', shortname: 'ps2', extensions: ['iso', 'bin', 'chd', 'gz'] },
ps3: { name: 'PlayStation 3', shortname: 'ps3', extensions: ['iso'] },
psp: { name: 'PlayStation Portable', shortname: 'psp', extensions: ['iso', 'cso', 'pbp'] },
gc: { name: 'GameCube', shortname: 'gamecube', extensions: ['gcm', 'gcz', 'iso', 'rvz'] },
wii: { name: 'Wii', shortname: 'wii', extensions: ['wbfs', 'iso', 'gcz', 'rvz'] },
switch: { name: 'Nintendo Switch', shortname: 'switch', extensions: ['nsp', 'xci'] },
arcade: { name: 'Arcade', shortname: 'arcade', extensions: ['zip', '7z'] },
dreamcast: { name: 'Sega Dreamcast', shortname: 'dreamcast', extensions: ['gdi', 'cdi', 'chd'] },
'sega-md': { name: 'Sega Mega Drive', shortname: 'genesis', extensions: ['bin', 'md', 'smd'] },
'sega-saturn': { name: 'Sega Saturn', shortname: 'saturn', extensions: ['bin', 'cue', 'chd'] },
pce: { name: 'PC Engine', shortname: 'pce', extensions: ['pce'] },
};
function getPegasusLaunchCommand(emulatorPath: string, emulatorId: string): string {
const fp = '{file.path}';
const esc = (s: string) => `"${s}"`;
switch (emulatorId) {
case 'retroarch': {
const core = isWindows() ? '_libretro.dll' : '_libretro.so';
return esc(emulatorPath) + ' -L ' + esc('{core_dir}/' + core) + ' ' + esc(fp);
}
case 'dolphin': return esc(emulatorPath) + ' --exec=' + esc(fp);
case 'rpcs3': return esc(emulatorPath) + ' ' + esc(fp);
case 'pcsx2': return esc(emulatorPath) + ' ' + esc(fp);
case 'duckstation':return esc(emulatorPath) + ' ' + esc(fp);
case 'mame': return esc(emulatorPath) + ' ' + esc(fp);
case 'flycast': return esc(emulatorPath) + ' ' + esc(fp);
case 'melonDS': return esc(emulatorPath) + ' ' + esc(fp);
case 'eden': return esc(emulatorPath) + ' ' + esc(fp);
case 'ppsspp': return esc(emulatorPath) + ' ' + esc(fp);
default: return esc(emulatorPath) + ' ' + esc(fp);
}
}
function generatePegasusCollection(systemId: string, romDir: string, launchCmd: string): string {
const meta = pegasusSystemMeta[systemId];
if (!meta) return '';
const systemDir = join(romDir, systemId);
const lines: string[] = [];
lines.push(`collection: ${meta.name}`);
lines.push(`shortname: ${meta.shortname}`);
lines.push(`extensions: ${meta.extensions.join(', ')}`);
lines.push(`launch: ${launchCmd}`);
lines.push('');
// Scan for ROM files in this system directory
if (existsSync(systemDir)) {
try {
const files = readdirSync(systemDir);
for (const file of files) {
const filePath = join(systemDir, file);
const stat = statSync(filePath);
if (!stat.isFile()) continue;
const ext = file.split('.').pop()?.toLowerCase() || '';
if (!meta.extensions.includes(ext)) continue;
const title = file.replace(/\.[^/.]+$/, '').replace(/ \(.*\)$/, '');
lines.push(`game: ${title}`);
lines.push(`file: ${file}`);
lines.push(`launch: ${launchCmd}`);
lines.push('');
}
} catch { /* skip unreadable */ }
}
return lines.join('\n');
}
/** Generate Pegasus collection files in each ROM subdirectory. Called on app startup. */
export function generatePegasusCollectionsForRomDir(romDir: string): void {
if (!existsSync(romDir)) return;
const emuPaths: Record<string, string> = {};
for (const emu of knownEmulators) {
if (emu.id === 'pegasus' || emu.id === 'esde' || emu.id === 'neostation' || emu.id === 'emubuddy') continue;
const state = checkEmulator(emu.id);
if (state.installed && state.path) {
emuPaths[emu.id] = state.path;
}
}
const defaultEmu = emuPaths['retroarch'] || Object.values(emuPaths)[0] || 'retroarch';
const emuPrefs: Record<string, string> = {
nes: 'retroarch', snes: 'retroarch', n64: 'retroarch',
gb: 'retroarch', gbc: 'retroarch', gba: 'retroarch', nds: 'retroarch',
ps1: 'duckstation', ps2: 'pcsx2', psp: 'ppsspp',
gc: 'dolphin', wii: 'dolphin', switch: 'eden',
arcade: 'mame', dreamcast: 'flycast',
'sega-md': 'retroarch', 'sega-saturn': 'retroarch', pce: 'retroarch',
};
for (const [systemId, meta] of Object.entries(pegasusSystemMeta)) {
const systemDir = join(romDir, systemId);
if (!existsSync(systemDir)) continue;
const preferredEmu = emuPrefs[systemId] || 'retroarch';
const emuPath = emuPaths[preferredEmu] || defaultEmu;
const launchCmd = getPegasusLaunchCommand(emuPath, preferredEmu);
const collection = generatePegasusCollection(systemId, romDir, launchCmd);
if (!collection) continue;
writeFileSync(join(systemDir, 'metadata.pegasus.txt'), collection, 'utf-8');
}
}
async function configurePegasus(
installPath: string,
onProgress?: (p: InstallProgress) => void
): Promise<boolean> {
const report = onProgress
? (percent: number, message: string) =>
onProgress({ emulatorId: 'pegasus', stage: 'configuring', percent, message })
: () => {};
report(0, 'Configuring Pegasus Frontend...');
const configDir = getConfigDir('pegasus', installPath);
const metafilesDir = join(configDir, 'metafiles');
if (!existsSync(metafilesDir)) mkdirSync(metafilesDir, { recursive: true });
report(10, 'Scanning ROM directories...');
const romDir = settings.get().romsDirectory || join(require('os').homedir(), 'Documents', 'roms');
// Get installed emulator paths
const emuPaths: Record<string, string> = {};
for (const emu of knownEmulators) {
if (emu.id === 'pegasus' || emu.id === 'esde' || emu.id === 'neostation' || emu.id === 'emubuddy') continue;
const state = checkEmulator(emu.id);
if (state.installed && state.path) {
emuPaths[emu.id] = state.path;
}
}
const defaultEmu = emuPaths['retroarch'] || Object.values(emuPaths)[0] || 'retroarch';
const emuPrefs: Record<string, string> = {
nes: 'retroarch', snes: 'retroarch', n64: 'retroarch',
gb: 'retroarch', gbc: 'retroarch', gba: 'retroarch', nds: 'retroarch',
ps1: 'duckstation', ps2: 'pcsx2', psp: 'ppsspp',
gc: 'dolphin', wii: 'dolphin', switch: 'eden',
arcade: 'mame', dreamcast: 'flycast',
'sega-md': 'retroarch', 'sega-saturn': 'retroarch', pce: 'retroarch',
};
report(25, 'Writing collection files to ROM directories...');
let systemsProcessed = 0;
const totalSystems = Object.keys(pegasusSystemMeta).length;
// Write a metadata.pegasus.txt directly into each ROM subdirectory
for (const [systemId, meta] of Object.entries(pegasusSystemMeta)) {
systemsProcessed++;
report(25 + Math.round((systemsProcessed / totalSystems) * 50),
`Processing ${meta.name}...`);
const systemDir = join(romDir, systemId);
if (!existsSync(systemDir)) continue;
// Pick the best emulator for this system
const preferredEmu = emuPrefs[systemId] || 'retroarch';
const emuPath = emuPaths[preferredEmu] || defaultEmu;
const launchCmd = getPegasusLaunchCommand(emuPath, preferredEmu);
// Generate collection content for this system
const collection = generatePegasusCollection(systemId, romDir, launchCmd);
if (!collection) continue;
// Write metadata.pegasus.txt directly into the ROM subdirectory
writeFileSync(join(systemDir, 'metadata.pegasus.txt'), collection, 'utf-8');
// Also write a copy to Pegasus's central metafiles dir as backup
writeFileSync(join(metafilesDir, `${systemId}.pegasus.txt`), collection, 'utf-8');
}
report(80, 'Writing Pegasus settings...');
// Point Pegasus at the ROMs directory
const settingsFile = join(configDir, 'settings.txt');
const settingsContent = [
'# Pegasus settings generated by OmniEmu',
'game_dirs:',
` ${romDir}`,
].join('\n');
writeFileSync(settingsFile, settingsContent, 'utf-8');
report(100, 'Pegasus Frontend configured');
const markerDir = join(app.getPath('userData'), 'configs');
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
writeFileSync(join(markerDir, 'pegasus.configured'), new Date().toISOString(), 'utf-8');
return true;
}
// ---- Controller Configuration ----
/** Write controller config for a given emulator — PS-aware */
export function applyControllerConfig(emulatorId: string, installPath: string, controllerName?: string): boolean {
const configDir = getConfigDir(emulatorId, installPath);
+229 -1
View File
@@ -436,6 +436,186 @@ export const knownEmulators: EmulatorConfig[] = [
linux: 'https://github.com/flyinghead/flycast/releases',
},
},
{
id: 'esde',
name: 'ES-DE',
description: 'EmulationStation Desktop Edition — multi-system frontend & game launcher',
platforms: [
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
'ps1', 'ps2', 'ps3', 'psp', 'gc', 'wii', 'switch',
'arcade', 'dreamcast', 'sega-md', 'sega-saturn', 'sega-dc', 'pce',
],
defaultPath: {
win32: 'C:\\Program Files\\ES-DE\\esde.exe',
darwin: '/Applications/ES-DE.app/Contents/MacOS/esde',
linux: '/usr/bin/es-de',
},
downloads: {
win32: [
{
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288156889/download',
format: 'exe',
installerType: 'nsis',
executablePath: 'esde.exe',
},
],
darwin: [
{
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288889626/download',
format: 'dmg',
arch: 'arm64',
},
{
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288889701/download',
format: 'dmg',
arch: 'x64',
},
],
linux: [
{
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288156961/download',
format: 'appimage',
arch: 'x64',
},
{
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288156935/download',
format: 'appimage',
arch: 'arm64',
},
],
},
supported: true,
websiteUrl: {
win32: 'https://es-de.org/download/',
darwin: 'https://es-de.org/download/',
linux: 'https://es-de.org/download/',
},
},
{
id: 'neostation',
name: 'NeoStation',
description: 'Modern multi-platform emulation frontend — auto-discovers emulators and ROMs',
platforms: [
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
'ps1', 'ps2', 'ps3', 'psp', 'gc', 'wii', 'switch',
'arcade', 'dreamcast', 'sega-md', 'sega-saturn', 'sega-dc', 'pce',
],
defaultPath: {
win32: 'C:\\Program Files\\NeoStation\\neostation.exe',
darwin: '/Applications/NeoStation.app/Contents/MacOS/neostation',
linux: '/usr/bin/neostation',
},
downloads: {
win32: [
{
url: 'https://github.com/misobadev/neostation-frontend/releases/download/v0.9.3%2B114/neostation-windows-x64-0.9.3+114.zip',
format: 'zip',
executablePath: 'neostation.exe',
},
],
darwin: [
{
url: 'https://github.com/misobadev/neostation-frontend/releases/download/v0.9.3%2B114/neostation-macos-universal-0.9.3+114.dmg',
format: 'dmg',
},
],
linux: [
{
url: 'https://github.com/misobadev/neostation-frontend/releases/download/v0.9.3%2B114/neostation-linux-x86_64-0.9.3+114.AppImage',
format: 'appimage',
},
],
},
supported: true,
websiteUrl: {
win32: 'https://neostation.dev/downloads/',
darwin: 'https://neostation.dev/downloads/',
linux: 'https://neostation.dev/downloads/',
},
},
{
id: 'emubuddy',
name: 'EmuBuddy',
description: 'Cross-platform emulation frontend — one-click setup, game downloading, controller support',
platforms: [
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
'ps1', 'ps2', 'psp', 'gc', 'wii', 'dreamcast', 'sega-saturn',
],
defaultPath: {
win32: 'C:\\Program Files\\EmuBuddy\\EmuBuddyLauncher.exe',
darwin: '/Applications/EmuBuddy.app/Contents/MacOS/EmuBuddy',
linux: '/usr/bin/emubuddy',
},
downloads: {
win32: [
{
url: 'https://github.com/computerex/EmuBuddy/releases/download/1.0.0/EmuBuddy-Windows-v1.0.0.zip',
format: 'zip',
executablePath: 'EmuBuddyLauncher.exe',
},
],
darwin: [
{
url: 'https://github.com/computerex/EmuBuddy/releases/download/1.0.0/EmuBuddy-macOS-v1.0.0.zip',
format: 'zip',
},
],
linux: [
{
url: 'https://github.com/computerex/EmuBuddy/releases/download/1.0.0/EmuBuddy-Linux-v1.0.0.zip',
format: 'zip',
},
],
},
supported: true,
websiteUrl: {
win32: 'https://github.com/computerex/EmuBuddy',
darwin: 'https://github.com/computerex/EmuBuddy',
linux: 'https://github.com/computerex/EmuBuddy',
},
},
{
id: 'pegasus',
name: 'Pegasus Frontend',
description: 'Cross-platform customizable game launcher with theme support',
platforms: [
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
'ps1', 'ps2', 'ps3', 'psp', 'gc', 'wii', 'switch',
'arcade', 'dreamcast', 'sega-md', 'sega-saturn', 'sega-dc', 'pce',
],
defaultPath: {
win32: 'C:\\Program Files\\Pegasus\\pegasus-fe.exe',
darwin: '/Applications/Pegasus.app/Contents/MacOS/pegasus-fe',
linux: '/usr/bin/pegasus-fe',
},
downloads: {
win32: [
{
url: 'https://github.com/mmatyas/pegasus-frontend/releases/download/weekly_2024w38/pegasus-fe_alpha16-82-gc3462e68_win-mingw-static.zip',
format: 'zip',
executablePath: 'pegasus-fe.exe',
},
],
darwin: [
{
url: 'https://github.com/mmatyas/pegasus-frontend/releases/download/weekly_2024w38/pegasus-fe_alpha16-82-gc3462e68_macos-static.zip',
format: 'zip',
},
],
linux: [
{
url: 'https://github.com/mmatyas/pegasus-frontend/releases/download/weekly_2024w38/pegasus-fe_alpha16-82-gc3462e68_x11-static.zip',
format: 'zip',
},
],
},
supported: true,
websiteUrl: {
win32: 'https://pegasus-frontend.org/download/',
darwin: 'https://pegasus-frontend.org/download/',
linux: 'https://pegasus-frontend.org/download/',
},
},
];
export function findEmulator(id: string): EmulatorConfig | undefined {
@@ -525,6 +705,42 @@ function alternativePaths(emulatorId: string): string[] {
join(omniEmuDir, 'flycast'),
join(omniEmuDir, 'Flycast.app', 'Contents', 'MacOS', 'Flycast'),
join(home, 'Applications', 'Flycast.app', 'Contents', 'MacOS', 'Flycast'),
'/usr/local/bin/flycast',
],
esde: [
join(omniEmuDir, 'esde.exe'),
join(omniEmuDir, 'ES-DE', 'esde.exe'),
join(omniEmuDir, 'es-de'),
join(omniEmuDir, 'ES-DE.app', 'Contents', 'MacOS', 'esde'),
join(home, 'Applications', 'ES-DE.app', 'Contents', 'MacOS', 'esde'),
'/usr/local/bin/es-de',
'/usr/bin/es-de',
],
neostation: [
join(omniEmuDir, 'neostation.exe'),
join(omniEmuDir, 'NeoStation', 'neostation.exe'),
join(omniEmuDir, 'neostation'),
join(omniEmuDir, 'NeoStation.app', 'Contents', 'MacOS', 'neostation'),
join(home, 'Applications', 'NeoStation.app', 'Contents', 'MacOS', 'neostation'),
'/usr/local/bin/neostation',
'/usr/bin/neostation',
],
emubuddy: [
join(omniEmuDir, 'EmuBuddyLauncher.exe'),
join(omniEmuDir, 'EmuBuddy', 'EmuBuddyLauncher.exe'),
join(omniEmuDir, 'EmuBuddyLauncher'),
join(omniEmuDir, 'EmuBuddy.app', 'Contents', 'MacOS', 'EmuBuddy'),
join(home, 'Applications', 'EmuBuddy.app', 'Contents', 'MacOS', 'EmuBuddy'),
'/usr/local/bin/emubuddy',
'/usr/bin/emubuddy',
],
pegasus: [
join(omniEmuDir, 'pegasus-fe.exe'),
join(omniEmuDir, 'pegasus-fe'),
join(omniEmuDir, 'Pegasus.app', 'Contents', 'MacOS', 'pegasus-fe'),
join(home, 'Applications', 'Pegasus.app', 'Contents', 'MacOS', 'pegasus-fe'),
'/usr/local/bin/pegasus-fe',
'/usr/bin/pegasus-fe',
],
melonds: [
join(omniEmuDir, 'melonDS.exe'),
@@ -597,7 +813,11 @@ export function checkEmulator(id: string): EmulatorState {
}
export function getAllEmulatorStates(): EmulatorState[] {
return knownEmulators.map((e) => checkEmulator(e.id));
const s = settings.get();
const beta = !!s.betaFeatures;
return knownEmulators
.filter(e => (e.id === 'esde' || e.id === 'neostation' || e.id === 'pegasus') ? beta : true)
.map((e) => checkEmulator(e.id));
}
export function launchEmulator(emulatorId: string): boolean {
@@ -697,6 +917,14 @@ function launchArgs(emulatorId: string, romPath: string): string {
return `"${romPath}"`;
case 'flycast':
return `"${romPath}"`;
case 'esde':
return '';
case 'neostation':
return '';
case 'emubuddy':
return '';
case 'pegasus':
return '';
default:
return `"${romPath}"`;
}
+9
View File
@@ -6,6 +6,7 @@ import { settings } from './settings';
import { isMacOS } from './platform';
import { ensureRomsStructure } from './emulators';
import { setupAutoUpdater } from './updater';
import { generatePegasusCollectionsForRomDir } from './configurator';
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
@@ -123,6 +124,14 @@ app.whenReady().then(() => {
createWindow();
createTray();
// Auto-generate Pegasus collection files if Pegasus is configured
try {
const s = settings.get();
if (s.betaFeatures && s.romsDirectory) {
generatePegasusCollectionsForRomDir(s.romsDirectory);
}
} catch { /* ignore */ }
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
else mainWindow?.show();
+54 -9
View File
@@ -26,7 +26,7 @@ function downloadFile(url: string, dest: string, onProgress: (pct: number) => vo
const doRequest = (currentUrl: string) => {
const protocol = currentUrl.startsWith('https') ? httpsGet : httpGet;
const opts: RequestOptions = {
headers: { 'User-Agent': 'OmniEmu/0.1.2' },
headers: { 'User-Agent': 'OmniEmu/0.1.3' },
timeout: 30000,
};
protocol(currentUrl, opts, (response) => {
@@ -85,14 +85,18 @@ function findAppInDir(dir: string): string | undefined {
for (const e of dirs) {
if (e.name.endsWith('.app')) {
const macosBin = join(dir, e.name, 'Contents', 'MacOS', basename(e.name, '.app'));
if (existsSync(macosBin)) return macosBin;
const macosDir = join(dir, e.name, 'Contents', 'MacOS');
if (existsSync(macosDir)) {
const candidates = readdirSync(macosDir);
const bin = candidates.find(b => statSync(join(macosDir, b)).mode & 0o111);
if (bin) return join(macosDir, bin);
}
}
}
for (const e of files) {
const lowerName = e.name.toLowerCase();
const knownNames = ['retroarch', 'dolphin-emu', 'dolphin', 'rpcs3', 'ryujinx', 'eden', 'mame', 'mame64', 'pcsx2', 'duckstation'];
const knownNames = ['retroarch', 'dolphin-emu', 'dolphin', 'rpcs3', 'ryujinx', 'eden', 'mame', 'mame64', 'pcsx2', 'duckstation', 'esde'];
const isExec = e.name.endsWith('.exe') || e.name.endsWith('.AppImage')
|| knownNames.includes(lowerName);
if (isExec) return join(dir, e.name);
@@ -152,18 +156,56 @@ async function extractArchive(
}
case 'dmg': {
if (!isMacOS()) throw new Error('DMG files can only be extracted on macOS');
const mountPoint = `/tmp/omniemu_${emulatorId}_${randomBytes(4).toString('hex')}`;
onProgress('Opening DMG (please accept the license in the dialog)...');
execSync(`open "${archivePath}"`);
let mountPoint = '';
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 1000));
try {
const lines = execSync('ls /Volumes/').toString().split('\n').filter(Boolean);
for (const vol of lines) {
const volPath = join('/Volumes', vol);
if (existsSync(join(volPath, 'ES-DE.app')) || existsSync(join(volPath, '..app'))) {
mountPoint = volPath;
break;
}
}
if (!mountPoint) {
for (const vol of lines) {
const volPath = join('/Volumes', vol);
if (statSync(volPath).isDirectory() && vol.toLowerCase().includes('es-de')) {
mountPoint = volPath;
break;
}
}
}
if (!mountPoint) {
for (const vol of lines) {
if (vol === 'Macintosh HD' || vol.startsWith('Recovery') || vol === 'OpenCode' || vol.startsWith('com.apple')) continue;
const volPath = join('/Volumes', vol);
try {
const entries = readdirSync(volPath);
if (entries.some(e => e.endsWith('.app'))) {
mountPoint = volPath;
break;
}
} catch { /* skip */ }
}
}
if (mountPoint) break;
} catch { /* retry */ }
}
if (!mountPoint) throw new Error('DMG did not mount within 60 seconds. Please accept the license dialog and try again.');
try {
execOrThrow(`hdiutil attach "${archivePath}" -mountpoint "${mountPoint}" -nobrowse -quiet`);
const result = execSync(`ls "${mountPoint}" 2>/dev/null || true`).toString();
for (const item of result.split('\n').filter(Boolean)) {
const entries = readdirSync(mountPoint);
for (const item of entries) {
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`);
try { execSync(`hdiutil detach "${mountPoint}" -quiet 2>/dev/null`); } catch { /* ignore */ }
}
break;
}
@@ -344,6 +386,9 @@ export function findInstalledBinary(emulatorId: string, installDir: string, exec
if (emulatorId === 'retroarch') {
appNames.unshift('RetroArch.app');
}
if (emulatorId === 'esde') {
appNames.unshift('ES-DE.app');
}
for (const appName of appNames) {
const appDir = join(installDir, appName);
+37
View File
@@ -33,7 +33,11 @@ export function registerIpcHandlers(): void {
// Emulators
ipcMain.handle('emulators:list', () => {
const plat = getPlatform();
const s = settings.get();
const beta = !!s.betaFeatures;
return knownEmulators.filter(e => {
// Gate beta emulators behind beta toggle
if ((e.id === 'esde' || e.id === 'neostation' || e.id === 'pegasus') && !beta) return false;
const hasDownload = e.downloads?.[plat] && e.downloads[plat]!.length > 0;
const hasPackage = !!e.packageNames?.[plat];
return hasDownload || hasPackage;
@@ -49,6 +53,39 @@ export function registerIpcHandlers(): void {
async (event, emulatorId: string) => {
const config = findEmulator(emulatorId);
if (!config) throw new Error(`Unknown emulator: ${emulatorId}`);
// ES-DE on macOS: manual install (DMG requires license acceptance in Finder)
if (emulatorId === 'esde' && getPlatform() === 'darwin') {
const result = await dialog.showMessageBox(BrowserWindow.fromWebContents(event.sender)!, {
type: 'info',
title: 'Install ES-DE',
message: 'ES-DE must be installed manually on macOS',
detail: 'OmniEmu will open the ES-DE download page. Please download and install ES-DE, then return to OmniEmu and click Configure.',
buttons: ['Open Download Page', 'Cancel'],
defaultId: 0,
});
if (result.response === 0) {
shell.openExternal('https://es-de.org/#Download');
}
return checkEmulator(emulatorId);
}
// NeoStation on macOS: manual install (DMG requires license acceptance in Finder)
if (emulatorId === 'neostation' && getPlatform() === 'darwin') {
const result = await dialog.showMessageBox(BrowserWindow.fromWebContents(event.sender)!, {
type: 'info',
title: 'Install NeoStation',
message: 'NeoStation must be installed manually on macOS',
detail: 'OmniEmu will open the NeoStation download page. Please download and install NeoStation, then return to OmniEmu and click Configure.',
buttons: ['Open Download Page', 'Cancel'],
defaultId: 0,
});
if (result.response === 0) {
shell.openExternal('https://neostation.dev/downloads/');
}
return checkEmulator(emulatorId);
}
if (!config.downloads) throw new Error(`No downloads configured for ${emulatorId}`);
const platform = getPlatform();
+1 -1
View File
@@ -15,7 +15,7 @@ const raConsoleIds: Record<string, number> = {
dreamcast: 27, arcade: 99,
};
const userAgent = 'OmniEmu/0.1.2';
const userAgent = 'OmniEmu/0.1.3';
function fetchText(url: string): Promise<string> {
return new Promise((resolve, reject) => {
+3 -3
View File
@@ -120,7 +120,7 @@ export async function findValidThumbnail(title: string, platform: string): Promi
function fetchText(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const req = httpsGet(url, { headers: { 'User-Agent': 'OmniEmu/0.1.2' }, timeout: 10000 }, (res) => {
const req = httpsGet(url, { headers: { 'User-Agent': 'OmniEmu/0.1.3' }, timeout: 10000 }, (res) => {
let data = '';
res.on('data', (chunk: string) => data += chunk);
res.on('end', () => resolve(data));
@@ -134,7 +134,7 @@ function urlExists(url: string): Promise<boolean> {
return new Promise((resolve) => {
const req = httpsGet(url, {
method: 'GET',
headers: { 'User-Agent': 'OmniEmu/0.1.2' },
headers: { 'User-Agent': 'OmniEmu/0.1.3' },
timeout: 10000,
}, (res) => {
// GitHub raw returns 200 for existing or redirects to it
@@ -238,7 +238,7 @@ export function cacheMetadata(romPath: string, metadata: GameMetadata): void {
writeMetadataCache(cache);
}
const mobygamesUA = 'Mozilla/5.0 (compatible; OmniEmu/0.1.2; +https://github.com/mileswolfallen2/OmniEmu2.0)';
const mobygamesUA = 'Mozilla/5.0 (compatible; OmniEmu/0.1.3; +https://github.com/mileswolfallen2/OmniEmu2.0)';
function mobyFetch(url: string): Promise<string> {
return new Promise((resolve, reject) => {
+1 -1
View File
@@ -36,7 +36,7 @@ export function Sidebar({ currentPage, onNavigate }: SidebarProps) {
))}
</nav>
<div className="sidebar-footer">
v0.1.2
v0.1.3
</div>
</div>
);
+1 -1
View File
@@ -11,7 +11,7 @@ export function useGamepadNav(onNavigate: (page: Page) => void, currentPage: Pag
const lastTime = useRef<Record<string, number>>({});
const [connected, setConnected] = useState(false);
const [showLegend, setShowLegend] = useState(false);
const legendTimer = useRef<ReturnType<typeof setTimeout>>();
const legendTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
const showLegendTemporarily = useCallback(() => {
setShowLegend(true);
+1 -1
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import type { EmulatorState, SystemInfo, GameEntry } from '../../shared/types';
export function Dashboard({ onNavigate }: { onNavigate?: (tab: string) => void }) {
export function Dashboard({ onNavigate }: { onNavigate?: (tab: 'dashboard' | 'emulators' | 'library' | 'settings' | 'controller' | 'utilities') => void }) {
const [emulators, setEmulators] = useState<EmulatorState[]>([]);
const [system, setSystem] = useState<SystemInfo | null>(null);
const [recent, setRecent] = useState<GameEntry[]>([]);
+36 -14
View File
@@ -5,6 +5,8 @@ interface ProgressMap {
[emulatorId: string]: InstallProgress;
}
const isMacOS = navigator.userAgent.includes('Mac');
export function EmulatorsPage() {
const [states, setStates] = useState<EmulatorState[]>([]);
const [loading, setLoading] = useState(true);
@@ -209,20 +211,40 @@ export function EmulatorsPage() {
>
{!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 Only
</button>
{state.config.id === 'esde' && isMacOS ? (
<button
className="btn btn-primary btn-sm"
disabled={isActioning}
onClick={() => handleInstall(state.config.id)}
>
{isActioning ? 'Working...' : 'Install Manually'}
</button>
) : state.config.id === 'neostation' && isMacOS ? (
<button
className="btn btn-primary btn-sm"
disabled={isActioning}
onClick={() => handleInstall(state.config.id)}
>
{isActioning ? 'Working...' : 'Install Manually'}
</button>
) : (
<>
<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 Only
</button>
</>
)}
</>
)}
+22 -1
View File
@@ -224,6 +224,27 @@ export function SettingsPage() {
</div>
</div>
<div className="settings-section">
<h3>Experimental</h3>
<div className="setting-row">
<div>
<div className="setting-label">Enable Beta Features</div>
<div className="setting-desc">
Unlocks experimental emulators and frontends (ES-DE, NeoStation)
</div>
</div>
<label className="toggle">
<input
type="checkbox"
checked={!!settings.betaFeatures}
onChange={(e) => update({ betaFeatures: e.target.checked })}
/>
<span className="toggle-slider" />
</label>
</div>
</div>
<div className="settings-section">
<h3>Presets</h3>
@@ -390,7 +411,7 @@ export function SettingsPage() {
<div className="settings-section">
<h3>About</h3>
<p className="text-sm text-muted">
OmniEmu v{updateInfo?.version || '0.1.2'} · Cross-platform emulator manager
OmniEmu v{updateInfo?.version || '0.1.3'} · Cross-platform emulator manager
<br />
Built with Electron + React + TypeScript
</p>
+25
View File
@@ -19,13 +19,17 @@ declare global {
};
emulators: {
list: () => Promise<EmulatorConfig[]>;
systemAssignments: () => Promise<Record<string, string[]>>;
states: () => Promise<EmulatorState[]>;
check: (id: string) => Promise<EmulatorState>;
install: (id: string) => Promise<EmulatorState>;
launch: (id: string) => Promise<boolean>;
uninstall: (id: string) => Promise<{ removed: boolean; state: 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>;
updateControllerConfig: (id: string, installPath: string, controllerName?: string) => Promise<boolean>;
onInstallProgress: (cb: (p: InstallProgress) => void) => () => void;
};
roms: {
@@ -34,11 +38,19 @@ declare global {
};
game: {
launch: (emulatorId: string, romPath: string) => Promise<boolean>;
recent: () => Promise<GameEntry[]>;
clearRecent: () => Promise<boolean>;
scrapeArt: (title: string, platform: string) => Promise<string | undefined>;
cacheCovers: (entries: { romPath: string; coverUrl: string }[]) => Promise<boolean>;
scrapeMetadata: (romPath: string, title: string, platform: string) => Promise<GameMetadata>;
achievements: (romPath: string, title: string, platform: string) => Promise<AchievementInfo | null>;
};
bios: {
listKnown: () => Promise<any[]>;
scan: (directory?: string) => Promise<any[]>;
selectDirectory: () => Promise<string | null>;
configureRetroArch: (configDir: string, biosDir: string) => Promise<boolean>;
};
settings: {
get: () => Promise<AppSettings>;
save: (s: Partial<AppSettings>) => Promise<AppSettings>;
@@ -48,6 +60,19 @@ declare global {
romsDirectory: () => Promise<string>;
emulatorsDirectory: () => Promise<string>;
};
utilities: {
regenerateRomsStructure: () => Promise<boolean>;
};
retroachievements: {
save: (username: string, password: string) => Promise<Record<string, boolean>>;
};
updates: {
check: () => Promise<boolean>;
download: () => Promise<boolean>;
quitAndInstall: () => Promise<boolean>;
onStatus: (cb: (status: Record<string, unknown>) => void) => () => void;
onDownloadProgress: (cb: (progress: Record<string, unknown>) => void) => () => void;
};
};
}
}
+2
View File
@@ -117,6 +117,8 @@ export interface AppSettings {
retroAchievementsPassword?: string;
/** RetroAchievements Web API Key (from retroachievements.org/settings) */
retroAchievementsApiKey?: string;
/** Enable experimental/beta features (ES-DE, NeoStation, etc.) */
betaFeatures?: boolean;
}
export interface RetroAchievement {