mirror of
https://github.com/mileswolfallen2/OmniEmu2.0.git
synced 2026-09-08 11:23:17 +00:00
testing
This commit is contained in:
@@ -1,2 +1,96 @@
|
|||||||
# OmniEmu2.0
|
# OmniEmu
|
||||||
idk weeee will see
|
|
||||||
|
Cross-platform emulator manager and ROM library — for Windows, macOS, and Linux.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Emulator Management** — Detect installed emulators, download new ones, manage versions
|
||||||
|
- **ROM Library** — Scan directories for ROMs, organise by platform, launch games
|
||||||
|
- **Cross-Platform** — Same experience on Windows, macOS, and Linux (x86-64 & ARM64)
|
||||||
|
- **Built-in Launcher** — Launch games directly from the library with proper emulator arguments
|
||||||
|
|
||||||
|
## Supported Emulators
|
||||||
|
|
||||||
|
| Emulator | Systems | Windows | macOS | Linux |
|
||||||
|
|------------|-------------------|---------|-------|-------|
|
||||||
|
| Dolphin | GameCube, Wii | ✅ | ✅ | ✅ |
|
||||||
|
| RPCS3 | PlayStation 3 | ✅ | ✅ | ✅ |
|
||||||
|
| Ryujinx | Nintendo Switch | ✅ | ✅ | ✅ |
|
||||||
|
| PCSX2 | PlayStation 2 | ✅ | ✅ | ✅ |
|
||||||
|
| MAME | Arcade | ✅ | ✅ | ✅ |
|
||||||
|
| RetroArch | Multi-system | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20+ (recommended: 22)
|
||||||
|
- npm 10+
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the Vite dev server for the renderer and the TypeScript compiler for the main process in watch mode.
|
||||||
|
|
||||||
|
### Build for production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package for distribution
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Current platform
|
||||||
|
npm run package:mac
|
||||||
|
npm run package:win
|
||||||
|
npm run package:linux
|
||||||
|
|
||||||
|
# All platforms
|
||||||
|
npm run package:all
|
||||||
|
```
|
||||||
|
|
||||||
|
Output goes to `./release/`.
|
||||||
|
|
||||||
|
### Platform-specific scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS (specify arch: x64 or arm64)
|
||||||
|
./scripts/build-mac.sh arm64
|
||||||
|
|
||||||
|
# Windows (PowerShell)
|
||||||
|
.\scripts\build-win.ps1 -Arch x64
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
./scripts/build-linux.sh x64
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/ # Electron main process
|
||||||
|
│ ├── index.ts # App entry, window creation, tray
|
||||||
|
│ ├── preload.ts # Context bridge API
|
||||||
|
│ ├── ipc.ts # IPC handler registration
|
||||||
|
│ ├── platform.ts # OS/arch detection utilities
|
||||||
|
│ ├── emulators.ts # Emulator detection, ROM scanning, launching
|
||||||
|
│ └── settings.ts # Persistent settings
|
||||||
|
├── renderer/ # React UI (Vite)
|
||||||
|
│ ├── App.tsx
|
||||||
|
│ ├── components/
|
||||||
|
│ ├── pages/
|
||||||
|
│ └── styles.css
|
||||||
|
└── shared/ # Types shared between main & renderer
|
||||||
|
└── types.ts
|
||||||
|
scripts/ # Build and install scripts
|
||||||
|
.github/workflows/ # CI configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
Generated
+7160
File diff suppressed because it is too large
Load Diff
Regular → Executable
Regular → Executable
Regular → Executable
@@ -0,0 +1,282 @@
|
|||||||
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { app } from 'electron';
|
||||||
|
import { ConfigPreset, InstallProgress, Platform } from '../shared/types';
|
||||||
|
import { getPlatform, isWindows, isMacOS } from './platform';
|
||||||
|
|
||||||
|
const platform = getPlatform();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in recommended presets for each emulator.
|
||||||
|
* These are known-good configs curated by the EmuDeck community and
|
||||||
|
* adapted for OmniEmu. They set optimal performance/quality balances.
|
||||||
|
*/
|
||||||
|
const builtInPresets: Record<string, ConfigPreset[]> = {
|
||||||
|
dolphin: [
|
||||||
|
{
|
||||||
|
name: 'OmniEmu Recommended',
|
||||||
|
description: 'Best balance of performance and quality for most systems',
|
||||||
|
files: {
|
||||||
|
'Config/Dolphin.ini': `[General]
|
||||||
|
LastFilename =
|
||||||
|
ShowLag = False
|
||||||
|
ShowRenderTimes = False
|
||||||
|
[Display]
|
||||||
|
FullscreenDisplayRes = Auto
|
||||||
|
Fullscreen = True
|
||||||
|
RenderToMain = True
|
||||||
|
[Interface]
|
||||||
|
ConfirmStop = False
|
||||||
|
PauseOnFocusLost = False
|
||||||
|
[Core]
|
||||||
|
CPUCore = 3
|
||||||
|
Fastmem = True
|
||||||
|
MMU = False
|
||||||
|
[Enhancements]
|
||||||
|
InternalResolution = 3
|
||||||
|
MaxAnisotropy = 4
|
||||||
|
`,
|
||||||
|
'Config/GFX.ini': `[Settings]
|
||||||
|
Backend = Vulkan
|
||||||
|
ShaderCompilationMode = 2
|
||||||
|
WaitForShadersBeforeStarting = True
|
||||||
|
HiresTextures = False
|
||||||
|
CacheHiresTextures = True
|
||||||
|
[Enhancements]
|
||||||
|
ForceFiltering = False
|
||||||
|
WidescreenHack = True
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rpcs3: [
|
||||||
|
{
|
||||||
|
name: 'OmniEmu Recommended',
|
||||||
|
description: 'Optimized settings for RPCS3 with Vulkan backend',
|
||||||
|
files: {
|
||||||
|
'config.yml': `Video:
|
||||||
|
Format: Vulkan
|
||||||
|
FrameLimit: Auto
|
||||||
|
Resolution: 1920x1080
|
||||||
|
AntiAliasing: Disabled
|
||||||
|
RenderScale: 100
|
||||||
|
Audio:
|
||||||
|
Format: XAudio2
|
||||||
|
Device: Default
|
||||||
|
Input:
|
||||||
|
PadHandler: DualShock4
|
||||||
|
MouseHandler: Basic
|
||||||
|
CPU:
|
||||||
|
PPUDecoder: LLVM
|
||||||
|
SPUDecoder: ASMJIT
|
||||||
|
ThreadScheduler: OS
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ryujinx: [
|
||||||
|
{
|
||||||
|
name: 'OmniEmu Recommended',
|
||||||
|
description: 'Best settings for Ryujinx Switch emulation',
|
||||||
|
files: {
|
||||||
|
'Config.json': `{
|
||||||
|
"graphics_backend": "Vulkan",
|
||||||
|
"resolution_scale": 2,
|
||||||
|
"docked_mode": true,
|
||||||
|
"anisotropy_filtering": 4,
|
||||||
|
"aspect_ratio": "16:9",
|
||||||
|
"enable_vsync": true,
|
||||||
|
"shader_cache": true,
|
||||||
|
"audio_backend": "OpenAL"
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pcsx2: [
|
||||||
|
{
|
||||||
|
name: 'OmniEmu Recommended',
|
||||||
|
description: 'Optimal PCSX2 configuration for modern hardware',
|
||||||
|
files: {
|
||||||
|
'inis/PCSX2.ini': `[Filenames]
|
||||||
|
[Settings]
|
||||||
|
UserMode = 0
|
||||||
|
EnableVSync = 1
|
||||||
|
[EmuCore]
|
||||||
|
GSRenderer = 13
|
||||||
|
EnableCheats = 0
|
||||||
|
EnableWideScreenPatches = 1
|
||||||
|
[GS]
|
||||||
|
Renderer = Vulkan
|
||||||
|
UpscaleMultiplier = 3
|
||||||
|
BilinearFilter = 1
|
||||||
|
TrilinearFilter = 1
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
retroarch: [
|
||||||
|
{
|
||||||
|
name: 'OmniEmu Recommended',
|
||||||
|
description: 'Universal RetroArch config with optimal defaults',
|
||||||
|
files: {
|
||||||
|
'retroarch.cfg': `video_driver = "vulkan"
|
||||||
|
audio_driver = "pulseaudio"
|
||||||
|
input_driver = "udev"
|
||||||
|
video_fullscreen = true
|
||||||
|
video_vsync = true
|
||||||
|
video_scale_integer = false
|
||||||
|
video_smooth = true
|
||||||
|
audio_sync = true
|
||||||
|
savestate_thumbnail_enable = true
|
||||||
|
notification_show_autoconfig = false
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mame: [
|
||||||
|
{
|
||||||
|
name: 'OmniEmu Recommended',
|
||||||
|
description: 'MAME optimized settings',
|
||||||
|
files: {
|
||||||
|
'mame.ini': `#
|
||||||
|
# OmniEmu Recommended MAME Config
|
||||||
|
#
|
||||||
|
video auto
|
||||||
|
screen auto
|
||||||
|
aspect 4:3
|
||||||
|
effect none
|
||||||
|
waitvsync 1
|
||||||
|
syncrefresh 0
|
||||||
|
sleep 0
|
||||||
|
autosave 0
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const presetSourceUrl = 'https://raw.githubusercontent.com/mileswolfallen2/omniemu-presets/main/presets.json';
|
||||||
|
|
||||||
|
/** Fetch remote presets, falling back to built-in */
|
||||||
|
async function fetchRemotePresets(): Promise<Record<string, ConfigPreset[]> | null> {
|
||||||
|
try {
|
||||||
|
const { get } = await import('https');
|
||||||
|
const data = await new Promise<string>((resolve, reject) => {
|
||||||
|
get(presetSourceUrl, (res) => {
|
||||||
|
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (c: Buffer) => chunks.push(c));
|
||||||
|
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBuiltInPresets(): Record<string, ConfigPreset[]> {
|
||||||
|
return builtInPresets;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPresets(emulatorId: string): Promise<ConfigPreset[]> {
|
||||||
|
// Try remote first, fall back to built-in
|
||||||
|
const remote = await fetchRemotePresets();
|
||||||
|
if (remote && remote[emulatorId]) return remote[emulatorId];
|
||||||
|
return builtInPresets[emulatorId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the config directory for an emulator given its install path */
|
||||||
|
function getConfigDir(emulatorId: string, installPath: string): string {
|
||||||
|
const platformDirs: Record<string, Record<string, string>> = {
|
||||||
|
dolphin: {
|
||||||
|
win32: join(process.env.APPDATA || join(require('os').homedir(), 'AppData', 'Roaming'), 'Dolphin'),
|
||||||
|
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'Dolphin'),
|
||||||
|
linux: join(require('os').homedir(), '.config', 'dolphin-emu'),
|
||||||
|
},
|
||||||
|
rpcs3: {
|
||||||
|
win32: join(process.env.APPDATA || '', 'RPCS3'),
|
||||||
|
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'rpcs3'),
|
||||||
|
linux: join(require('os').homedir(), '.config', 'rpcs3'),
|
||||||
|
},
|
||||||
|
ryujinx: {
|
||||||
|
win32: join(process.env.APPDATA || '', 'Ryujinx'),
|
||||||
|
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'Ryujinx'),
|
||||||
|
linux: join(require('os').homedir(), '.config', 'Ryujinx'),
|
||||||
|
},
|
||||||
|
pcsx2: {
|
||||||
|
win32: join(process.env.APPDATA || '', 'PCSX2'),
|
||||||
|
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'PCSX2'),
|
||||||
|
linux: join(require('os').homedir(), '.config', 'PCSX2'),
|
||||||
|
},
|
||||||
|
retroarch: {
|
||||||
|
win32: join(process.env.APPDATA || '', 'RetroArch'),
|
||||||
|
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'RetroArch'),
|
||||||
|
linux: join(require('os').homedir(), '.config', 'retroarch'),
|
||||||
|
},
|
||||||
|
mame: {
|
||||||
|
win32: dirname(installPath),
|
||||||
|
darwin: dirname(installPath),
|
||||||
|
linux: join(require('os').homedir(), '.mame'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return platformDirs[emulatorId]?.[platform] || dirname(installPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if an emulator has been configured with OmniEmu presets */
|
||||||
|
export function checkConfigured(emulatorId: string, installPath?: string): boolean {
|
||||||
|
if (!installPath) return false;
|
||||||
|
const marker = join(app.getPath('userData'), 'configs', `${emulatorId}.configured`);
|
||||||
|
return existsSync(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply a config preset to the emulator's config directory */
|
||||||
|
export async function applyPreset(
|
||||||
|
emulatorId: string,
|
||||||
|
preset: ConfigPreset,
|
||||||
|
installPath: string,
|
||||||
|
onProgress?: (p: InstallProgress) => void
|
||||||
|
): Promise<void> {
|
||||||
|
const configDir = getConfigDir(emulatorId, installPath);
|
||||||
|
const report = onProgress
|
||||||
|
? (percent: number, message: string) =>
|
||||||
|
onProgress({ emulatorId, stage: 'configuring', percent, message })
|
||||||
|
: () => {};
|
||||||
|
|
||||||
|
report(0, `Configuring ${emulatorId} with "${preset.name}"...`);
|
||||||
|
|
||||||
|
const totalFiles = Object.keys(preset.files).length;
|
||||||
|
let done = 0;
|
||||||
|
|
||||||
|
for (const [relativePath, content] of Object.entries(preset.files)) {
|
||||||
|
const fullPath = join(configDir, relativePath);
|
||||||
|
const parentDir = dirname(fullPath);
|
||||||
|
|
||||||
|
if (!existsSync(parentDir)) {
|
||||||
|
mkdirSync(parentDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(fullPath, content, 'utf-8');
|
||||||
|
|
||||||
|
done++;
|
||||||
|
report(Math.round((done / totalFiles) * 100), `Wrote ${relativePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write marker so we know this emulator was configured
|
||||||
|
const markerDir = join(app.getPath('userData'), 'configs');
|
||||||
|
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
||||||
|
writeFileSync(join(markerDir, `${emulatorId}.configured`), new Date().toISOString(), 'utf-8');
|
||||||
|
|
||||||
|
report(100, `${emulatorId} configured with "${preset.name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply the recommended preset (first preset available) */
|
||||||
|
export async function applyRecommendedConfig(
|
||||||
|
emulatorId: string,
|
||||||
|
installPath: string,
|
||||||
|
onProgress?: (p: InstallProgress) => void
|
||||||
|
): Promise<boolean> {
|
||||||
|
const presets = await getPresets(emulatorId);
|
||||||
|
if (presets.length === 0) return false;
|
||||||
|
await applyPreset(emulatorId, presets[0], installPath, onProgress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+215
-110
@@ -1,18 +1,18 @@
|
|||||||
import { execSync, exec, ChildProcess } from 'child_process';
|
import { execSync, exec, ChildProcess } from 'child_process';
|
||||||
import { existsSync, mkdirSync } from 'fs';
|
import { existsSync, mkdirSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join, dirname, basename, extname } from 'path';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import {
|
import {
|
||||||
EmulatorConfig,
|
EmulatorConfig,
|
||||||
EmulatorState,
|
EmulatorState,
|
||||||
Platform,
|
|
||||||
GameEntry,
|
GameEntry,
|
||||||
RomFile,
|
InstallProgress,
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
import { getPlatform, getArch, isWindows, isMacOS, isLinux } from './platform';
|
import { getPlatform, getArch, isWindows, isMacOS, isLinux } from './platform';
|
||||||
import { settings } from './settings';
|
import { settings } from './settings';
|
||||||
|
|
||||||
const platform = getPlatform();
|
const platform = getPlatform();
|
||||||
|
const arch = getArch();
|
||||||
|
|
||||||
export const knownEmulators: EmulatorConfig[] = [
|
export const knownEmulators: EmulatorConfig[] = [
|
||||||
{
|
{
|
||||||
@@ -25,13 +25,41 @@ export const knownEmulators: EmulatorConfig[] = [
|
|||||||
darwin: '/Applications/Dolphin.app/Contents/MacOS/Dolphin',
|
darwin: '/Applications/Dolphin.app/Contents/MacOS/Dolphin',
|
||||||
linux: '/usr/bin/dolphin-emu',
|
linux: '/usr/bin/dolphin-emu',
|
||||||
},
|
},
|
||||||
installUrl: {
|
downloads: {
|
||||||
|
win32: [
|
||||||
|
{
|
||||||
|
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-x64.7z',
|
||||||
|
format: '7z',
|
||||||
|
executablePath: 'Dolphin.exe',
|
||||||
|
arch: 'x64',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
darwin: [
|
||||||
|
{
|
||||||
|
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-universal.dmg',
|
||||||
|
format: 'dmg',
|
||||||
|
arch: 'arm64',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-x64.dmg',
|
||||||
|
format: 'dmg',
|
||||||
|
arch: 'x64',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
{
|
||||||
|
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-x86_64.AppImage',
|
||||||
|
format: 'appimage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
packageNames: { linux: 'dolphin-emu' },
|
||||||
|
supported: true,
|
||||||
|
websiteUrl: {
|
||||||
win32: 'https://dolphin-emu.org/download/',
|
win32: 'https://dolphin-emu.org/download/',
|
||||||
darwin: 'https://dolphin-emu.org/download/',
|
darwin: 'https://dolphin-emu.org/download/',
|
||||||
linux: null,
|
linux: 'https://dolphin-emu.org/download/',
|
||||||
},
|
},
|
||||||
installVia: 'download',
|
|
||||||
supported: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rpcs3',
|
id: 'rpcs3',
|
||||||
@@ -43,31 +71,27 @@ export const knownEmulators: EmulatorConfig[] = [
|
|||||||
darwin: '/Applications/RPCS3.app/Contents/MacOS/rpcs3',
|
darwin: '/Applications/RPCS3.app/Contents/MacOS/rpcs3',
|
||||||
linux: '/usr/bin/rpcs3',
|
linux: '/usr/bin/rpcs3',
|
||||||
},
|
},
|
||||||
installUrl: {
|
downloads: {
|
||||||
win32: 'https://rpcs3.net/download',
|
win32: [
|
||||||
darwin: null,
|
{
|
||||||
linux: null,
|
url: 'https://github.com/RPCS3/rpcs3-binaries-win/releases/download/build-d2c3b344332efc6e545a09ad44b9f083c2a1a519/rpcs3-v0.0.34-17491-d2c3b344_win64.7z',
|
||||||
|
format: '7z',
|
||||||
|
executablePath: 'rpcs3.exe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/RPCS3/rpcs3-binaries-linux/releases/download/build-d2c3b344332efc6e545a09ad44b9f083c2a1a519/rpcs3-v0.0.34-17491-d2c3b344_linux64.AppImage',
|
||||||
|
format: 'appimage',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
installVia: 'download',
|
|
||||||
supported: true,
|
supported: true,
|
||||||
},
|
websiteUrl: {
|
||||||
{
|
win32: 'https://rpcs3.net/download',
|
||||||
id: 'yuzu',
|
darwin: 'https://rpcs3.net/download',
|
||||||
name: 'Yuzu',
|
linux: 'https://rpcs3.net/download',
|
||||||
description: 'Nintendo Switch emulator',
|
|
||||||
platforms: ['switch'],
|
|
||||||
defaultPath: {
|
|
||||||
win32: 'C:\\Program Files\\Yuzu\\yuzu.exe',
|
|
||||||
darwin: '/Applications/Yuzu.app/Contents/MacOS/yuzu',
|
|
||||||
linux: '/usr/bin/yuzu',
|
|
||||||
},
|
},
|
||||||
installUrl: {
|
|
||||||
win32: 'https://yuzu-emu.org/downloads/',
|
|
||||||
darwin: null,
|
|
||||||
linux: null,
|
|
||||||
},
|
|
||||||
installVia: 'download',
|
|
||||||
supported: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ryujinx',
|
id: 'ryujinx',
|
||||||
@@ -79,13 +103,35 @@ export const knownEmulators: EmulatorConfig[] = [
|
|||||||
darwin: '/Applications/Ryujinx.app/Contents/MacOS/Ryujinx',
|
darwin: '/Applications/Ryujinx.app/Contents/MacOS/Ryujinx',
|
||||||
linux: '/usr/bin/Ryujinx',
|
linux: '/usr/bin/Ryujinx',
|
||||||
},
|
},
|
||||||
installUrl: {
|
downloads: {
|
||||||
|
win32: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/Ryujinx/release-channel-master/releases/latest/download/ryujinx-1.2.0-win_x64.zip',
|
||||||
|
format: 'zip',
|
||||||
|
executablePath: 'Ryujinx.exe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
darwin: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/Ryujinx/release-channel-master/releases/latest/download/ryujinx-1.2.0-mac_universal.zip',
|
||||||
|
format: 'zip',
|
||||||
|
executablePath: 'Ryujinx.app/Contents/MacOS/Ryujinx',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/Ryujinx/release-channel-master/releases/latest/download/ryujinx-1.2.0-linux_x64.zip',
|
||||||
|
format: 'zip',
|
||||||
|
executablePath: 'Ryujinx',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
supported: true,
|
||||||
|
websiteUrl: {
|
||||||
win32: 'https://ryujinx.org/download',
|
win32: 'https://ryujinx.org/download',
|
||||||
darwin: 'https://ryujinx.org/download',
|
darwin: 'https://ryujinx.org/download',
|
||||||
linux: 'https://ryujinx.org/download',
|
linux: 'https://ryujinx.org/download',
|
||||||
},
|
},
|
||||||
installVia: 'download',
|
|
||||||
supported: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pcsx2',
|
id: 'pcsx2',
|
||||||
@@ -97,13 +143,39 @@ export const knownEmulators: EmulatorConfig[] = [
|
|||||||
darwin: '/Applications/PCSX2.app/Contents/MacOS/PCSX2',
|
darwin: '/Applications/PCSX2.app/Contents/MacOS/PCSX2',
|
||||||
linux: '/usr/bin/pcsx2',
|
linux: '/usr/bin/pcsx2',
|
||||||
},
|
},
|
||||||
installUrl: {
|
downloads: {
|
||||||
|
win32: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-windows-x64-installer.exe',
|
||||||
|
format: 'exe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
darwin: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-macos-arm64.dmg',
|
||||||
|
format: 'dmg',
|
||||||
|
arch: 'arm64',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-macos-x64.dmg',
|
||||||
|
format: 'dmg',
|
||||||
|
arch: 'x64',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-linux-x86_64.AppImage',
|
||||||
|
format: 'appimage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
packageNames: { linux: 'pcsx2' },
|
||||||
|
supported: true,
|
||||||
|
websiteUrl: {
|
||||||
win32: 'https://pcsx2.net/downloads/',
|
win32: 'https://pcsx2.net/downloads/',
|
||||||
darwin: 'https://pcsx2.net/downloads/',
|
darwin: 'https://pcsx2.net/downloads/',
|
||||||
linux: null,
|
linux: 'https://pcsx2.net/downloads/',
|
||||||
},
|
},
|
||||||
installVia: 'downloacd',
|
|
||||||
supported: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mame',
|
id: 'mame',
|
||||||
@@ -115,18 +187,33 @@ export const knownEmulators: EmulatorConfig[] = [
|
|||||||
darwin: '/Applications/MAME.app/Contents/MacOS/mame',
|
darwin: '/Applications/MAME.app/Contents/MacOS/mame',
|
||||||
linux: '/usr/bin/mame',
|
linux: '/usr/bin/mame',
|
||||||
},
|
},
|
||||||
installUrl: {
|
downloads: {
|
||||||
win32: 'https://www.mamedev.org/release.html',
|
win32: [
|
||||||
darwin: null,
|
{
|
||||||
linux: null,
|
url: 'https://github.com/mamedev/mame/releases/download/mame0276/mame0276_64bit.7z',
|
||||||
|
format: '7z',
|
||||||
|
executablePath: 'mame64.exe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
{
|
||||||
|
url: 'https://github.com/mamedev/mame/releases/download/mame0276/mame0276-x86_64.AppImage',
|
||||||
|
format: 'appimage',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
installVia: 'download',
|
packageNames: { linux: 'mame' },
|
||||||
supported: true,
|
supported: true,
|
||||||
|
websiteUrl: {
|
||||||
|
win32: 'https://www.mamedev.org/release.html',
|
||||||
|
darwin: 'https://www.mamedev.org/release.html',
|
||||||
|
linux: 'https://www.mamedev.org/release.html',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'retroarch',
|
id: 'retroarch',
|
||||||
name: 'RetroArch',
|
name: 'RetroArch',
|
||||||
description: 'Multi-system emulator frontend',
|
description: 'Multi-system emulator frontend (NES, SNES, N64, GB, GBA, PS1, etc.)',
|
||||||
platforms: [
|
platforms: [
|
||||||
'nes', 'snes', 'n64', 'gb', 'gba', 'gbc',
|
'nes', 'snes', 'n64', 'gb', 'gba', 'gbc',
|
||||||
'ps1', 'pce', 'sega-md', 'sega-saturn', 'sega-dc',
|
'ps1', 'pce', 'sega-md', 'sega-saturn', 'sega-dc',
|
||||||
@@ -136,17 +223,42 @@ export const knownEmulators: EmulatorConfig[] = [
|
|||||||
darwin: '/Applications/RetroArch.app/Contents/MacOS/RetroArch',
|
darwin: '/Applications/RetroArch.app/Contents/MacOS/RetroArch',
|
||||||
linux: '/usr/bin/retroarch',
|
linux: '/usr/bin/retroarch',
|
||||||
},
|
},
|
||||||
installUrl: {
|
downloads: {
|
||||||
|
win32: [
|
||||||
|
{
|
||||||
|
url: 'https://buildbot.libretro.com/stable/1.19.1/windows/x86_64/RetroArch.7z',
|
||||||
|
format: '7z',
|
||||||
|
executablePath: 'RetroArch.exe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
darwin: [
|
||||||
|
{
|
||||||
|
url: 'https://buildbot.libretro.com/stable/1.19.1/apple/osx/universal/RetroArch.dmg',
|
||||||
|
format: 'dmg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
{
|
||||||
|
url: 'https://buildbot.libretro.com/stable/1.19.1/linux/x86_64/RetroArch.7z',
|
||||||
|
format: '7z',
|
||||||
|
executablePath: 'retroarch',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
packageNames: {
|
||||||
|
linux: 'retroarch',
|
||||||
|
darwin: 'retroarch',
|
||||||
|
},
|
||||||
|
supported: true,
|
||||||
|
websiteUrl: {
|
||||||
win32: 'https://retroarch.com/?page=platforms',
|
win32: 'https://retroarch.com/?page=platforms',
|
||||||
darwin: 'https://retroarch.com/?page=platforms',
|
darwin: 'https://retroarch.com/?page=platforms',
|
||||||
linux: 'https://retroarch.com/?page=platforms',
|
linux: 'https://retroarch.com/?page=platforms',
|
||||||
},
|
},
|
||||||
installVia: 'download',
|
|
||||||
supported: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function findEmulator(id: string): EmulatorConfig | undefined {
|
export function findEmulator(id: string): EmulatorConfig | undefined {
|
||||||
return knownEmulators.find((e) => e.id === id);
|
return knownEmulators.find((e) => e.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,20 +266,18 @@ function detectEmulatorPath(config: EmulatorConfig): string | undefined {
|
|||||||
const path = config.defaultPath[platform];
|
const path = config.defaultPath[platform];
|
||||||
if (path && existsSync(path)) return path;
|
if (path && existsSync(path)) return path;
|
||||||
|
|
||||||
// Search common alternative paths
|
|
||||||
const alternatives = alternativePaths(config.id);
|
const alternatives = alternativePaths(config.id);
|
||||||
for (const alt of alternatives) {
|
for (const alt of alternatives) {
|
||||||
if (existsSync(alt)) return alt;
|
if (existsSync(alt)) return alt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try which/where command
|
|
||||||
try {
|
try {
|
||||||
const cmd = isWindows() ? 'where' : 'which';
|
const cmd = isWindows() ? 'where' : 'which';
|
||||||
const result = execSync(`${cmd} ${config.id} 2>${isWindows() ? 'nul' : '/dev/null'}`)
|
const result = execSync(`${cmd} ${config.id} 2>${isWindows() ? 'nul' : '/dev/null'}`)
|
||||||
.toString().trim();
|
.toString().trim();
|
||||||
if (result) return result.split('\n')[0];
|
if (result) return result.split('\n')[0];
|
||||||
} catch {
|
} catch {
|
||||||
// not found via path
|
// not found
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -175,19 +285,47 @@ function detectEmulatorPath(config: EmulatorConfig): string | undefined {
|
|||||||
|
|
||||||
function alternativePaths(emulatorId: string): string[] {
|
function alternativePaths(emulatorId: string): string[] {
|
||||||
const home = require('os').homedir();
|
const home = require('os').homedir();
|
||||||
|
const userData = app.getPath('userData');
|
||||||
|
const omniEmuDir = join(userData, 'emulators', emulatorId);
|
||||||
|
|
||||||
const common: Record<string, string[]> = {
|
const common: Record<string, string[]> = {
|
||||||
dolphin: [
|
dolphin: [
|
||||||
|
join(omniEmuDir, 'Dolphin.exe'),
|
||||||
|
join(omniEmuDir, 'Dolphin'),
|
||||||
|
join(omniEmuDir, 'dolphin-emu'),
|
||||||
join(home, 'Applications', 'Dolphin.app', 'Contents', 'MacOS', 'Dolphin'),
|
join(home, 'Applications', 'Dolphin.app', 'Contents', 'MacOS', 'Dolphin'),
|
||||||
'/usr/local/bin/dolphin-emu',
|
'/usr/local/bin/dolphin-emu',
|
||||||
'/snap/bin/dolphin-emu',
|
'/snap/bin/dolphin-emu',
|
||||||
],
|
],
|
||||||
retroarch: [
|
retroarch: [
|
||||||
|
join(omniEmuDir, 'RetroArch.exe'),
|
||||||
|
join(omniEmuDir, 'RetroArch'),
|
||||||
|
join(omniEmuDir, 'retroarch'),
|
||||||
join(home, 'Applications', 'RetroArch.app', 'Contents', 'MacOS', 'RetroArch'),
|
join(home, 'Applications', 'RetroArch.app', 'Contents', 'MacOS', 'RetroArch'),
|
||||||
'/usr/local/bin/retroarch',
|
'/usr/local/bin/retroarch',
|
||||||
'/snap/bin/retroarch',
|
'/snap/bin/retroarch',
|
||||||
],
|
],
|
||||||
|
rpcs3: [
|
||||||
|
join(omniEmuDir, 'rpcs3.exe'),
|
||||||
|
join(omniEmuDir, 'rpcs3'),
|
||||||
|
join(omniEmuDir, 'RPCS3.AppImage'),
|
||||||
|
],
|
||||||
|
ryujinx: [
|
||||||
|
join(omniEmuDir, 'Ryujinx.exe'),
|
||||||
|
join(omniEmuDir, 'Ryujinx'),
|
||||||
|
],
|
||||||
|
pcsx2: [
|
||||||
|
join(omniEmuDir, 'pcsx2.exe'),
|
||||||
|
join(omniEmuDir, 'PCSX2'),
|
||||||
|
join(omniEmuDir, 'pcsx2.AppImage'),
|
||||||
|
],
|
||||||
|
mame: [
|
||||||
|
join(omniEmuDir, 'mame64.exe'),
|
||||||
|
join(omniEmuDir, 'mame'),
|
||||||
|
join(omniEmuDir, 'MAME.AppImage'),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
return common[emulatorId] || [];
|
return common[emulatorId] || [join(omniEmuDir)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkEmulator(id: string): EmulatorState {
|
export function checkEmulator(id: string): EmulatorState {
|
||||||
@@ -195,14 +333,14 @@ export function checkEmulator(id: string): EmulatorState {
|
|||||||
if (!config) {
|
if (!config) {
|
||||||
return {
|
return {
|
||||||
installed: false,
|
installed: false,
|
||||||
|
configured: false,
|
||||||
config: {
|
config: {
|
||||||
id,
|
id,
|
||||||
name: id,
|
name: id,
|
||||||
description: '',
|
description: '',
|
||||||
platforms: [],
|
platforms: [],
|
||||||
defaultPath: { win32: '', darwin: '', linux: '' },
|
defaultPath: { win32: '', darwin: '', linux: '' },
|
||||||
installUrl: { win32: null, darwin: null, linux: null },
|
downloads: {},
|
||||||
installVia: 'manual',
|
|
||||||
supported: false,
|
supported: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -226,6 +364,7 @@ export function checkEmulator(id: string): EmulatorState {
|
|||||||
version,
|
version,
|
||||||
path,
|
path,
|
||||||
config,
|
config,
|
||||||
|
configured: !!path && existsSync(join(app.getPath('userData'), 'configs', `${id}.configured`)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +383,7 @@ export function launchGame(emulatorId: string, romPath: string): ChildProcess |
|
|||||||
const cmd = `"${state.path}" ${args}`;
|
const cmd = `"${state.path}" ${args}`;
|
||||||
|
|
||||||
const proc = exec(cmd, {
|
const proc = exec(cmd, {
|
||||||
cwd: require('path').dirname(state.path),
|
cwd: dirname(state.path),
|
||||||
});
|
});
|
||||||
|
|
||||||
return proc;
|
return proc;
|
||||||
@@ -285,18 +424,12 @@ export function getEmulatorsDirectory(): string {
|
|||||||
const s = settings.get();
|
const s = settings.get();
|
||||||
if (s.emulatorsDirectory && existsSync(s.emulatorsDirectory)) return s.emulatorsDirectory;
|
if (s.emulatorsDirectory && existsSync(s.emulatorsDirectory)) return s.emulatorsDirectory;
|
||||||
|
|
||||||
const home = require('os').homedir();
|
const dir = join(app.getPath('userData'), 'emulators');
|
||||||
const defaultDir = join(home, 'OmniEmu', 'emulators');
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||||
if (!existsSync(defaultDir)) {
|
return dir;
|
||||||
mkdirSync(defaultDir, { recursive: true });
|
|
||||||
}
|
|
||||||
return defaultDir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scanRoms(directory: string): GameEntry[] {
|
export function scanRoms(directory: string): GameEntry[] {
|
||||||
const { readdirSync, statSync } = require('fs');
|
|
||||||
const { extname, basename, join: pathJoin } = require('path');
|
|
||||||
|
|
||||||
const romExtensions = [
|
const romExtensions = [
|
||||||
'.nes', '.sfc', '.smc', '.n64', '.z64', '.v64',
|
'.nes', '.sfc', '.smc', '.n64', '.z64', '.v64',
|
||||||
'.gba', '.gb', '.gbc', '.nds', '.iso', '.bin', '.cue',
|
'.gba', '.gb', '.gbc', '.nds', '.iso', '.bin', '.cue',
|
||||||
@@ -309,10 +442,10 @@ export function scanRoms(directory: string): GameEntry[] {
|
|||||||
|
|
||||||
function scanDir(dir: string) {
|
function scanDir(dir: string) {
|
||||||
try {
|
try {
|
||||||
const files = readdirSync(dir);
|
const files = require('fs').readdirSync(dir);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const fullPath = pathJoin(dir, file);
|
const fullPath = join(dir, file);
|
||||||
const stat = statSync(fullPath);
|
const stat = require('fs').statSync(fullPath);
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
scanDir(fullPath);
|
scanDir(fullPath);
|
||||||
} else {
|
} else {
|
||||||
@@ -330,9 +463,7 @@ export function scanRoms(directory: string): GameEntry[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch { /* skip unreadable */ }
|
||||||
// skip unreadable dirs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scanDir(directory);
|
scanDir(directory);
|
||||||
@@ -341,55 +472,29 @@ export function scanRoms(directory: string): GameEntry[] {
|
|||||||
|
|
||||||
function guessPlatform(ext: string): string {
|
function guessPlatform(ext: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'.nes': 'nes',
|
'.nes': 'nes', '.sfc': 'snes', '.smc': 'snes',
|
||||||
'.sfc': 'snes',
|
'.n64': 'n64', '.z64': 'n64', '.v64': 'n64',
|
||||||
'.smc': 'snes',
|
'.gba': 'gba', '.gb': 'gb', '.gbc': 'gbc', '.nds': 'nds',
|
||||||
'.n64': 'n64',
|
'.iso': 'ps1', '.bin': 'ps1', '.cue': 'ps1',
|
||||||
'.z64': 'n64',
|
'.wbfs': 'wii', '.wad': 'wii',
|
||||||
'.v64': 'n64',
|
'.nsp': 'switch', '.xci': 'switch',
|
||||||
'.gba': 'gba',
|
'.pkg': 'ps3', '.chd': 'ps1',
|
||||||
'.gb': 'gb',
|
'.gcm': 'gc', '.gcz': 'gc', '.rvz': 'gc',
|
||||||
'.gbc': 'gbc',
|
'.ps2': 'ps2', '.cso': 'ps2',
|
||||||
'.nds': 'nds',
|
|
||||||
'.iso': 'ps1',
|
|
||||||
'.bin': 'ps1',
|
|
||||||
'.cue': 'ps1',
|
|
||||||
'.wbfs': 'wii',
|
|
||||||
'.wad': 'wii',
|
|
||||||
'.nsp': 'switch',
|
|
||||||
'.xci': 'switch',
|
|
||||||
'.pkg': 'ps3',
|
|
||||||
'.chd': 'ps1',
|
|
||||||
'.gcm': 'gc',
|
|
||||||
'.gcz': 'gc',
|
|
||||||
'.rvz': 'gc',
|
|
||||||
'.ps2': 'ps2',
|
|
||||||
'.cso': 'ps2',
|
|
||||||
};
|
};
|
||||||
return map[ext] || 'other';
|
return map[ext] || 'other';
|
||||||
}
|
}
|
||||||
|
|
||||||
function guessEmulator(ext: string): string {
|
function guessEmulator(ext: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'.nes': 'retroarch',
|
'.nes': 'retroarch', '.sfc': 'retroarch', '.smc': 'retroarch',
|
||||||
'.sfc': 'retroarch',
|
'.n64': 'retroarch', '.z64': 'retroarch', '.v64': 'retroarch',
|
||||||
'.smc': 'retroarch',
|
'.gba': 'retroarch', '.gb': 'retroarch', '.gbc': 'retroarch',
|
||||||
'.n64': 'retroarch',
|
'.wbfs': 'dolphin', '.wad': 'dolphin', '.gcm': 'dolphin',
|
||||||
'.z64': 'retroarch',
|
'.gcz': 'dolphin', '.rvz': 'dolphin',
|
||||||
'.v64': 'retroarch',
|
'.nsp': 'ryujinx', '.xci': 'ryujinx',
|
||||||
'.gba': 'retroarch',
|
|
||||||
'.gb': 'retroarch',
|
|
||||||
'.gbc': 'retroarch',
|
|
||||||
'.wbfs': 'dolphin',
|
|
||||||
'.wad': 'dolphin',
|
|
||||||
'.gcm': 'dolphin',
|
|
||||||
'.gcz': 'dolphin',
|
|
||||||
'.rvz': 'dolphin',
|
|
||||||
'.nsp': 'ryujinx',
|
|
||||||
'.xci': 'ryujinx',
|
|
||||||
'.pkg': 'rpcs3',
|
'.pkg': 'rpcs3',
|
||||||
'.ps2': 'pcsx2',
|
'.ps2': 'pcsx2', '.cso': 'pcsx2',
|
||||||
'.cso': 'pcsx2',
|
|
||||||
};
|
};
|
||||||
return map[ext] || 'retroarch';
|
return map[ext] || 'retroarch';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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 { 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';
|
||||||
|
import { getPlatform, getArch, isWindows, isMacOS, isLinux } from './platform';
|
||||||
|
|
||||||
|
type ProgressCallback = (progress: InstallProgress) => void;
|
||||||
|
|
||||||
|
function downloadDir(): string {
|
||||||
|
const d = join(app.getPath('userData'), 'downloads');
|
||||||
|
if (!existsSync(d)) mkdirSync(d, { recursive: true });
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tempName(suffix: string): string {
|
||||||
|
return join(downloadDir(), `${randomBytes(8).toString('hex')}${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 code = response.statusCode ?? 500;
|
||||||
|
if (code >= 300 && code < 400 && response.headers.location) {
|
||||||
|
downloadFile(response.headers.location, dest, onProgress).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code !== 200) {
|
||||||
|
reject(new Error(`HTTP ${code} downloading ${url}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = parseInt(response.headers['content-length'] ?? '0', 10);
|
||||||
|
let downloaded = 0;
|
||||||
|
const file = 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'tar.gz':
|
||||||
|
case 'tar.bz2': {
|
||||||
|
const decompress = format === 'tar.bz2' ? '-j' : '-z';
|
||||||
|
execSync(`tar -x${decompress}f "${archivePath}" -C "${destDir}"`, { stdio: 'pipe' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '7z': {
|
||||||
|
execSync(`7z x "${archivePath}" -o"${destDir}" -y`, { stdio: 'pipe' });
|
||||||
|
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' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'exe':
|
||||||
|
case 'msi':
|
||||||
|
case 'appimage':
|
||||||
|
case 'pkg': {
|
||||||
|
// These are direct 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}...`);
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'exe': {
|
||||||
|
if (isWindows()) {
|
||||||
|
execSync(`"${installerPath}" /S /D="${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||||
|
} else {
|
||||||
|
execSync(`chmod +x "${installerPath}" && "${installerPath}" -- "${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'msi': {
|
||||||
|
execSync(`msiexec /i "${installerPath}" /quiet /norestart INSTALLDIR="${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'appimage': {
|
||||||
|
execSync(`chmod +x "${installerPath}"`, { stdio: 'pipe' });
|
||||||
|
// Copy AppImage to install dir
|
||||||
|
execSync(`cp "${installerPath}" "${installDir}/${emulatorId}.AppImage"`, { stdio: 'pipe' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'pkg': {
|
||||||
|
execSync(`installer -pkg "${installerPath}" -target /`, { stdio: 'pipe', timeout: 300000 });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'dmg': {
|
||||||
|
// Already handled in extractArchive
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Cannot install format: ${format}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress('Installer finished');
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ProgressCallback };
|
||||||
|
|
||||||
|
export async function installEmulator(
|
||||||
|
emulatorId: string,
|
||||||
|
downloads: EmulatorDownload[],
|
||||||
|
platform: Platform,
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
const report = (stage: InstallProgress['stage'], percent: number, message: string) => {
|
||||||
|
onProgress({ emulatorId, stage, percent, message });
|
||||||
|
};
|
||||||
|
|
||||||
|
report('downloading', 0, `Downloading ${download.url}...`);
|
||||||
|
|
||||||
|
const downloadPath = tempName(`.${download.format}`);
|
||||||
|
await downloadFile(download.url, downloadPath, (pct) => {
|
||||||
|
report('downloading', pct, `Downloading ${emulatorId}... ${pct}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
if (archiveFormats.includes(download.format)) {
|
||||||
|
report('extracting', 0, `Extracting ${emulatorId}...`);
|
||||||
|
extractArchive(downloadPath, installDir, download.format, emulatorId, (msg) => {
|
||||||
|
report('extracting', 50, msg);
|
||||||
|
});
|
||||||
|
report('extracting', 100, 'Extraction complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installerFormats.includes(download.format)) {
|
||||||
|
report('installing', 0, `Installing ${emulatorId}...`);
|
||||||
|
runInstaller(downloadPath, download.format, emulatorId, installDir, (msg) => {
|
||||||
|
report('installing', 50, msg);
|
||||||
|
});
|
||||||
|
report('installing', 100, 'Installation complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
try { execSync(`rm -f "${downloadPath}"`); } catch { /* ignore */ }
|
||||||
|
|
||||||
|
report('done', 100, `${emulatorId} installed successfully`);
|
||||||
|
|
||||||
|
return installDir;
|
||||||
|
}
|
||||||
+65
-11
@@ -1,16 +1,19 @@
|
|||||||
import { ipcMain, shell, dialog } from 'electron';
|
import { ipcMain, shell, dialog, BrowserWindow } from 'electron';
|
||||||
import {
|
import {
|
||||||
getAllEmulatorStates,
|
getAllEmulatorStates,
|
||||||
checkEmulator,
|
checkEmulator,
|
||||||
launchGame,
|
launchGame,
|
||||||
scanRoms,
|
scanRoms,
|
||||||
knownEmulators,
|
knownEmulators,
|
||||||
|
findEmulator,
|
||||||
getRomsDirectory,
|
getRomsDirectory,
|
||||||
getEmulatorsDirectory,
|
getEmulatorsDirectory,
|
||||||
} from './emulators';
|
} from './emulators';
|
||||||
|
import { installEmulator } from './installer';
|
||||||
|
import { applyRecommendedConfig, getPresets, checkConfigured } from './configurator';
|
||||||
import { settings } from './settings';
|
import { settings } from './settings';
|
||||||
import { getSystemInfo, platformName } from './platform';
|
import { getSystemInfo, platformName, getPlatform, getArch } from './platform';
|
||||||
import { GameEntry, AppSettings } from '../shared/types';
|
import { InstallProgress, AppSettings } from '../shared/types';
|
||||||
|
|
||||||
export function registerIpcHandlers(): void {
|
export function registerIpcHandlers(): void {
|
||||||
// System
|
// System
|
||||||
@@ -21,15 +24,66 @@ export function registerIpcHandlers(): void {
|
|||||||
ipcMain.handle('emulators:list', () => knownEmulators);
|
ipcMain.handle('emulators:list', () => knownEmulators);
|
||||||
ipcMain.handle('emulators:states', () => getAllEmulatorStates());
|
ipcMain.handle('emulators:states', () => getAllEmulatorStates());
|
||||||
ipcMain.handle('emulators:check', (_event, id: string) => checkEmulator(id));
|
ipcMain.handle('emulators:check', (_event, id: string) => checkEmulator(id));
|
||||||
ipcMain.handle('emulators:install-url', (_event, id: string) => {
|
|
||||||
const emu = knownEmulators.find((e) => e.id === id);
|
// Install emulator - direct download + install
|
||||||
if (emu?.installUrl) {
|
ipcMain.handle(
|
||||||
const platform = getSystemInfo().platform;
|
'emulators:install',
|
||||||
const url = emu.installUrl[platform];
|
async (event, emulatorId: string) => {
|
||||||
if (url) shell.openExternal(url);
|
const config = findEmulator(emulatorId);
|
||||||
return url;
|
if (!config) throw new Error(`Unknown emulator: ${emulatorId}`);
|
||||||
|
if (!config.downloads) throw new Error(`No downloads configured for ${emulatorId}`);
|
||||||
|
|
||||||
|
const platform = getPlatform();
|
||||||
|
const arch = getArch();
|
||||||
|
const downloads = config.downloads[platform];
|
||||||
|
if (!downloads || downloads.length === 0)
|
||||||
|
throw new Error(`No downloads available for ${emulatorId} on ${platform}`);
|
||||||
|
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
const sendProgress = (p: InstallProgress) => {
|
||||||
|
win?.webContents.send('emulators:install-progress', p);
|
||||||
|
};
|
||||||
|
|
||||||
|
await installEmulator(emulatorId, downloads, platform, arch, sendProgress);
|
||||||
|
|
||||||
|
// Re-check state after install
|
||||||
|
return checkEmulator(emulatorId);
|
||||||
}
|
}
|
||||||
return null;
|
);
|
||||||
|
|
||||||
|
// Configure emulator with recommended settings
|
||||||
|
ipcMain.handle(
|
||||||
|
'emulators:configure',
|
||||||
|
async (_event, emulatorId: string, installPath: string) => {
|
||||||
|
const success = await applyRecommendedConfig(emulatorId, installPath);
|
||||||
|
return { success, state: checkEmulator(emulatorId) };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get available presets for an emulator
|
||||||
|
ipcMain.handle(
|
||||||
|
'emulators:presets',
|
||||||
|
async (_event, emulatorId: string) => {
|
||||||
|
return getPresets(emulatorId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if configured
|
||||||
|
ipcMain.handle(
|
||||||
|
'emulators:configured',
|
||||||
|
(_event, emulatorId: string, installPath?: string) => {
|
||||||
|
return checkConfigured(emulatorId, installPath);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open website (fallback for manual download)
|
||||||
|
ipcMain.handle('emulators:open-website', (_event, id: string) => {
|
||||||
|
const emu = findEmulator(id);
|
||||||
|
if (emu?.websiteUrl?.[getPlatform()]) {
|
||||||
|
shell.openExternal(emu.websiteUrl[getPlatform()]!);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ROMs / Games
|
// ROMs / Games
|
||||||
|
|||||||
+30
-3
@@ -1,10 +1,12 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron';
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
import {
|
import type {
|
||||||
EmulatorConfig,
|
EmulatorConfig,
|
||||||
EmulatorState,
|
EmulatorState,
|
||||||
GameEntry,
|
GameEntry,
|
||||||
SystemInfo,
|
SystemInfo,
|
||||||
AppSettings,
|
AppSettings,
|
||||||
|
InstallProgress,
|
||||||
|
ConfigPreset,
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
@@ -18,8 +20,33 @@ const api = {
|
|||||||
states: (): Promise<EmulatorState[]> => ipcRenderer.invoke('emulators:states'),
|
states: (): Promise<EmulatorState[]> => ipcRenderer.invoke('emulators:states'),
|
||||||
check: (id: string): Promise<EmulatorState> =>
|
check: (id: string): Promise<EmulatorState> =>
|
||||||
ipcRenderer.invoke('emulators:check', id),
|
ipcRenderer.invoke('emulators:check', id),
|
||||||
openInstallUrl: (id: string): Promise<string | null> =>
|
|
||||||
ipcRenderer.invoke('emulators:install-url', id),
|
/** Download + install emulator directly. Returns final EmulatorState */
|
||||||
|
install: (id: string): Promise<EmulatorState> =>
|
||||||
|
ipcRenderer.invoke('emulators:install', id),
|
||||||
|
|
||||||
|
/** Apply recommended config preset to an installed emulator */
|
||||||
|
configure: (id: string, installPath: string): Promise<{ success: boolean; state: EmulatorState }> =>
|
||||||
|
ipcRenderer.invoke('emulators:configure', id, installPath),
|
||||||
|
|
||||||
|
/** Get available config presets for an emulator */
|
||||||
|
presets: (id: string): Promise<ConfigPreset[]> =>
|
||||||
|
ipcRenderer.invoke('emulators:presets', id),
|
||||||
|
|
||||||
|
/** Check if an emulator has been configured */
|
||||||
|
configured: (id: string, installPath?: string): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('emulators:configured', id, installPath),
|
||||||
|
|
||||||
|
/** Open the emulator's website in browser (fallback) */
|
||||||
|
openWebsite: (id: string): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('emulators:open-website', id),
|
||||||
|
|
||||||
|
/** Listen for install progress updates */
|
||||||
|
onInstallProgress: (cb: (progress: InstallProgress) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, p: InstallProgress) => cb(p);
|
||||||
|
ipcRenderer.on('emulators:install-progress', handler);
|
||||||
|
return () => ipcRenderer.removeListener('emulators:install-progress', handler);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
roms: {
|
roms: {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { AppSettings } from '../shared/types';
|
import { AppSettings } from '../shared/types';
|
||||||
import { getPlatform } from './platform';
|
|
||||||
|
|
||||||
const SETTINGS_FILE = 'settings.json';
|
const SETTINGS_FILE = 'settings.json';
|
||||||
|
|
||||||
@@ -17,6 +16,7 @@ const defaultSettings: AppSettings = {
|
|||||||
minimiseToTray: true,
|
minimiseToTray: true,
|
||||||
launchInFullscreen: false,
|
launchInFullscreen: false,
|
||||||
closeToTray: true,
|
closeToTray: true,
|
||||||
|
presetSourceUrl: 'https://raw.githubusercontent.com/mileswolfallen2/omniemu-presets/main/presets.json',
|
||||||
};
|
};
|
||||||
|
|
||||||
let cached: AppSettings | null = null;
|
let cached: AppSettings | null = null;
|
||||||
|
|||||||
Vendored
+57
@@ -0,0 +1,57 @@
|
|||||||
|
export type Platform = 'win32' | 'darwin' | 'linux';
|
||||||
|
export type Arch = 'x64' | 'arm64';
|
||||||
|
export interface EmulatorConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
platforms: string[];
|
||||||
|
defaultPath: Record<Platform, string>;
|
||||||
|
installUrl: Record<Platform, string | null>;
|
||||||
|
installVia: 'download' | 'brew' | 'apt' | 'winget' | 'manual';
|
||||||
|
supported: boolean;
|
||||||
|
}
|
||||||
|
export interface RomFile {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
format: string;
|
||||||
|
platform: string;
|
||||||
|
lastPlayed?: string;
|
||||||
|
playCount: number;
|
||||||
|
}
|
||||||
|
export interface GameEntry {
|
||||||
|
id: string;
|
||||||
|
romPath: string;
|
||||||
|
title: string;
|
||||||
|
platform: string;
|
||||||
|
emulatorId: string;
|
||||||
|
coverUrl?: string;
|
||||||
|
lastPlayed?: string;
|
||||||
|
playCount: number;
|
||||||
|
addedAt: string;
|
||||||
|
}
|
||||||
|
export interface ScanResult {
|
||||||
|
emulatorId: string;
|
||||||
|
games: GameEntry[];
|
||||||
|
}
|
||||||
|
export interface AppSettings {
|
||||||
|
romsDirectory: string;
|
||||||
|
emulatorsDirectory: string;
|
||||||
|
theme: 'light' | 'dark' | 'system';
|
||||||
|
minimiseToTray: boolean;
|
||||||
|
launchInFullscreen: boolean;
|
||||||
|
closeToTray: boolean;
|
||||||
|
}
|
||||||
|
export interface SystemInfo {
|
||||||
|
platform: Platform;
|
||||||
|
arch: Arch;
|
||||||
|
homeDir: string;
|
||||||
|
appDataDir: string;
|
||||||
|
}
|
||||||
|
export interface EmulatorState {
|
||||||
|
installed: boolean;
|
||||||
|
version?: string;
|
||||||
|
path?: string;
|
||||||
|
config: EmulatorConfig;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=types.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AACpD,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,OAAO,CAAC;AAEnC,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,UAAU,EAAE,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC9D,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACnC,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;CACxB"}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
//# sourceMappingURL=types.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}
|
||||||
+45
-2
@@ -1,15 +1,55 @@
|
|||||||
export type Platform = 'win32' | 'darwin' | 'linux';
|
export type Platform = 'win32' | 'darwin' | 'linux';
|
||||||
export type Arch = 'x64' | 'arm64';
|
export type Arch = 'x64' | 'arm64';
|
||||||
|
|
||||||
|
export interface EmulatorDownload {
|
||||||
|
url: string;
|
||||||
|
/** Archive type: 'exe' = direct installer, 'msi' = windows msi,
|
||||||
|
* 'dmg' = macOS disk image, 'appimage' = linux appimage,
|
||||||
|
* 'tar.gz' | 'tar.bz2' | 'zip' = archive to extract,
|
||||||
|
* 'pkg' = macOS installer, '7z' = 7zip archive */
|
||||||
|
format: string;
|
||||||
|
/** Path inside archive to the executable (if archive format) */
|
||||||
|
executablePath?: string;
|
||||||
|
/** Expected file size in bytes (for validation) */
|
||||||
|
size?: number;
|
||||||
|
/** Only for this arch (omitted = all arches) */
|
||||||
|
arch?: Arch;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EmulatorConfig {
|
export interface EmulatorConfig {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
platforms: string[];
|
platforms: string[];
|
||||||
defaultPath: Record<Platform, string>;
|
defaultPath: Record<Platform, string>;
|
||||||
installUrl: Record<Platform, string | null>;
|
/** Direct download sources per platform */
|
||||||
installVia: 'download' | 'brew' | 'apt' | 'winget' | 'manual';
|
downloads: Partial<Record<Platform, EmulatorDownload[]>>;
|
||||||
|
/** Package manager names for auto-install via system pkg manager */
|
||||||
|
packageNames?: Partial<Record<Platform, string>>;
|
||||||
supported: boolean;
|
supported: boolean;
|
||||||
|
/** URL to fetch recommended config presets from */
|
||||||
|
presetUrl?: string;
|
||||||
|
/** Website URL for manual/fallback */
|
||||||
|
websiteUrl?: Record<Platform, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstallProgress {
|
||||||
|
emulatorId: string;
|
||||||
|
stage: 'downloading' | 'extracting' | 'installing' | 'configuring' | 'done' | 'error';
|
||||||
|
percent: number;
|
||||||
|
message: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigPreset {
|
||||||
|
/** Name of the preset (e.g. "Performance", "Quality", "Recommended") */
|
||||||
|
name: string;
|
||||||
|
/** Description of what this preset does */
|
||||||
|
description: string;
|
||||||
|
/** Platform-specific config files to write: relative path -> file content */
|
||||||
|
files: Record<string, string>;
|
||||||
|
/** Registry/settings changes for Windows */
|
||||||
|
registry?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RomFile {
|
export interface RomFile {
|
||||||
@@ -46,6 +86,8 @@ export interface AppSettings {
|
|||||||
minimiseToTray: boolean;
|
minimiseToTray: boolean;
|
||||||
launchInFullscreen: boolean;
|
launchInFullscreen: boolean;
|
||||||
closeToTray: boolean;
|
closeToTray: boolean;
|
||||||
|
/** URL to fetch recommended config presets from */
|
||||||
|
presetSourceUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemInfo {
|
export interface SystemInfo {
|
||||||
@@ -60,4 +102,5 @@ export interface EmulatorState {
|
|||||||
version?: string;
|
version?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
config: EmulatorConfig;
|
config: EmulatorConfig;
|
||||||
|
configured: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"outDir": "dist/main",
|
"moduleResolution": "node10",
|
||||||
"rootDir": "src/main",
|
"outDir": "dist",
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/main/**/*", "src/shared/**/*"]
|
"include": ["src/main/**/*", "src/shared/**/*"]
|
||||||
|
|||||||
Reference in New Issue
Block a user