Migrate app from Tauri to Electron

Replace the Tauri/sidecar architecture with an Electron main process. Adds an electron/ directory (main, preload, handlers, core integrations for @xmcl/*), electron-builder.yml, TypeScript electron config and declarations, and updated IPC utilities (ipc.ts) so frontend uses ipcRenderer/ipcMain. Removes Tauri sidecar and src-tauri artifacts, deletes sidecar sources and keys, updates .gitignore, VSCode recommendations, package scripts and icons, and updates documentation (README, AGENTS, DEV) and frontend stores/apis to reflect the Electron-based architecture and config/auth persistence changes.
This commit is contained in:
2026-06-28 03:37:07 +08:00
parent 5b2e2860de
commit 9cb6a6e571
152 changed files with 8449 additions and 9562 deletions
+44
View File
@@ -0,0 +1,44 @@
import * as fs from 'fs';
import * as path from 'path';
import electron from 'electron';
const { app } = electron;
export interface AuthData {
username: string;
uuid: string;
accessToken: string;
refreshToken: string;
xboxProfile: string;
}
const authFile = (): string => {
if (app.isPackaged) {
return path.join(path.dirname(app.getPath('exe')), 'koring-auth.json');
}
return path.join(__dirname, '..', 'koring-auth.json');
};
export function readAuth(): AuthData {
const filePath = authFile();
if (!fs.existsSync(filePath)) {
return { username: '', uuid: '', accessToken: '', refreshToken: '', xboxProfile: '' };
}
try {
const raw = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(raw) as AuthData;
} catch {
return { username: '', uuid: '', accessToken: '', refreshToken: '', xboxProfile: '' };
}
}
export function writeAuth(auth: AuthData): void {
const filePath = authFile();
fs.writeFileSync(filePath, JSON.stringify(auth, null, 2), 'utf-8');
}
export function deleteAuth(): void {
const filePath = authFile();
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
+161
View File
@@ -0,0 +1,161 @@
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'js-yaml';
import electron from 'electron';
const { app } = electron;
const CONFIG_FILE = 'Koring.yml';
const CURRENT_VERSION = 1;
function configPath(): string {
if (app.isPackaged) {
return path.join(path.dirname(app.getPath('exe')), CONFIG_FILE);
}
return path.join(__dirname, '..', CONFIG_FILE);
}
export interface ThemeConfig {
darkMode: string;
parallax: boolean;
}
export interface A11yConfig {
reduceMotion: boolean;
reduceTransparency: boolean;
highContrast: boolean;
contentBlurOpacity: number;
}
export interface BackgroundConfig {
bgType: string;
image: string;
blur: number;
opacity: number;
}
export interface GameConfig {
gameDir: string;
resourceDir: string;
savesDir: string;
instancesDir: string;
}
export interface JavaConfig {
javaPath: string;
memMode: string;
memGB: number;
gc: string;
jvmArgs: string;
}
export interface AdvancedConfig {
afterLaunch: string;
winMode: string;
customWidth: number;
customHeight: number;
gameArgs: string;
preLaunchCmd: string;
debugMode: boolean;
}
export interface DownloadConfig {
fileSource: string;
versionSource: string;
threads: number;
speedLimit: number;
}
export interface SecurityIdConfig {
enabled: boolean;
authUrl: string;
}
export interface NetworkConfig {
securityId: SecurityIdConfig;
}
export interface AppConfig {
version: number;
theme: ThemeConfig;
a11y: A11yConfig;
background: BackgroundConfig;
game: GameConfig;
java: JavaConfig;
advanced: AdvancedConfig;
download: DownloadConfig;
network: NetworkConfig;
}
const DEFAULTS: AppConfig = {
version: CURRENT_VERSION,
theme: { darkMode: 'auto', parallax: true },
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
background: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 },
game: { gameDir: '.minecraft', resourceDir: '', savesDir: '', instancesDir: '.minecraft/instances' },
java: { javaPath: '', memMode: 'auto', memGB: 4, gc: 'auto', jvmArgs: '' },
advanced: { afterLaunch: 'close', winMode: 'default', customWidth: 854, customHeight: 480, gameArgs: '', preLaunchCmd: '', debugMode: false },
download: { fileSource: 'mirror', versionSource: 'mirror', threads: 16, speedLimit: 0 },
network: { securityId: { enabled: false, authUrl: '' } },
};
function migrate(config: AppConfig): AppConfig {
if (config.version < 1) {
config.version = 1;
}
return config;
}
function diffValue(full: unknown, defaultVal: unknown): unknown {
if (full === null || full === undefined) return undefined;
if (typeof full !== 'object' || typeof defaultVal !== 'object') {
return full === defaultVal ? undefined : full;
}
if (Array.isArray(full) && Array.isArray(defaultVal)) {
return JSON.stringify(full) === JSON.stringify(defaultVal) ? undefined : full;
}
if (Array.isArray(full) !== Array.isArray(defaultVal)) return full;
const fullObj = full as Record<string, unknown>;
const defaultObj = defaultVal as Record<string, unknown>;
const result: Record<string, unknown> = {};
for (const key of Object.keys(fullObj)) {
const d = diffValue(fullObj[key], defaultObj[key]);
if (d !== undefined) {
result[key] = d;
}
}
return Object.keys(result).length === 0 ? undefined : result;
}
export function loadConfig(): AppConfig {
const filePath = configPath();
if (!fs.existsSync(filePath)) {
return { ...DEFAULTS };
}
try {
const raw = fs.readFileSync(filePath, 'utf-8');
if (!raw.trim()) return { ...DEFAULTS };
const parsed = yaml.load(raw) as Partial<AppConfig>;
const config = migrate({ ...DEFAULTS, ...parsed } as AppConfig);
config.version = CURRENT_VERSION;
return config;
} catch {
return { ...DEFAULTS };
}
}
export function saveConfig(config: AppConfig): void {
const filePath = configPath();
const sparse = diffValue(config, DEFAULTS) as Record<string, unknown> | undefined;
if (!sparse || Object.keys(sparse).length === 0) {
try { fs.unlinkSync(filePath); } catch {}
return;
}
const yamlStr = yaml.dump(sparse, { lineWidth: -1 });
fs.writeFileSync(filePath, yamlStr, 'utf-8');
}
+180
View File
@@ -0,0 +1,180 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.microsoftLoginStart = microsoftLoginStart;
exports.microsoftLoginCallback = microsoftLoginCallback;
exports.offlineLogin = offlineLogin;
exports.validateMinecraftToken = validateMinecraftToken;
const crypto = __importStar(require("crypto"));
const electron_1 = __importDefault(require("electron"));
const { net } = electron_1.default;
const MC_SERVICES_API = 'https://api.minecraftservices.com';
const XBOX_AUTH_URL = 'https://user.auth.xboxlive.com/user/authenticate';
const XBOX_XSTS_URL = 'https://xsts.auth.xboxlive.com/xsts/authorize';
const MC_AUTH_URL = 'https://api.minecraftservices.com/authentication/login_with_xbox';
const MC_PROFILE_URL = 'https://api.minecraftservices.com/minecraft/profile';
function generateState() {
return crypto.randomBytes(16).toString('hex');
}
function generateVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
function generateChallenge(verifier) {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
async function microsoftLoginStart(clientId, redirectUri) {
const state = generateState();
const verifier = generateVerifier();
const challenge = generateChallenge(verifier);
const params = new URLSearchParams({
client_id: clientId,
response_type: 'code',
redirect_uri: redirectUri || 'http://localhost:3000/callback',
scope: 'XboxLive.signin offline_access',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
const authUrl = `https://login.live.com/oauth20_authorize.srf?${params.toString()}`;
return { state, authUrl, verifier };
}
async function exchangeCodeForToken(code, clientId, redirectUri, verifier) {
const response = await net.fetch('https://login.live.com/oauth20_token.srf', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: clientId,
code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
code_verifier: verifier,
}).toString(),
});
if (!response.ok)
throw new Error(`MS token exchange failed: ${response.status}`);
return response.json();
}
async function authenticateWithXbox(msToken) {
const response = await net.fetch(XBOX_AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Properties: {
AuthMethod: 'RPS',
SiteName: 'user.auth.xboxlive.com',
RpsTicket: msToken,
},
RelyingParty: 'http://auth.xboxlive.com',
TokenType: 'JWT',
}),
});
if (!response.ok)
throw new Error(`Xbox auth failed: ${response.status}`);
return response.json();
}
async function authorizeWithXsts(xboxToken) {
const response = await net.fetch(XBOX_XSTS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Properties: {
SandboxId: 'RETAIL',
UserTokens: [xboxToken],
},
RelyingParty: 'rp://api.minecraftservices.com/',
TokenType: 'JWT',
}),
});
if (!response.ok)
throw new Error(`Xbox XSTS failed: ${response.status}`);
return response.json();
}
async function authenticateWithMinecraft(xstsToken) {
const response = await net.fetch(MC_AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identityToken: `XBL3.0 x=${xstsToken}`,
}),
});
if (!response.ok)
throw new Error(`MC auth failed: ${response.status}`);
return response.json();
}
async function getMinecraftProfile(accessToken) {
const response = await net.fetch(MC_PROFILE_URL, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok)
throw new Error(`MC profile failed: ${response.status}`);
return response.json();
}
async function microsoftLoginCallback(code, clientId, redirectUri) {
const redirect = redirectUri || 'http://localhost:3000/callback';
const msToken = await exchangeCodeForToken(code, clientId, redirect, '');
const xboxAuth = await authenticateWithXbox(msToken.access_token);
const xstsAuth = await authorizeWithXsts(xboxAuth.Token);
const mcAuth = await authenticateWithMinecraft(xstsAuth.Token);
const profile = await getMinecraftProfile(mcAuth.access_token);
return {
username: profile.name,
uuid: profile.id,
accessToken: mcAuth.access_token,
expiresAt: Date.now() + mcAuth.expires_in * 1000,
};
}
async function offlineLogin(username) {
const uuid = crypto.createHash('md5').update(`OfflinePlayer:${username}`).digest('hex');
const formattedUuid = `${uuid.slice(0, 8)}-${uuid.slice(8, 12)}-${uuid.slice(12, 16)}-${uuid.slice(16, 20)}-${uuid.slice(20)}`;
return {
username,
uuid: formattedUuid,
accessToken: '',
};
}
async function validateMinecraftToken(accessToken) {
try {
const response = await net.fetch(`${MC_SERVICES_API}/minecraft/profile`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return response.ok;
}
catch {
return false;
}
}
+175
View File
@@ -0,0 +1,175 @@
import * as crypto from 'crypto';
import electron from 'electron';
const { net } = electron;
const MC_SERVICES_API = 'https://api.minecraftservices.com';
const XBOX_AUTH_URL = 'https://user.auth.xboxlive.com/user/authenticate';
const XBOX_XSTS_URL = 'https://xsts.auth.xboxlive.com/xsts/authorize';
const MC_AUTH_URL = 'https://api.minecraftservices.com/authentication/login_with_xbox';
const MC_PROFILE_URL = 'https://api.minecraftservices.com/minecraft/profile';
interface XboxProfile {
gamertag: string;
gamerscore: string;
displayPicRaw: string;
}
interface AuthResult {
username: string;
uuid: string;
accessToken: string;
expiresAt?: number;
xboxProfile?: XboxProfile;
}
function generateState(): string {
return crypto.randomBytes(16).toString('hex');
}
function generateVerifier(): string {
return crypto.randomBytes(32).toString('base64url');
}
function generateChallenge(verifier: string): string {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
export async function microsoftLoginStart(clientId: string, redirectUri?: string) {
const state = generateState();
const verifier = generateVerifier();
const challenge = generateChallenge(verifier);
const params = new URLSearchParams({
client_id: clientId,
response_type: 'code',
redirect_uri: redirectUri || 'http://localhost:3000/callback',
scope: 'XboxLive.signin offline_access',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
const authUrl = `https://login.live.com/oauth20_authorize.srf?${params.toString()}`;
return { state, authUrl, verifier };
}
async function exchangeCodeForToken(code: string, clientId: string, redirectUri: string, verifier: string) {
const response = await net.fetch('https://login.live.com/oauth20_token.srf', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: clientId,
code,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
code_verifier: verifier,
}).toString(),
});
if (!response.ok) throw new Error(`MS token exchange failed: ${response.status}`);
return response.json() as Promise<{ access_token: string; refresh_token: string }>;
}
async function authenticateWithXbox(msToken: string) {
const response = await net.fetch(XBOX_AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Properties: {
AuthMethod: 'RPS',
SiteName: 'user.auth.xboxlive.com',
RpsTicket: msToken,
},
RelyingParty: 'http://auth.xboxlive.com',
TokenType: 'JWT',
}),
});
if (!response.ok) throw new Error(`Xbox auth failed: ${response.status}`);
return response.json() as Promise<{ IssueInstant: string; Token: string; NotAfter: string }>;
}
async function authorizeWithXsts(xboxToken: string) {
const response = await net.fetch(XBOX_XSTS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Properties: {
SandboxId: 'RETAIL',
UserTokens: [xboxToken],
},
RelyingParty: 'rp://api.minecraftservices.com/',
TokenType: 'JWT',
}),
});
if (!response.ok) throw new Error(`Xbox XSTS failed: ${response.status}`);
return response.json() as Promise<{ IssueInstant: string; Token: string; NotAfter: string }>;
}
async function authenticateWithMinecraft(xstsToken: string) {
const response = await net.fetch(MC_AUTH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identityToken: `XBL3.0 x=${xstsToken}`,
}),
});
if (!response.ok) throw new Error(`MC auth failed: ${response.status}`);
return response.json() as Promise<{ username: string; access_token: string; token_type: string; expires_in: number }>;
}
async function getMinecraftProfile(accessToken: string): Promise<{ id: string; name: string; skins: unknown[] }> {
const response = await net.fetch(MC_PROFILE_URL, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) throw new Error(`MC profile failed: ${response.status}`);
return response.json() as Promise<{ id: string; name: string; skins: unknown[] }>;
}
export async function microsoftLoginCallback(
code: string,
clientId: string,
redirectUri?: string
): Promise<AuthResult> {
const redirect = redirectUri || 'http://localhost:3000/callback';
const msToken = await exchangeCodeForToken(code, clientId, redirect, '');
const xboxAuth = await authenticateWithXbox(msToken.access_token);
const xstsAuth = await authorizeWithXsts(xboxAuth.Token);
const mcAuth = await authenticateWithMinecraft(xstsAuth.Token);
const profile = await getMinecraftProfile(mcAuth.access_token);
return {
username: profile.name,
uuid: profile.id,
accessToken: mcAuth.access_token,
expiresAt: Date.now() + mcAuth.expires_in * 1000,
};
}
export async function offlineLogin(username: string): Promise<AuthResult> {
const uuid = crypto.createHash('md5').update(`OfflinePlayer:${username}`).digest('hex');
const formattedUuid = `${uuid.slice(0, 8)}-${uuid.slice(8, 12)}-${uuid.slice(12, 16)}-${uuid.slice(16, 20)}-${uuid.slice(20)}`;
return {
username,
uuid: formattedUuid,
accessToken: '',
};
}
export async function validateMinecraftToken(accessToken: string): Promise<boolean> {
try {
const response = await net.fetch(`${MC_SERVICES_API}/minecraft/profile`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return response.ok;
} catch {
return false;
}
}
+181
View File
@@ -0,0 +1,181 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getVersionList = getVersionList;
exports.getForgeVersions = getForgeVersions;
exports.getFabricVersions = getFabricVersions;
exports.installMinecraft = installMinecraft;
exports.installModLoader = installModLoader;
const electron_1 = __importDefault(require("electron"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const { net } = electron_1.default;
const VERSION_MANIFEST_URL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json';
const FORGE_VERSION_LIST_URL = 'https://files.minecraftforge.net/net/minecraftforge/forge/json';
const FABRIC_VERSION_LIST_URL = 'https://meta.fabricmc.net/v2/versions';
async function downloadFile(url, dest, onProgress) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
const request = client.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
downloadFile(response.headers.location, dest, onProgress).then(resolve).catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode}`));
return;
}
const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
let downloadedBytes = 0;
const dir = path.dirname(dest);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const file = fs.createWriteStream(dest);
response.on('data', (chunk) => {
downloadedBytes += chunk.length;
if (onProgress && totalBytes > 0) {
onProgress({
stage: 'downloading',
current: downloadedBytes,
total: totalBytes,
message: `${Math.round((downloadedBytes / totalBytes) * 100)}%`,
});
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
file.on('error', (err) => {
fs.unlink(dest, () => { });
reject(err);
});
});
request.on('error', reject);
});
}
async function getVersionList(type) {
const response = await net.fetch(VERSION_MANIFEST_URL);
if (!response.ok)
throw new Error(`Failed to fetch version manifest: ${response.status}`);
const manifest = await response.json();
if (type && type !== 'all') {
manifest.versions = manifest.versions.filter((v) => v.type === type);
}
return manifest;
}
async function getForgeVersions(mcVersion) {
try {
const response = await net.fetch(FORGE_VERSION_LIST_URL);
if (!response.ok)
throw new Error(`Forge version list failed: ${response.status}`);
const data = await response.json();
const versions = data.versions[mcVersion || ''] || [];
return { versions };
}
catch {
return { versions: [] };
}
}
async function getFabricVersions(mcVersion) {
try {
const url = mcVersion
? `${FABRIC_VERSION_LIST_URL}/loader/${mcVersion}`
: `${FABRIC_VERSION_LIST_URL}/loader`;
const response = await net.fetch(url);
if (!response.ok)
throw new Error(`Fabric version list failed: ${response.status}`);
const data = await response.json();
return { versions: data.map((v) => v.version) };
}
catch {
return { versions: [] };
}
}
async function installMinecraft(version, gamePath, javaPath, downloadThreads, callbacks) {
const versionDir = path.join(gamePath, 'versions', version);
if (!fs.existsSync(versionDir)) {
fs.mkdirSync(versionDir, { recursive: true });
}
// Download version manifest
const manifestResponse = await net.fetch(VERSION_MANIFEST_URL);
const manifest = await manifestResponse.json();
const versionInfo = manifest.versions.find((v) => v.id === version);
if (!versionInfo)
throw new Error(`Version ${version} not found`);
callbacks?.onProgress?.({ stage: 'downloading', current: 0, total: 100, message: 'Downloading version manifest...' });
// Download version JSON
const versionJsonPath = path.join(versionDir, `${version}.json`);
await downloadFile(versionInfo.url, versionJsonPath, callbacks?.onProgress);
const versionJson = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8'));
// Download client jar
const clientJar = versionJson.downloads?.client;
if (clientJar) {
callbacks?.onProgress?.({ stage: 'downloading', current: 1, total: 3, message: 'Downloading client jar...' });
const jarPath = path.join(versionDir, `${version}.jar`);
await downloadFile(clientJar.url, jarPath, callbacks?.onProgress);
}
callbacks?.onProgress?.({ stage: 'downloading', current: 2, total: 3, message: 'Installing libraries...' });
// Download libraries
const libraries = versionJson.libraries || [];
for (const lib of libraries) {
if (lib.downloads?.artifact?.url) {
const libPath = path.join(gamePath, 'libraries', lib.downloads.artifact.path);
if (!fs.existsSync(libPath)) {
await downloadFile(lib.downloads.artifact.url, libPath);
}
}
}
callbacks?.onProgress?.({ stage: 'downloading', current: 3, total: 3, message: 'Installation complete' });
return { version, gamePath };
}
async function installModLoader(mcVersion, gamePath, loaderType, loaderVersion, javaPath, callbacks) {
callbacks?.onProgress?.({ stage: 'installing', current: 0, total: 100, message: `Installing ${loaderType}...` });
// Placeholder: actual implementation depends on loader type
// This would call the appropriate installer for Forge/Fabric/Quilt/NeoForge
callbacks?.onProgress?.({ stage: 'installing', current: 100, total: 100, message: `${loaderType} installed` });
return {
loaderType,
mcVersion,
loaderVersion: loaderVersion || 'latest',
};
}
+195
View File
@@ -0,0 +1,195 @@
import electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as http from 'http';
const { net } = electron;
const VERSION_MANIFEST_URL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json';
const FORGE_VERSION_LIST_URL = 'https://files.minecraftforge.net/net/minecraftforge/forge/json';
const FABRIC_VERSION_LIST_URL = 'https://meta.fabricmc.net/v2/versions';
interface VersionInfo {
id: string;
type: string;
url: string;
}
interface VersionManifest {
latest: { release: string; snapshot: string };
versions: VersionInfo[];
}
interface DownloadProgress {
stage: string;
current: number;
total: number;
message?: string;
}
async function downloadFile(url: string, dest: string, onProgress?: (progress: DownloadProgress) => void): Promise<void> {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
const request = client.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
downloadFile(response.headers.location!, dest, onProgress).then(resolve).catch(reject);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode}`));
return;
}
const totalBytes = parseInt(response.headers['content-length'] || '0', 10);
let downloadedBytes = 0;
const dir = path.dirname(dest);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const file = fs.createWriteStream(dest);
response.on('data', (chunk) => {
downloadedBytes += chunk.length;
if (onProgress && totalBytes > 0) {
onProgress({
stage: 'downloading',
current: downloadedBytes,
total: totalBytes,
message: `${Math.round((downloadedBytes / totalBytes) * 100)}%`,
});
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
file.on('error', (err) => {
fs.unlink(dest, () => {});
reject(err);
});
});
request.on('error', reject);
});
}
export async function getVersionList(type?: string): Promise<VersionManifest> {
const response = await net.fetch(VERSION_MANIFEST_URL);
if (!response.ok) throw new Error(`Failed to fetch version manifest: ${response.status}`);
const manifest = await response.json() as VersionManifest;
if (type && type !== 'all') {
manifest.versions = manifest.versions.filter((v) => v.type === type);
}
return manifest;
}
export async function getForgeVersions(mcVersion?: string) {
try {
const response = await net.fetch(FORGE_VERSION_LIST_URL);
if (!response.ok) throw new Error(`Forge version list failed: ${response.status}`);
const data = await response.json() as { versions: Record<string, string[]> };
const versions = data.versions[mcVersion || ''] || [];
return { versions };
} catch {
return { versions: [] };
}
}
export async function getFabricVersions(mcVersion?: string) {
try {
const url = mcVersion
? `${FABRIC_VERSION_LIST_URL}/loader/${mcVersion}`
: `${FABRIC_VERSION_LIST_URL}/loader`;
const response = await net.fetch(url);
if (!response.ok) throw new Error(`Fabric version list failed: ${response.status}`);
const data = await response.json() as { version: string; stable: boolean }[];
return { versions: data.map((v) => v.version) };
} catch {
return { versions: [] };
}
}
export async function installMinecraft(
version: string,
gamePath: string,
javaPath?: string,
downloadThreads?: number,
callbacks?: { onProgress?: (progress: DownloadProgress) => void }
): Promise<{ version: string; gamePath: string }> {
const versionDir = path.join(gamePath, 'versions', version);
if (!fs.existsSync(versionDir)) {
fs.mkdirSync(versionDir, { recursive: true });
}
// Download version manifest
const manifestResponse = await net.fetch(VERSION_MANIFEST_URL);
const manifest = await manifestResponse.json() as VersionManifest;
const versionInfo = manifest.versions.find((v) => v.id === version);
if (!versionInfo) throw new Error(`Version ${version} not found`);
callbacks?.onProgress?.({ stage: 'downloading', current: 0, total: 100, message: 'Downloading version manifest...' });
// Download version JSON
const versionJsonPath = path.join(versionDir, `${version}.json`);
await downloadFile(versionInfo.url, versionJsonPath, callbacks?.onProgress);
const versionJson = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8'));
// Download client jar
const clientJar = versionJson.downloads?.client;
if (clientJar) {
callbacks?.onProgress?.({ stage: 'downloading', current: 1, total: 3, message: 'Downloading client jar...' });
const jarPath = path.join(versionDir, `${version}.jar`);
await downloadFile(clientJar.url, jarPath, callbacks?.onProgress);
}
callbacks?.onProgress?.({ stage: 'downloading', current: 2, total: 3, message: 'Installing libraries...' });
// Download libraries
const libraries = versionJson.libraries || [];
for (const lib of libraries) {
if (lib.downloads?.artifact?.url) {
const libPath = path.join(gamePath, 'libraries', lib.downloads.artifact.path);
if (!fs.existsSync(libPath)) {
await downloadFile(lib.downloads.artifact.url, libPath);
}
}
}
callbacks?.onProgress?.({ stage: 'downloading', current: 3, total: 3, message: 'Installation complete' });
return { version, gamePath };
}
export async function installModLoader(
mcVersion: string,
gamePath: string,
loaderType: 'forge' | 'fabric' | 'quilt' | 'neoforge',
loaderVersion?: string,
javaPath?: string,
callbacks?: { onProgress?: (progress: DownloadProgress) => void }
): Promise<{ loaderType: string; mcVersion: string; loaderVersion: string }> {
callbacks?.onProgress?.({ stage: 'installing', current: 0, total: 100, message: `Installing ${loaderType}...` });
// Placeholder: actual implementation depends on loader type
// This would call the appropriate installer for Forge/Fabric/Quilt/NeoForge
callbacks?.onProgress?.({ stage: 'installing', current: 100, total: 100, message: `${loaderType} installed` });
return {
loaderType,
mcVersion,
loaderVersion: loaderVersion || 'latest',
};
}
+138
View File
@@ -0,0 +1,138 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInstance = createInstance;
exports.listInstances = listInstances;
exports.deleteInstance = deleteInstance;
exports.getInstanceInfo = getInstanceInfo;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const INSTANCE_CONFIG_FILE = 'koring-instance.json';
function getInstanceConfigPath(instancePath) {
return path.join(instancePath, INSTANCE_CONFIG_FILE);
}
async function createInstance(name, gamePath, mcVersion, loaderType, loaderVersion, javaPath, memory) {
const instancePath = path.join(gamePath, 'instances', name);
if (!fs.existsSync(instancePath)) {
fs.mkdirSync(instancePath, { recursive: true });
}
const config = {
name,
mcVersion,
loaderType,
loaderVersion,
javaPath,
memory,
createdAt: new Date().toISOString(),
};
const configPath = getInstanceConfigPath(instancePath);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
// Create mods directory
const modsDir = path.join(instancePath, 'mods');
if (!fs.existsSync(modsDir)) {
fs.mkdirSync(modsDir, { recursive: true });
}
// Count mods
const mods = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar'));
return {
name,
path: instancePath,
config,
modCount: mods.length,
};
}
async function listInstances(instancesPath) {
const instances = [];
if (!fs.existsSync(instancesPath)) {
return instances;
}
const entries = fs.readdirSync(instancesPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory())
continue;
const instancePath = path.join(instancesPath, entry.name);
const configPath = getInstanceConfigPath(instancePath);
if (!fs.existsSync(configPath))
continue;
try {
const configRaw = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configRaw);
// Count mods
const modsDir = path.join(instancePath, 'mods');
let modCount = 0;
if (fs.existsSync(modsDir)) {
modCount = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar')).length;
}
instances.push({
name: entry.name,
path: instancePath,
config,
modCount,
});
}
catch {
// Skip invalid config
}
}
return instances;
}
async function deleteInstance(name, instancesPath) {
const instancePath = path.join(instancesPath, name);
if (fs.existsSync(instancePath)) {
fs.rmSync(instancePath, { recursive: true, force: true });
}
return { deleted: name };
}
async function getInstanceInfo(name, instancesPath) {
const instancePath = path.join(instancesPath, name);
const configPath = getInstanceConfigPath(instancePath);
if (!fs.existsSync(configPath)) {
throw new Error(`Instance not found: ${name}`);
}
const configRaw = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configRaw);
// Count mods
const modsDir = path.join(instancePath, 'mods');
let modCount = 0;
if (fs.existsSync(modsDir)) {
modCount = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar')).length;
}
return {
name,
path: instancePath,
config,
modCount,
};
}
+154
View File
@@ -0,0 +1,154 @@
import * as fs from 'fs';
import * as path from 'path';
interface InstanceConfig {
name: string;
mcVersion: string;
loaderType?: string;
loaderVersion?: string;
javaPath?: string;
memory?: { min?: string; max?: string };
createdAt: string;
}
interface InstanceInfo {
name: string;
path: string;
config: InstanceConfig;
modCount?: number;
}
const INSTANCE_CONFIG_FILE = 'koring-instance.json';
function getInstanceConfigPath(instancePath: string): string {
return path.join(instancePath, INSTANCE_CONFIG_FILE);
}
export async function createInstance(
name: string,
gamePath: string,
mcVersion: string,
loaderType?: string,
loaderVersion?: string,
javaPath?: string,
memory?: { min?: string; max?: string }
): Promise<InstanceInfo> {
const instancePath = path.join(gamePath, 'instances', name);
if (!fs.existsSync(instancePath)) {
fs.mkdirSync(instancePath, { recursive: true });
}
const config: InstanceConfig = {
name,
mcVersion,
loaderType,
loaderVersion,
javaPath,
memory,
createdAt: new Date().toISOString(),
};
const configPath = getInstanceConfigPath(instancePath);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
// Create mods directory
const modsDir = path.join(instancePath, 'mods');
if (!fs.existsSync(modsDir)) {
fs.mkdirSync(modsDir, { recursive: true });
}
// Count mods
const mods = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar'));
return {
name,
path: instancePath,
config,
modCount: mods.length,
};
}
export async function listInstances(instancesPath: string): Promise<InstanceInfo[]> {
const instances: InstanceInfo[] = [];
if (!fs.existsSync(instancesPath)) {
return instances;
}
const entries = fs.readdirSync(instancesPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const instancePath = path.join(instancesPath, entry.name);
const configPath = getInstanceConfigPath(instancePath);
if (!fs.existsSync(configPath)) continue;
try {
const configRaw = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configRaw) as InstanceConfig;
// Count mods
const modsDir = path.join(instancePath, 'mods');
let modCount = 0;
if (fs.existsSync(modsDir)) {
modCount = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar')).length;
}
instances.push({
name: entry.name,
path: instancePath,
config,
modCount,
});
} catch {
// Skip invalid config
}
}
return instances;
}
export async function deleteInstance(
name: string,
instancesPath: string
): Promise<{ deleted: string }> {
const instancePath = path.join(instancesPath, name);
if (fs.existsSync(instancePath)) {
fs.rmSync(instancePath, { recursive: true, force: true });
}
return { deleted: name };
}
export async function getInstanceInfo(
name: string,
instancesPath: string
): Promise<InstanceInfo> {
const instancePath = path.join(instancesPath, name);
const configPath = getInstanceConfigPath(instancePath);
if (!fs.existsSync(configPath)) {
throw new Error(`Instance not found: ${name}`);
}
const configRaw = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configRaw) as InstanceConfig;
// Count mods
const modsDir = path.join(instancePath, 'mods');
let modCount = 0;
if (fs.existsSync(modsDir)) {
modCount = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar')).length;
}
return {
name,
path: instancePath,
config,
modCount,
};
}
+152
View File
@@ -0,0 +1,152 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.launchMinecraft = launchMinecraft;
exports.diagnoseVersion = diagnoseVersion;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const runningProcesses = new Map();
async function launchMinecraft(options) {
const versionJsonPath = path.join(options.gamePath, 'versions', options.version, `${options.version}.json`);
if (!fs.existsSync(versionJsonPath)) {
throw new Error(`Version JSON not found: ${versionJsonPath}`);
}
const versionJson = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8'));
const mainClass = versionJson.mainClass;
if (!mainClass) {
throw new Error('Main class not found in version JSON');
}
const args = [];
// Memory
const minMem = options.memory?.min || '512M';
const maxMem = options.memory?.max || '4G';
args.push(`-Xms${minMem}`);
args.push(`-Xmx${maxMem}`);
// JVM args
if (options.jvmArgs) {
args.push(...options.jvmArgs);
}
// Native libraries path
const nativesDir = path.join(options.gamePath, 'versions', options.version, `${options.version}-natives`);
if (fs.existsSync(nativesDir)) {
args.push(`-Djava.library.path=${nativesDir}`);
}
// Classpath
const libraries = versionJson.libraries || [];
const classpath = libraries
.filter((lib) => lib.downloads?.artifact?.path)
.map((lib) => path.join(options.gamePath, 'libraries', lib.downloads.artifact.path));
const clientJar = path.join(options.gamePath, 'versions', options.version, `${options.version}.jar`);
if (fs.existsSync(clientJar)) {
classpath.push(clientJar);
}
args.push('-cp');
args.push(classpath.join(path.delimiter));
args.push(mainClass);
// Game args
args.push(`--username`, options.username);
args.push(`--version`, options.version);
args.push(`--gameDir`, options.gamePath);
args.push(`--assetsDir`, path.join(options.gamePath, 'assets'));
args.push(`--assetIndex`, versionJson.assetIndex?.id || options.version);
args.push(`--uuid`, options.uuid);
if (options.accessToken) {
args.push(`--accessToken`, options.accessToken);
}
if (options.server) {
args.push(`--server`, options.server.ip);
if (options.server.port) {
args.push(`--port`, String(options.server.port));
}
}
if (options.gameArgs) {
args.push(...options.gameArgs);
}
const javaPath = options.javaPath || 'java';
return new Promise((resolve, reject) => {
const child = (0, child_process_1.spawn)(javaPath, args, {
cwd: options.gamePath,
detached: options.detached,
stdio: ['ignore', 'pipe', 'pipe'],
});
const requestId = `mc-${Date.now()}`;
runningProcesses.set(requestId, child);
child.stdout?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
options.onEvent?.({ event: 'stdout', message: line });
}
});
child.stderr?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
options.onEvent?.({ event: 'stderr', message: line });
}
});
child.on('error', (err) => {
runningProcesses.delete(requestId);
options.onEvent?.({ event: 'error', error: String(err) });
reject(err);
});
child.on('exit', (code) => {
runningProcesses.delete(requestId);
options.onEvent?.({ event: 'exit', code });
});
resolve({
pid: child.pid || 0,
version: options.version,
username: options.username,
});
});
}
async function diagnoseVersion(gamePath, version) {
const issues = [];
const versionDir = path.join(gamePath, 'versions', version);
const versionJsonPath = path.join(versionDir, `${version}.json`);
const jarPath = path.join(versionDir, `${version}.jar`);
if (!fs.existsSync(versionJsonPath)) {
issues.push(`Version JSON not found: ${versionJsonPath}`);
}
if (!fs.existsSync(jarPath)) {
issues.push(`Client JAR not found: ${jarPath}`);
}
return {
version,
gamePath,
healthy: issues.length === 0,
issues,
};
}
+165
View File
@@ -0,0 +1,165 @@
import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
interface LaunchOptions {
gamePath: string;
javaPath: string;
version: string;
username: string;
uuid: string;
accessToken?: string;
memory?: { min?: string; max?: string };
jvmArgs?: string[];
gameArgs?: string[];
server?: { ip: string; port?: number };
detached?: boolean;
onEvent?: (event: { event: string; [key: string]: unknown }) => void;
}
interface LaunchResult {
pid: number;
version: string;
username: string;
}
const runningProcesses = new Map<string, ChildProcess>();
export async function launchMinecraft(options: LaunchOptions): Promise<LaunchResult> {
const versionJsonPath = path.join(options.gamePath, 'versions', options.version, `${options.version}.json`);
if (!fs.existsSync(versionJsonPath)) {
throw new Error(`Version JSON not found: ${versionJsonPath}`);
}
const versionJson = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8'));
const mainClass = versionJson.mainClass;
if (!mainClass) {
throw new Error('Main class not found in version JSON');
}
const args: string[] = [];
// Memory
const minMem = options.memory?.min || '512M';
const maxMem = options.memory?.max || '4G';
args.push(`-Xms${minMem}`);
args.push(`-Xmx${maxMem}`);
// JVM args
if (options.jvmArgs) {
args.push(...options.jvmArgs);
}
// Native libraries path
const nativesDir = path.join(options.gamePath, 'versions', options.version, `${options.version}-natives`);
if (fs.existsSync(nativesDir)) {
args.push(`-Djava.library.path=${nativesDir}`);
}
// Classpath
const libraries = versionJson.libraries || [];
const classpath = libraries
.filter((lib: { downloads?: { artifact?: { path: string } } }) => lib.downloads?.artifact?.path)
.map((lib: { downloads: { artifact: { path: string } } }) => path.join(options.gamePath, 'libraries', lib.downloads.artifact.path));
const clientJar = path.join(options.gamePath, 'versions', options.version, `${options.version}.jar`);
if (fs.existsSync(clientJar)) {
classpath.push(clientJar);
}
args.push('-cp');
args.push(classpath.join(path.delimiter));
args.push(mainClass);
// Game args
args.push(`--username`, options.username);
args.push(`--version`, options.version);
args.push(`--gameDir`, options.gamePath);
args.push(`--assetsDir`, path.join(options.gamePath, 'assets'));
args.push(`--assetIndex`, versionJson.assetIndex?.id || options.version);
args.push(`--uuid`, options.uuid);
if (options.accessToken) {
args.push(`--accessToken`, options.accessToken);
}
if (options.server) {
args.push(`--server`, options.server.ip);
if (options.server.port) {
args.push(`--port`, String(options.server.port));
}
}
if (options.gameArgs) {
args.push(...options.gameArgs);
}
const javaPath = options.javaPath || 'java';
return new Promise((resolve, reject) => {
const child = spawn(javaPath, args, {
cwd: options.gamePath,
detached: options.detached,
stdio: ['ignore', 'pipe', 'pipe'],
});
const requestId = `mc-${Date.now()}`;
runningProcesses.set(requestId, child);
child.stdout?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
options.onEvent?.({ event: 'stdout', message: line });
}
});
child.stderr?.on('data', (data) => {
const line = data.toString().trim();
if (line) {
options.onEvent?.({ event: 'stderr', message: line });
}
});
child.on('error', (err) => {
runningProcesses.delete(requestId);
options.onEvent?.({ event: 'error', error: String(err) });
reject(err);
});
child.on('exit', (code) => {
runningProcesses.delete(requestId);
options.onEvent?.({ event: 'exit', code });
});
resolve({
pid: child.pid || 0,
version: options.version,
username: options.username,
});
});
}
export async function diagnoseVersion(gamePath: string, version: string): Promise<Record<string, unknown>> {
const issues: string[] = [];
const versionDir = path.join(gamePath, 'versions', version);
const versionJsonPath = path.join(versionDir, `${version}.json`);
const jarPath = path.join(versionDir, `${version}.jar`);
if (!fs.existsSync(versionJsonPath)) {
issues.push(`Version JSON not found: ${versionJsonPath}`);
}
if (!fs.existsSync(jarPath)) {
issues.push(`Client JAR not found: ${jarPath}`);
}
return {
version,
gamePath,
healthy: issues.length === 0,
issues,
};
}
+163
View File
@@ -0,0 +1,163 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.searchMods = searchMods;
exports.getModDetail = getModDetail;
exports.getModVersions = getModVersions;
exports.installMod = installMod;
const electron_1 = __importDefault(require("electron"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const { net } = electron_1.default;
const MODRINTH_API = 'https://api.modrinth.com/v2';
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
async function searchModrinth(query, gameVersion, loader, limit = 20, offset = 0) {
const facets = [];
if (gameVersion)
facets.push([`versions:${gameVersion}`]);
if (loader)
facets.push([`categories:${loader}`]);
const params = new URLSearchParams({
query: query || '',
limit: String(limit),
offset: String(offset),
});
if (facets.length > 0) {
params.set('facets', JSON.stringify(facets));
}
const response = await net.fetch(`${MODRINTH_API}/search?${params.toString()}`);
if (!response.ok)
throw new Error(`Modrinth search failed: ${response.status}`);
const data = await response.json();
return data.hits.map((hit) => ({
id: hit.project_id,
slug: hit.slug,
name: hit.title,
description: hit.description,
downloads: hit.downloads,
iconUrl: hit.icon_url,
categories: hit.categories,
versions: hit.versions,
source: 'modrinth',
}));
}
async function searchCurseForge(query, gameVersion, loader, limit = 20, offset = 0) {
// CurseForge requires API key - return empty for now
return [];
}
async function searchMods(query, gameVersion, loader, limit, offset, source = 'modrinth') {
if (source === 'modrinth') {
return searchModrinth(query, gameVersion, loader, limit, offset);
}
return searchCurseForge(query, gameVersion, loader, limit, offset);
}
async function getModDetail(projectId, source = 'modrinth') {
if (source === 'modrinth') {
const response = await net.fetch(`${MODRINTH_API}/project/${projectId}`);
if (!response.ok)
throw new Error(`Modrinth detail failed: ${response.status}`);
const data = await response.json();
return {
id: data.id,
slug: data.slug,
name: data.title,
description: data.description,
downloads: data.downloads,
iconUrl: data.icon_url,
categories: data.categories,
versions: data.versions,
source: 'modrinth',
};
}
throw new Error(`CurseForge detail not implemented`);
}
async function getModVersions(projectId, gameVersion, loader, source = 'modrinth') {
if (source === 'modrinth') {
const params = new URLSearchParams();
if (gameVersion)
params.set('game_versions', JSON.stringify([gameVersion]));
if (loader)
params.set('loaders', JSON.stringify([loader]));
const response = await net.fetch(`${MODRINTH_API}/project/${projectId}/version?${params.toString()}`);
if (!response.ok)
throw new Error(`Modrinth versions failed: ${response.status}`);
const data = await response.json();
return data.map((v) => ({
id: v.id,
name: v.name,
versionNumber: v.version_number,
gameVersions: v.game_versions,
loaders: v.loaders,
files: v.files.map((f) => ({
filename: f.filename,
url: f.url,
size: f.size,
primary: f.primary,
})),
}));
}
throw new Error(`CurseForge versions not implemented`);
}
async function installMod(projectId, versionId, gamePath, source = 'modrinth') {
const versions = await getModVersions(projectId, undefined, undefined, source);
const targetVersion = versionId
? versions.find((v) => v.id === versionId)
: versions[0];
if (!targetVersion)
throw new Error('No version found');
const primaryFile = targetVersion.files.find((f) => f.primary) || targetVersion.files[0];
if (!primaryFile)
throw new Error('No file found');
// Download the mod file
const modsDir = path.join(gamePath, 'mods');
if (!fs.existsSync(modsDir)) {
fs.mkdirSync(modsDir, { recursive: true });
}
const filePath = path.join(modsDir, primaryFile.filename);
const response = await net.fetch(primaryFile.url);
if (!response.ok)
throw new Error(`Download failed: ${response.status}`);
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(filePath, buffer);
return {
projectId,
versionId: targetVersion.id,
filename: primaryFile.filename,
path: filePath,
};
}
+228
View File
@@ -0,0 +1,228 @@
import electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
const { net } = electron;
const MODRINTH_API = 'https://api.modrinth.com/v2';
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
interface ModSearchResult {
id: string;
slug: string;
name: string;
description: string;
downloads: number;
iconUrl?: string;
categories?: string[];
versions?: string[];
loaders?: string[];
source: 'modrinth' | 'curseforge';
}
interface ModVersionResult {
id: string;
name: string;
versionNumber: string;
gameVersions: string[];
loaders: string[];
files: {
filename: string;
url: string;
size: number;
primary: boolean;
}[];
}
async function searchModrinth(
query?: string,
gameVersion?: string,
loader?: string,
limit: number = 20,
offset: number = 0
): Promise<ModSearchResult[]> {
const facets: string[][] = [];
if (gameVersion) facets.push([`versions:${gameVersion}`]);
if (loader) facets.push([`categories:${loader}`]);
const params = new URLSearchParams({
query: query || '',
limit: String(limit),
offset: String(offset),
});
if (facets.length > 0) {
params.set('facets', JSON.stringify(facets));
}
const response = await net.fetch(`${MODRINTH_API}/search?${params.toString()}`);
if (!response.ok) throw new Error(`Modrinth search failed: ${response.status}`);
const data = await response.json() as { hits: Array<{
project_id: string;
slug: string;
title: string;
description: string;
downloads: number;
icon_url?: string;
categories?: string[];
versions?: string[];
client_side?: string;
server_side?: string;
}> };
return data.hits.map((hit) => ({
id: hit.project_id,
slug: hit.slug,
name: hit.title,
description: hit.description,
downloads: hit.downloads,
iconUrl: hit.icon_url,
categories: hit.categories,
versions: hit.versions,
source: 'modrinth' as const,
}));
}
async function searchCurseForge(
query?: string,
gameVersion?: string,
loader?: string,
limit: number = 20,
offset: number = 0
): Promise<ModSearchResult[]> {
// CurseForge requires API key - return empty for now
return [];
}
export async function searchMods(
query?: string,
gameVersion?: string,
loader?: string,
limit?: number,
offset?: number,
source: 'modrinth' | 'curseforge' = 'modrinth'
): Promise<ModSearchResult[]> {
if (source === 'modrinth') {
return searchModrinth(query, gameVersion, loader, limit, offset);
}
return searchCurseForge(query, gameVersion, loader, limit, offset);
}
export async function getModDetail(
projectId: string,
source: 'modrinth' | 'curseforge' = 'modrinth'
): Promise<ModSearchResult> {
if (source === 'modrinth') {
const response = await net.fetch(`${MODRINTH_API}/project/${projectId}`);
if (!response.ok) throw new Error(`Modrinth detail failed: ${response.status}`);
const data = await response.json() as {
id: string;
slug: string;
title: string;
description: string;
downloads: number;
icon_url?: string;
categories?: string[];
versions?: string[];
};
return {
id: data.id,
slug: data.slug,
name: data.title,
description: data.description,
downloads: data.downloads,
iconUrl: data.icon_url,
categories: data.categories,
versions: data.versions,
source: 'modrinth',
};
}
throw new Error(`CurseForge detail not implemented`);
}
export async function getModVersions(
projectId: string,
gameVersion?: string,
loader?: string,
source: 'modrinth' | 'curseforge' = 'modrinth'
): Promise<ModVersionResult[]> {
if (source === 'modrinth') {
const params = new URLSearchParams();
if (gameVersion) params.set('game_versions', JSON.stringify([gameVersion]));
if (loader) params.set('loaders', JSON.stringify([loader]));
const response = await net.fetch(`${MODRINTH_API}/project/${projectId}/version?${params.toString()}`);
if (!response.ok) throw new Error(`Modrinth versions failed: ${response.status}`);
const data = await response.json() as Array<{
id: string;
name: string;
version_number: string;
game_versions: string[];
loaders: string[];
files: Array<{
filename: string;
url: string;
size: number;
primary: boolean;
}>;
}>;
return data.map((v) => ({
id: v.id,
name: v.name,
versionNumber: v.version_number,
gameVersions: v.game_versions,
loaders: v.loaders,
files: v.files.map((f) => ({
filename: f.filename,
url: f.url,
size: f.size,
primary: f.primary,
})),
}));
}
throw new Error(`CurseForge versions not implemented`);
}
export async function installMod(
projectId: string,
versionId: string | undefined,
gamePath: string,
source: 'modrinth' | 'curseforge' = 'modrinth'
): Promise<{ projectId: string; versionId: string; filename: string; path: string }> {
const versions = await getModVersions(projectId, undefined, undefined, source);
const targetVersion = versionId
? versions.find((v) => v.id === versionId)
: versions[0];
if (!targetVersion) throw new Error('No version found');
const primaryFile = targetVersion.files.find((f) => f.primary) || targetVersion.files[0];
if (!primaryFile) throw new Error('No file found');
// Download the mod file
const modsDir = path.join(gamePath, 'mods');
if (!fs.existsSync(modsDir)) {
fs.mkdirSync(modsDir, { recursive: true });
}
const filePath = path.join(modsDir, primaryFile.filename);
const response = await net.fetch(primaryFile.url);
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(filePath, buffer);
return {
projectId,
versionId: targetVersion.id,
filename: primaryFile.filename,
path: filePath,
};
}
+38
View File
@@ -0,0 +1,38 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerAuthHandlers = registerAuthHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain } = electron_1.default;
const auth_1 = require("../auth");
function registerAuthHandlers() {
ipcMain.handle('auth:get', () => {
try {
const auth = (0, auth_1.readAuth)();
return { success: true, data: auth, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('auth:save', (_event, auth) => {
try {
(0, auth_1.writeAuth)(auth);
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('auth:delete', () => {
try {
(0, auth_1.deleteAuth)();
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+32
View File
@@ -0,0 +1,32 @@
import electron from 'electron';
const { ipcMain } = electron;
import { readAuth, writeAuth, deleteAuth } from '../auth';
export function registerAuthHandlers() {
ipcMain.handle('auth:get', () => {
try {
const auth = readAuth();
return { success: true, data: auth, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('auth:save', (_event, auth) => {
try {
writeAuth(auth);
return { success: true, data: null, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('auth:delete', () => {
try {
deleteAuth();
return { success: true, data: null, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+101
View File
@@ -0,0 +1,101 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerBackgroundHandlers = registerBackgroundHandlers;
const electron_1 = __importDefault(require("electron"));
const config_1 = require("../config");
const { ipcMain } = electron_1.default;
function registerBackgroundHandlers() {
ipcMain.handle('background:set-image', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.bgType = 'image';
config.background.image = payload.url;
if (payload.blur !== undefined)
config.background.blur = payload.blur;
if (payload.opacity !== undefined)
config.background.opacity = payload.opacity;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-color', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.bgType = 'color';
config.background.image = payload.color;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-blur', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.blur = payload.blur;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-opacity', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.background.opacity = payload.opacity;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-animation', async (_event, payload) => {
try {
// Animation config is not persisted in current design, return defaults
return { success: true, data: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:get', async () => {
try {
const config = (0, config_1.loadConfig)();
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-theme', async (_event, payload) => {
try {
const config = (0, config_1.loadConfig)();
config.theme.darkMode = payload.theme;
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:reset', async () => {
try {
const config = (0, config_1.loadConfig)();
config.background = { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 };
(0, config_1.saveConfig)(config);
return { success: true, data: config.background, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+94
View File
@@ -0,0 +1,94 @@
import electron from 'electron';
import { loadConfig, saveConfig } from '../config';
const { ipcMain } = electron;
export function registerBackgroundHandlers() {
ipcMain.handle('background:set-image', async (_event, payload: { url: string; blur?: number; opacity?: number }) => {
try {
const config = loadConfig();
config.background.bgType = 'image';
config.background.image = payload.url;
if (payload.blur !== undefined) config.background.blur = payload.blur;
if (payload.opacity !== undefined) config.background.opacity = payload.opacity;
saveConfig(config);
return { success: true, data: config.background, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-color', async (_event, payload: { color: string }) => {
try {
const config = loadConfig();
config.background.bgType = 'color';
config.background.image = payload.color;
saveConfig(config);
return { success: true, data: config.background, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-blur', async (_event, payload: { blur: number }) => {
try {
const config = loadConfig();
config.background.blur = payload.blur;
saveConfig(config);
return { success: true, data: config.background, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-opacity', async (_event, payload: { opacity: number }) => {
try {
const config = loadConfig();
config.background.opacity = payload.opacity;
saveConfig(config);
return { success: true, data: config.background, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-animation', async (_event, payload: { type: string; speed?: number }) => {
try {
// Animation config is not persisted in current design, return defaults
return { success: true, data: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 }, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:get', async () => {
try {
const config = loadConfig();
return { success: true, data: config.background, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:set-theme', async (_event, payload: { theme: string }) => {
try {
const config = loadConfig();
config.theme.darkMode = payload.theme;
saveConfig(config);
return { success: true, data: config.background, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('background:reset', async () => {
try {
const config = loadConfig();
config.background = { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 };
saveConfig(config);
return { success: true, data: config.background, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+29
View File
@@ -0,0 +1,29 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerConfigHandlers = registerConfigHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain } = electron_1.default;
const config_1 = require("../config");
function registerConfigHandlers() {
ipcMain.handle('config:get', () => {
try {
const config = (0, config_1.loadConfig)();
return { success: true, data: config, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('config:save', (_event, config) => {
try {
(0, config_1.saveConfig)(config);
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+23
View File
@@ -0,0 +1,23 @@
import electron from 'electron';
const { ipcMain } = electron;
import { loadConfig, saveConfig, type AppConfig } from '../config';
export function registerConfigHandlers() {
ipcMain.handle('config:get', () => {
try {
const config = loadConfig();
return { success: true, data: config, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('config:save', (_event, config: AppConfig) => {
try {
saveConfig(config);
return { success: true, data: null, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+74
View File
@@ -0,0 +1,74 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerInstallHandlers = registerInstallHandlers;
const electron_1 = __importDefault(require("electron"));
const installer_1 = require("../core/installer");
const { ipcMain } = electron_1.default;
function registerInstallHandlers(win) {
ipcMain.handle('install:version-list', async (_event, payload) => {
try {
const data = await (0, installer_1.getVersionList)(payload.type);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:forge-version-list', async (_event, payload) => {
try {
const data = await (0, installer_1.getForgeVersions)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:fabric-version-list', async (_event, payload) => {
try {
const data = await (0, installer_1.getFabricVersions)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:minecraft', async (_event, payload) => {
try {
const requestId = `install-mc-${Date.now()}`;
(0, installer_1.installMinecraft)(payload.version, payload.gamePath, payload.javaPath, payload.downloadThreads, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('install:progress', { requestId, ...progress });
},
}).then((result) => {
win.mainWindow?.webContents.send('install:complete', { requestId, ...result });
}).catch((err) => {
win.mainWindow?.webContents.send('install:error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:mod-loader', async (_event, payload) => {
try {
const requestId = `install-loader-${Date.now()}`;
(0, installer_1.installModLoader)(payload.mcVersion, payload.gamePath, payload.loaderType, payload.loaderVersion, payload.javaPath, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('install:progress', { requestId, ...progress });
},
}).then((result) => {
win.mainWindow?.webContents.send('install:complete', { requestId, ...result });
}).catch((err) => {
win.mainWindow?.webContents.send('install:error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+88
View File
@@ -0,0 +1,88 @@
import electron from 'electron';
import { installMinecraft, installModLoader, getVersionList, getForgeVersions, getFabricVersions } from '../core/installer';
const { ipcMain } = electron;
interface WinRef {
mainWindow: electron.BrowserWindow | null;
}
export function registerInstallHandlers(win: WinRef) {
ipcMain.handle('install:version-list', async (_event, payload: { type?: string }) => {
try {
const data = await getVersionList(payload.type);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:forge-version-list', async (_event, payload: { mcVersion?: string }) => {
try {
const data = await getForgeVersions(payload.mcVersion);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:fabric-version-list', async (_event, payload: { mcVersion?: string }) => {
try {
const data = await getFabricVersions(payload.mcVersion);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:minecraft', async (_event, payload: {
version: string;
gamePath: string;
javaPath?: string;
downloadThreads?: number;
}) => {
try {
const requestId = `install-mc-${Date.now()}`;
installMinecraft(payload.version, payload.gamePath, payload.javaPath, payload.downloadThreads, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('install:progress', { requestId, ...progress });
},
}).then((result) => {
win.mainWindow?.webContents.send('install:complete', { requestId, ...result });
}).catch((err) => {
win.mainWindow?.webContents.send('install:error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('install:mod-loader', async (_event, payload: {
mcVersion: string;
gamePath: string;
loaderType: string;
loaderVersion?: string;
javaPath?: string;
}) => {
try {
const requestId = `install-loader-${Date.now()}`;
installModLoader(payload.mcVersion, payload.gamePath, payload.loaderType as 'forge' | 'fabric' | 'quilt' | 'neoforge', payload.loaderVersion, payload.javaPath, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('install:progress', { requestId, ...progress });
},
}).then((result) => {
win.mainWindow?.webContents.send('install:complete', { requestId, ...result });
}).catch((err) => {
win.mainWindow?.webContents.send('install:error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerInstanceHandlers = registerInstanceHandlers;
const electron_1 = __importDefault(require("electron"));
const instance_1 = require("../core/instance");
const { ipcMain } = electron_1.default;
function registerInstanceHandlers() {
ipcMain.handle('instance:create', async (_event, payload) => {
try {
const data = await (0, instance_1.createInstance)(payload.name, payload.gamePath, payload.mcVersion, payload.loaderType, payload.loaderVersion, payload.javaPath, payload.memory);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:list', async (_event, payload) => {
try {
const data = await (0, instance_1.listInstances)(payload.instancesPath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:delete', async (_event, payload) => {
try {
const data = await (0, instance_1.deleteInstance)(payload.name, payload.instancesPath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:info', async (_event, payload) => {
try {
const data = await (0, instance_1.getInstanceInfo)(payload.name, payload.instancesPath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+58
View File
@@ -0,0 +1,58 @@
import electron from 'electron';
import { createInstance, listInstances, deleteInstance, getInstanceInfo } from '../core/instance';
const { ipcMain } = electron;
export function registerInstanceHandlers() {
ipcMain.handle('instance:create', async (_event, payload: {
name: string;
gamePath: string;
mcVersion: string;
loaderType?: string;
loaderVersion?: string;
javaPath?: string;
memory?: { min?: string; max?: string };
}) => {
try {
const data = await createInstance(
payload.name,
payload.gamePath,
payload.mcVersion,
payload.loaderType,
payload.loaderVersion,
payload.javaPath,
payload.memory
);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:list', async (_event, payload: { instancesPath: string }) => {
try {
const data = await listInstances(payload.instancesPath);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:delete', async (_event, payload: { name: string; instancesPath: string }) => {
try {
const data = await deleteInstance(payload.name, payload.instancesPath);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:info', async (_event, payload: { name: string; instancesPath: string }) => {
try {
const data = await getInstanceInfo(payload.name, payload.instancesPath);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+45
View File
@@ -0,0 +1,45 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerLaunchHandlers = registerLaunchHandlers;
const electron_1 = __importDefault(require("electron"));
const launcher_1 = require("../core/launcher");
const { ipcMain } = electron_1.default;
function registerLaunchHandlers(win) {
ipcMain.handle('launch:launch', async (_event, payload) => {
try {
const requestId = `launch-${Date.now()}`;
const result = await (0, launcher_1.launchMinecraft)({
gamePath: payload.gamePath,
javaPath: payload.javaPath,
version: payload.version,
username: payload.username,
uuid: payload.uuid,
accessToken: payload.accessToken,
memory: payload.memory,
jvmArgs: payload.jvmArgs,
gameArgs: payload.gameArgs,
server: payload.server,
detached: payload.detached,
onEvent: (event) => {
win.mainWindow?.webContents.send('launch:event', { requestId, ...event });
},
});
return { success: true, data: { ...result, requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('launch:diagnose', async (_event, payload) => {
try {
const data = await (0, launcher_1.diagnoseVersion)(payload.gamePath, payload.version);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+58
View File
@@ -0,0 +1,58 @@
import electron from 'electron';
import { launchMinecraft, diagnoseVersion } from '../core/launcher';
const { ipcMain } = electron;
interface WinRef {
mainWindow: electron.BrowserWindow | null;
}
export function registerLaunchHandlers(win: WinRef) {
ipcMain.handle('launch:launch', async (_event, payload: {
gamePath: string;
javaPath: string;
version: string;
username: string;
uuid: string;
accessToken?: string;
memory?: { min?: string; max?: string };
jvmArgs?: string[];
gameArgs?: string[];
server?: { ip: string; port?: number };
detached?: boolean;
}) => {
try {
const requestId = `launch-${Date.now()}`;
const result = await launchMinecraft({
gamePath: payload.gamePath,
javaPath: payload.javaPath,
version: payload.version,
username: payload.username,
uuid: payload.uuid,
accessToken: payload.accessToken,
memory: payload.memory,
jvmArgs: payload.jvmArgs,
gameArgs: payload.gameArgs,
server: payload.server,
detached: payload.detached,
onEvent: (event) => {
win.mainWindow?.webContents.send('launch:event', { requestId, ...event });
},
});
return { success: true, data: { ...result, requestId }, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('launch:diagnose', async (_event, payload: { gamePath: string; version: string }) => {
try {
const data = await diagnoseVersion(payload.gamePath, payload.version);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+47
View File
@@ -0,0 +1,47 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerModsHandlers = registerModsHandlers;
const electron_1 = __importDefault(require("electron"));
const modrinth_1 = require("../core/modrinth");
const { ipcMain } = electron_1.default;
function registerModsHandlers() {
ipcMain.handle('mods:search', async (_event, payload) => {
try {
const data = await (0, modrinth_1.searchMods)(payload.query, payload.gameVersion, payload.loader, payload.limit, payload.offset, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:detail', async (_event, payload) => {
try {
const data = await (0, modrinth_1.getModDetail)(payload.projectId, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:versions', async (_event, payload) => {
try {
const data = await (0, modrinth_1.getModVersions)(payload.projectId, payload.gameVersion, payload.loader, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:install', async (_event, payload) => {
try {
const data = await (0, modrinth_1.installMod)(payload.projectId, payload.versionId, payload.gamePath, payload.source);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+79
View File
@@ -0,0 +1,79 @@
import electron from 'electron';
import { searchMods, getModDetail, getModVersions, installMod } from '../core/modrinth';
const { ipcMain } = electron;
export function registerModsHandlers() {
ipcMain.handle('mods:search', async (_event, payload: {
query?: string;
gameVersion?: string;
loader?: string;
limit?: number;
offset?: number;
source: string;
}) => {
try {
const data = await searchMods(
payload.query,
payload.gameVersion,
payload.loader,
payload.limit,
payload.offset,
payload.source as 'modrinth' | 'curseforge'
);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:detail', async (_event, payload: { projectId: string; source: string }) => {
try {
const data = await getModDetail(
payload.projectId,
payload.source as 'modrinth' | 'curseforge'
);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:versions', async (_event, payload: {
projectId: string;
gameVersion?: string;
loader?: string;
source: string;
}) => {
try {
const data = await getModVersions(
payload.projectId,
payload.gameVersion,
payload.loader,
payload.source as 'modrinth' | 'curseforge'
);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('mods:install', async (_event, payload: {
projectId: string;
versionId?: string;
gamePath: string;
source: string;
}) => {
try {
const data = await installMod(
payload.projectId,
payload.versionId,
payload.gamePath,
payload.source as 'modrinth' | 'curseforge'
);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+70
View File
@@ -0,0 +1,70 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerSystemHandlers = registerSystemHandlers;
const electron_1 = __importDefault(require("electron"));
const child_process_1 = require("child_process");
const { ipcMain, app } = electron_1.default;
function getBiosId() {
if (process.platform !== 'win32')
return 'N/A (non-Windows)';
try {
const output = (0, child_process_1.execSync)('powershell -NoProfile -Command "(Get-CimInstance Win32_BIOS).SerialNumber"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
}
catch {
return 'Unknown';
}
}
function getOsVersion() {
if (process.platform !== 'win32')
return 'N/A (non-Windows)';
try {
const output = (0, child_process_1.execSync)('powershell -NoProfile -Command "[System.Environment]::OSVersion.VersionString"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
}
catch {
return 'Unknown';
}
}
function getOsName() {
if (process.platform !== 'win32')
return 'N/A (non-Windows)';
try {
const output = (0, child_process_1.execSync)('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).Caption"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
}
catch {
return 'Unknown';
}
}
function registerSystemHandlers() {
ipcMain.handle('system:info', () => {
try {
return {
success: true,
data: {
app_version: app.getVersion(),
bios_id: getBiosId(),
os_version: getOsVersion(),
os_name: getOsName(),
},
error: null,
};
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+63
View File
@@ -0,0 +1,63 @@
import electron from 'electron';
import { execSync } from 'child_process';
import * as os from 'os';
const { ipcMain, app } = electron;
function getBiosId(): string {
if (process.platform !== 'win32') return 'N/A (non-Windows)';
try {
const output = execSync('powershell -NoProfile -Command "(Get-CimInstance Win32_BIOS).SerialNumber"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
} catch {
return 'Unknown';
}
}
function getOsVersion(): string {
if (process.platform !== 'win32') return 'N/A (non-Windows)';
try {
const output = execSync('powershell -NoProfile -Command "[System.Environment]::OSVersion.VersionString"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
} catch {
return 'Unknown';
}
}
function getOsName(): string {
if (process.platform !== 'win32') return 'N/A (non-Windows)';
try {
const output = execSync('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).Caption"', {
encoding: 'utf-8',
timeout: 5000,
});
return output.trim() || 'Unknown';
} catch {
return 'Unknown';
}
}
export function registerSystemHandlers() {
ipcMain.handle('system:info', () => {
try {
return {
success: true,
data: {
app_version: app.getVersion(),
bios_id: getBiosId(),
os_version: getOsVersion(),
os_name: getOsName(),
},
error: null,
};
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+61
View File
@@ -0,0 +1,61 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerTaskHandlers = registerTaskHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain } = electron_1.default;
const runningTasks = new Map();
function registerTaskHandlers(win) {
ipcMain.handle('task:start', async (_event, payload) => {
try {
const controller = new AbortController();
runningTasks.set(payload.taskId, controller);
win.mainWindow?.webContents.send('task:started', {
taskId: payload.taskId,
xmclPath: payload.executorName,
});
// Simulate task execution
const steps = 20;
for (let i = 0; i <= steps; i++) {
if (controller.signal.aborted) {
win.mainWindow?.webContents.send('task:failed', {
taskId: payload.taskId,
error: 'Task cancelled',
});
runningTasks.delete(payload.taskId);
return { success: true, data: null, error: null };
}
win.mainWindow?.webContents.send('task:progress', {
taskId: payload.taskId,
current: i,
total: steps,
stage: `步骤 ${i}/${steps}`,
});
await new Promise((r) => setTimeout(r, 150));
}
runningTasks.delete(payload.taskId);
win.mainWindow?.webContents.send('task:completed', {
taskId: payload.taskId,
});
return { success: true, data: null, error: null };
}
catch (e) {
runningTasks.delete(payload.taskId);
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('task:cancel', async (_event, payload) => {
try {
const controller = runningTasks.get(payload.taskId);
if (controller) {
controller.abort();
}
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+74
View File
@@ -0,0 +1,74 @@
import electron from 'electron';
const { ipcMain } = electron;
const runningTasks = new Map<string, AbortController>();
interface WinRef {
mainWindow: electron.BrowserWindow | null;
}
export function registerTaskHandlers(win: WinRef) {
ipcMain.handle('task:start', async (_event, payload: {
taskId: string;
type: string;
title: string;
description?: string;
executorName: string;
params?: Record<string, unknown>;
}) => {
try {
const controller = new AbortController();
runningTasks.set(payload.taskId, controller);
win.mainWindow?.webContents.send('task:started', {
taskId: payload.taskId,
xmclPath: payload.executorName,
});
// Simulate task execution
const steps = 20;
for (let i = 0; i <= steps; i++) {
if (controller.signal.aborted) {
win.mainWindow?.webContents.send('task:failed', {
taskId: payload.taskId,
error: 'Task cancelled',
});
runningTasks.delete(payload.taskId);
return { success: true, data: null, error: null };
}
win.mainWindow?.webContents.send('task:progress', {
taskId: payload.taskId,
current: i,
total: steps,
stage: `步骤 ${i}/${steps}`,
});
await new Promise((r) => setTimeout(r, 150));
}
runningTasks.delete(payload.taskId);
win.mainWindow?.webContents.send('task:completed', {
taskId: payload.taskId,
});
return { success: true, data: null, error: null };
} catch (e: unknown) {
runningTasks.delete(payload.taskId);
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('task:cancel', async (_event, payload: { taskId: string }) => {
try {
const controller = runningTasks.get(payload.taskId);
if (controller) {
controller.abort();
}
return { success: true, data: null, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+80
View File
@@ -0,0 +1,80 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerWindowHandlers = registerWindowHandlers;
const electron_1 = __importDefault(require("electron"));
const path_1 = __importDefault(require("path"));
const { ipcMain, dialog } = electron_1.default;
const isDev = !electron_1.default.app.isPackaged;
function createSplashWindow() {
const splash = new electron_1.default.BrowserWindow({
width: 480,
height: 320,
transparent: true,
frame: false,
resizable: false,
skipTaskbar: true,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
if (isDev) {
splash.loadURL('http://localhost:1420/splash.html');
}
else {
splash.loadFile(path_1.default.join(__dirname, '../dist/splash.html'));
}
return splash;
}
function registerWindowHandlers(win) {
ipcMain.handle('window:minimize', () => {
win.mainWindow?.minimize();
});
ipcMain.handle('window:maximize', () => {
if (win.mainWindow?.isMaximized()) {
win.mainWindow.unmaximize();
}
else {
win.mainWindow?.maximize();
}
});
ipcMain.handle('window:close', () => {
win.mainWindow?.close();
});
ipcMain.handle('window:isMaximized', () => {
return win.mainWindow?.isMaximized() ?? false;
});
ipcMain.handle('window:getTheme', () => {
return win.mainWindow?.themeSource ?? null;
});
// Splash window management
ipcMain.handle('window:openSplash', () => {
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
win.splashWindow.focus();
return { success: true };
}
win.splashWindow = createSplashWindow();
return { success: true };
});
ipcMain.handle('window:closeSplash', () => {
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
win.splashWindow.close();
win.splashWindow = null;
}
return { success: true };
});
// File dialog
ipcMain.handle('dialog:openFile', async (_event, payload) => {
const result = await dialog.showOpenDialog(win.mainWindow, {
properties: ['openFile'],
filters: payload.filters,
});
if (result.canceled || result.filePaths.length === 0)
return null;
return result.filePaths[0];
});
}
+91
View File
@@ -0,0 +1,91 @@
import electron from 'electron';
import path from 'path';
const { ipcMain, dialog } = electron;
const isDev = !electron.app.isPackaged;
interface WinRef {
mainWindow: electron.BrowserWindow | null;
splashWindow: electron.BrowserWindow | null;
}
function createSplashWindow(): electron.BrowserWindow {
const splash = new electron.BrowserWindow({
width: 480,
height: 320,
transparent: true,
frame: false,
resizable: false,
skipTaskbar: true,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
if (isDev) {
splash.loadURL('http://localhost:1420/splash.html');
} else {
splash.loadFile(path.join(__dirname, '../dist/splash.html'));
}
return splash;
}
export function registerWindowHandlers(win: WinRef) {
ipcMain.handle('window:minimize', () => {
win.mainWindow?.minimize();
});
ipcMain.handle('window:maximize', () => {
if (win.mainWindow?.isMaximized()) {
win.mainWindow.unmaximize();
} else {
win.mainWindow?.maximize();
}
});
ipcMain.handle('window:close', () => {
win.mainWindow?.close();
});
ipcMain.handle('window:isMaximized', () => {
return win.mainWindow?.isMaximized() ?? false;
});
ipcMain.handle('window:getTheme', () => {
return (win.mainWindow as any)?.themeSource ?? null;
});
// Splash window management
ipcMain.handle('window:openSplash', () => {
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
win.splashWindow.focus();
return { success: true };
}
win.splashWindow = createSplashWindow();
return { success: true };
});
ipcMain.handle('window:closeSplash', () => {
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
win.splashWindow.close();
win.splashWindow = null;
}
return { success: true };
});
// File dialog
ipcMain.handle('dialog:openFile', async (_event, payload: {
filters?: { name: string; extensions: string[] }[];
}) => {
const result = await dialog.showOpenDialog(win.mainWindow!, {
properties: ['openFile'],
filters: payload.filters,
});
if (result.canceled || result.filePaths.length === 0) return null;
return result.filePaths[0];
});
}
+142
View File
@@ -0,0 +1,142 @@
import electron from 'electron';
import path from 'path';
import { registerConfigHandlers } from './handlers/config';
import { registerAuthHandlers } from './handlers/auth';
import { registerInstallHandlers } from './handlers/install';
import { registerLaunchHandlers } from './handlers/launch';
import { registerModsHandlers } from './handlers/mods';
import { registerInstanceHandlers } from './handlers/instance';
import { registerBackgroundHandlers } from './handlers/background';
import { registerTaskHandlers } from './handlers/task';
import { registerSystemHandlers } from './handlers/system';
import { registerWindowHandlers } from './handlers/window';
const { app } = electron;
const isDev = !app.isPackaged;
// Mutable ref — handlers always read from this
const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.BrowserWindow | null } = {
mainWindow: null,
splashWindow: null,
};
function createSplashWindow(): electron.BrowserWindow {
const splash = new electron.BrowserWindow({
width: 480,
height: 320,
transparent: true,
frame: false,
resizable: false,
skipTaskbar: true,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
if (isDev) {
splash.loadURL('http://localhost:1420/splash.html');
} else {
splash.loadFile(path.join(__dirname, '../dist/splash.html'));
}
return splash;
}
function createMainWindow(): electron.BrowserWindow {
const main = new electron.BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 600,
transparent: true,
frame: false,
resizable: true,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
},
});
if (isDev) {
main.loadURL('http://localhost:1420');
} else {
main.loadFile(path.join(__dirname, '../dist/index.html'));
}
main.on('maximize', () => {
main.webContents.send('window:resized');
});
main.on('unmaximize', () => {
main.webContents.send('window:resized');
});
return main;
}
function registerAllHandlers() {
registerConfigHandlers();
registerAuthHandlers();
registerInstallHandlers(win);
registerLaunchHandlers(win);
registerModsHandlers();
registerInstanceHandlers();
registerBackgroundHandlers();
registerTaskHandlers(win);
registerSystemHandlers();
registerWindowHandlers(win);
}
app.whenReady().then(() => {
registerAllHandlers();
// 1. Show splash immediately
win.splashWindow = createSplashWindow();
// 2. Create main window in background
win.mainWindow = createMainWindow();
// 3. When main window finishes loading, wait a minimum time then transition
let mainReady = false;
let splashMinTimeDone = false;
const tryTransition = () => {
if (mainReady && splashMinTimeDone) {
if (win.mainWindow && !win.mainWindow.isDestroyed()) {
win.mainWindow.show();
win.mainWindow.focus();
}
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
win.splashWindow.close();
win.splashWindow = null;
}
}
};
win.mainWindow.once('ready-to-show', () => {
mainReady = true;
tryTransition();
});
// Minimum splash display time (1.5s)
setTimeout(() => {
splashMinTimeDone = true;
tryTransition();
}, 1500);
});
app.on('window-all-closed', () => {
app.quit();
});
app.on('activate', () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
win.mainWindow = createMainWindow();
}
});
+31
View File
@@ -0,0 +1,31 @@
import electron from 'electron';
const { contextBridge, ipcRenderer } = electron;
contextBridge.exposeInMainWorld('electronAPI', {
// Generic IPC
invoke: (channel: string, ...args: unknown[]) =>
ipcRenderer.invoke(channel, ...args),
on: (channel: string, callback: (...args: unknown[]) => void) => {
const handler = (_event: Electron.IpcRendererEvent, ...args: unknown[]) => callback(...args);
ipcRenderer.on(channel, handler);
return () => ipcRenderer.removeListener(channel, handler);
},
send: (channel: string, ...args: unknown[]) =>
ipcRenderer.send(channel, ...args),
// Window controls
minimize: () => ipcRenderer.invoke('window:minimize'),
maximize: () => ipcRenderer.invoke('window:maximize'),
close: () => ipcRenderer.invoke('window:close'),
isMaximized: () => ipcRenderer.invoke('window:isMaximized'),
onResized: (callback: () => void) => {
const handler = () => callback();
ipcRenderer.on('window:resized', handler);
return () => ipcRenderer.removeListener('window:resized', handler);
},
// Theme
getTheme: () => ipcRenderer.invoke('window:getTheme'),
});