mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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];
|
||||
});
|
||||
}
|
||||
@@ -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];
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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'),
|
||||
});
|
||||
Reference in New Issue
Block a user