Upload via GUI (19 Dateien)
This commit is contained in:
+614
@@ -0,0 +1,614 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* main.js – Electron Hauptprozess für ViperCraft
|
||||
* Phase 1: Fenster, Instanz-Verwaltung, Einstellungen.
|
||||
* (Vanilla-Start, Microsoft-Login und Mod-Loader folgen in späteren Phasen.)
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { execFile } = require('child_process');
|
||||
const store = require('./store');
|
||||
const services = require('./services');
|
||||
const launcher = require('./launcher');
|
||||
const loaders = require('./loaders');
|
||||
const forge = require('./forge');
|
||||
const auth = require('./auth');
|
||||
|
||||
// undici als globales fetch verwenden -> ermöglicht Proxy via setGlobalDispatcher
|
||||
const undici = require('undici');
|
||||
globalThis.fetch = undici.fetch;
|
||||
globalThis.Headers = undici.Headers;
|
||||
globalThis.Request = undici.Request;
|
||||
globalThis.Response = undici.Response;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Automatische Überwachung der Anmelde-Dienste
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const AUTH_ENDPOINTS = [
|
||||
{ name: 'Microsoft-Login', url: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode' },
|
||||
{ name: 'Xbox Live', url: 'https://user.auth.xboxlive.com/user/authenticate' },
|
||||
{ name: 'Minecraft-Dienste', url: 'https://api.minecraftservices.com/minecraft/profile' },
|
||||
];
|
||||
|
||||
let authHealth = { ok: true, checked: null, down: [] };
|
||||
|
||||
async function checkAuthServices() {
|
||||
const down = [];
|
||||
for (const ep of AUTH_ENDPOINTS) {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(() => ctrl.abort(), 8000);
|
||||
// Antwort-Status ist egal (401/404 = Dienst lebt); nur Ausfall/5xx zählt als "down"
|
||||
const res = await fetch(ep.url, { method: 'GET', signal: ctrl.signal, headers: { 'User-Agent': 'AeroMC-Launcher/0.28' } });
|
||||
clearTimeout(t);
|
||||
if (res.status >= 500) down.push(ep.name);
|
||||
} catch {
|
||||
down.push(ep.name);
|
||||
}
|
||||
}
|
||||
authHealth = { ok: down.length === 0, checked: new Date().toISOString(), down };
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('auth:health', authHealth);
|
||||
return authHealth;
|
||||
}
|
||||
|
||||
function applyProxy(s) {
|
||||
try {
|
||||
if (s.proxyEnabled && s.proxyHost && String(s.proxyPort).trim()) {
|
||||
const cred = s.proxyUser ? `${encodeURIComponent(s.proxyUser)}:${encodeURIComponent(s.proxyPass || '')}@` : '';
|
||||
undici.setGlobalDispatcher(new undici.ProxyAgent(`http://${cred}${s.proxyHost}:${s.proxyPort}`));
|
||||
} else {
|
||||
undici.setGlobalDispatcher(new undici.Agent());
|
||||
}
|
||||
} catch { /* ungültige Proxy-Konfig ignorieren */ }
|
||||
}
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
function windowIcon() {
|
||||
const png = path.join(__dirname, 'assets', 'logo.png');
|
||||
const ico = path.join(__dirname, 'assets', 'icon.ico');
|
||||
if (fs.existsSync(ico)) return ico;
|
||||
if (fs.existsSync(png)) return png;
|
||||
return undefined; // Electron-Standard, solange kein Logo hinterlegt ist
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1100,
|
||||
height: 720,
|
||||
minWidth: 880,
|
||||
minHeight: 560,
|
||||
backgroundColor: '#1b1e24',
|
||||
title: 'AeroMC',
|
||||
icon: windowIcon(),
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
store.init(app.getPath('userData'));
|
||||
// Konten verschlüsselt speichern (Windows-DPAPI, an das Benutzerkonto gebunden)
|
||||
auth.init(app.getPath('userData'), {
|
||||
available: () => { try { return safeStorage.isEncryptionAvailable(); } catch { return false; } },
|
||||
encrypt: (s) => safeStorage.encryptString(s).toString('base64'),
|
||||
decrypt: (b) => safeStorage.decryptString(Buffer.from(b, 'base64')),
|
||||
});
|
||||
applyProxy(store.getSettings());
|
||||
createWindow();
|
||||
|
||||
// Anmelde-Dienste automatisch überwachen (sofort + alle 2 Minuten)
|
||||
setTimeout(() => checkAuthServices(), 2500);
|
||||
setInterval(() => checkAuthServices(), 120000);
|
||||
|
||||
// Anmeldung im Hintergrund auffrischen -> kein erneuter Login beim Start nötig
|
||||
const cid = store.getSettings().azureClientId || auth.DEFAULT_CLIENT_ID;
|
||||
auth.getValidAccount(cid)
|
||||
.then((acc) => {
|
||||
if (acc && mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('auth:refreshed');
|
||||
})
|
||||
.catch(() => { /* offline o. Ä. – Konto bleibt gespeichert */ });
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java-Erkennung (für Anzeige in den Einstellungen)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function detectJava(javaPath) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = javaPath && javaPath.trim() ? javaPath : 'java';
|
||||
execFile(cmd, ['-version'], (err, stdout, stderr) => {
|
||||
if (err) return resolve({ ok: false, path: cmd, version: null, error: err.message });
|
||||
// "java -version" schreibt nach stderr
|
||||
const out = (stderr || stdout || '').split('\n')[0].trim();
|
||||
resolve({ ok: true, path: cmd, version: out });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Einstellungen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ipcMain.handle('settings:get', () => store.getSettings());
|
||||
|
||||
ipcMain.handle('settings:save', (_e, patch) => { const s = store.saveSettings(patch); applyProxy(s); return s; });
|
||||
|
||||
ipcMain.handle('settings:instancesDir', () => store.getInstancesDir());
|
||||
|
||||
ipcMain.handle('java:detect', (_e, javaPath) => detectJava(javaPath));
|
||||
|
||||
// automatische Java-Suche über das System
|
||||
ipcMain.handle('java:scan', () => services.scanJava());
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Minecraft-Versionen (Mojang)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ipcMain.handle('mc:versions', () => services.getMcVersions(app.getPath('userData')));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Mods (Modrinth)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ipcMain.handle('mods:search', (_e, opts) => services.modrinthSearch(opts));
|
||||
|
||||
ipcMain.handle('mods:list', (_e, id) => store.listMods(id));
|
||||
|
||||
ipcMain.handle('mods:install', async (_e, id, project) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const gameVersion = inst.minecraft.version;
|
||||
const loader = inst.minecraft.loader;
|
||||
if (!gameVersion) return { ok: false, message: 'Instanz hat keine Minecraft-Version gesetzt.' };
|
||||
if (!loader || loader === 'vanilla') return { ok: false, message: 'Mods brauchen einen Mod-Loader (Fabric/Forge/NeoForge/Quilt).' };
|
||||
|
||||
const modsDir = store.modsDir(id);
|
||||
const done = new Set(store.listMods(id).map((m) => m.projectId));
|
||||
const added = [];
|
||||
let firstError = null;
|
||||
|
||||
// installiert ein Projekt und rekursiv seine PFLICHT-Abhängigkeiten
|
||||
async function installOne(proj, isDep) {
|
||||
if (done.has(proj.projectId)) return;
|
||||
done.add(proj.projectId);
|
||||
try {
|
||||
const res = await services.modrinthInstall({
|
||||
projectId: proj.projectId, title: proj.title, gameVersion, loader, modsDir,
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (!isDep && !firstError) firstError = res.message;
|
||||
return;
|
||||
}
|
||||
store.addMod(id, res.entry);
|
||||
added.push(res.entry);
|
||||
for (const dep of res.dependencies || []) {
|
||||
if (dep.dependency_type === 'required' && dep.project_id && !done.has(dep.project_id)) {
|
||||
const title = await services.modrinthProjectTitle(dep.project_id);
|
||||
await installOne({ projectId: dep.project_id, title }, true);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isDep && !firstError) firstError = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
await installOne(project, false);
|
||||
if (!added.length) return { ok: false, message: firstError || 'Installation fehlgeschlagen.' };
|
||||
return { ok: true, entry: added[0], added, depCount: added.length - 1 };
|
||||
});
|
||||
|
||||
// verfügbare Loader-Versionen (Fabric/Quilt) für eine MC-Version
|
||||
ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
|
||||
try { return await loaders.listLoaderVersions(loader, mcVersion); }
|
||||
catch { return []; }
|
||||
});
|
||||
|
||||
ipcMain.handle('mods:remove', (_e, id, filename) => store.removeMod(id, filename));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Microsoft-Login (Phase 3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ipcMain.handle('auth:status', () => auth.currentAccount());
|
||||
ipcMain.handle('auth:health', () => authHealth);
|
||||
ipcMain.handle('auth:recheck', () => checkAuthServices());
|
||||
ipcMain.handle('auth:accounts', () => auth.listAccounts());
|
||||
ipcMain.handle('auth:setActive', (_e, id) => auth.setActive(id));
|
||||
ipcMain.handle('auth:remove', (_e, id) => auth.removeAccount(id));
|
||||
ipcMain.handle('auth:logout', () => { auth.clearAccount(); return true; });
|
||||
|
||||
ipcMain.handle('auth:login', async () => {
|
||||
const clientId = store.getSettings().azureClientId || auth.DEFAULT_CLIENT_ID;
|
||||
try {
|
||||
const account = await auth.login(
|
||||
clientId,
|
||||
(code) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('auth:code', code);
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
return { ok: true, account };
|
||||
} catch (err) {
|
||||
return { ok: false, message: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Instanzen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ipcMain.handle('instances:list', () => store.listInstances());
|
||||
|
||||
ipcMain.handle('instances:get', (_e, id) => store.getInstance(id));
|
||||
|
||||
ipcMain.handle('instances:create', (_e, data) => store.createInstance(data));
|
||||
|
||||
ipcMain.handle('instances:update', (_e, id, patch) => store.updateInstance(id, patch));
|
||||
|
||||
ipcMain.handle('instances:delete', (_e, id) => store.deleteInstance(id));
|
||||
|
||||
ipcMain.handle('instances:duplicate', (_e, id) => store.duplicateInstance(id));
|
||||
|
||||
ipcMain.handle('instances:openFolder', (_e, id) => {
|
||||
const p = store.instancePath(id);
|
||||
if (fs.existsSync(p)) shell.openPath(p);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('instances:openGameDir', (_e, id) => {
|
||||
const p = store.gameDir(id);
|
||||
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
||||
shell.openPath(p);
|
||||
return true;
|
||||
});
|
||||
|
||||
// ---- Welten & Ressourcenpakete ----
|
||||
|
||||
function listDir(dir, dirsOnly) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true })
|
||||
.filter((e) => (dirsOnly ? e.isDirectory() : true) && !e.name.startsWith('.'))
|
||||
.map((e) => {
|
||||
let size = 0;
|
||||
try { size = fs.statSync(path.join(dir, e.name)).size; } catch { /* egal */ }
|
||||
return { name: e.name, isDir: e.isDirectory(), size };
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('instance:worlds', (_e, id) => listDir(path.join(store.gameDir(id), 'saves'), true));
|
||||
ipcMain.handle('instance:resourcepacks', (_e, id) => listDir(path.join(store.gameDir(id), 'resourcepacks'), false));
|
||||
|
||||
function safeSub(id, sub, name) {
|
||||
const base = path.join(store.gameDir(id), sub);
|
||||
const target = path.join(base, name);
|
||||
// Pfad-Ausbruch verhindern
|
||||
if (!target.startsWith(base + path.sep)) return null;
|
||||
return target;
|
||||
}
|
||||
|
||||
ipcMain.handle('instance:deleteContent', (_e, id, sub, name) => {
|
||||
const target = safeSub(id, sub, name);
|
||||
if (target && fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('instance:openSubdir', (_e, id, sub) => {
|
||||
const p = path.join(store.gameDir(id), sub);
|
||||
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
||||
shell.openPath(p);
|
||||
return true;
|
||||
});
|
||||
|
||||
// ---- Screenshots ----
|
||||
|
||||
ipcMain.handle('instance:screenshots', (_e, id) =>
|
||||
listDir(path.join(store.gameDir(id), 'screenshots'), false)
|
||||
.filter((f) => /\.(png|jpe?g|gif|webp)$/i.test(f.name))
|
||||
.sort((a, b) => b.name.localeCompare(a.name)));
|
||||
|
||||
ipcMain.handle('instance:screenshotData', (_e, id, name) => {
|
||||
const target = safeSub(id, 'screenshots', name);
|
||||
try {
|
||||
if (!target || !fs.existsSync(target)) return null;
|
||||
if (fs.statSync(target).size > 12 * 1024 * 1024) return null;
|
||||
let ext = path.extname(target).slice(1).toLowerCase();
|
||||
if (ext === 'jpg') ext = 'jpeg';
|
||||
return `data:image/${ext};base64,` + fs.readFileSync(target).toString('base64');
|
||||
} catch { return null; }
|
||||
});
|
||||
|
||||
ipcMain.handle('instance:openScreenshot', (_e, id, name) => {
|
||||
const target = safeSub(id, 'screenshots', name);
|
||||
if (target && fs.existsSync(target)) shell.openPath(target);
|
||||
return true;
|
||||
});
|
||||
|
||||
// ---- Instanz Import/Export (ZIP über Windows-Bordmittel) ----
|
||||
|
||||
function runPwsh(command, env) {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile('powershell', ['-NoProfile', '-NonInteractive', '-Command', command],
|
||||
{ env: Object.assign({}, process.env, env), windowsHide: true, maxBuffer: 64 * 1024 * 1024 },
|
||||
(err, stdout, stderr) => (err ? reject(new Error(stderr || err.message)) : resolve(stdout)));
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('instances:export', async (_e, id) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const safe = (inst.name || 'instanz').replace(/[^\w.-]+/g, '_');
|
||||
const res = await dialog.showSaveDialog(mainWindow, {
|
||||
title: 'Instanz exportieren',
|
||||
defaultPath: safe + '.zip',
|
||||
filters: [{ name: 'ZIP-Archiv', extensions: ['zip'] }],
|
||||
});
|
||||
if (res.canceled) return { ok: false, canceled: true };
|
||||
try {
|
||||
if (fs.existsSync(res.filePath)) fs.unlinkSync(res.filePath);
|
||||
await runPwsh("Compress-Archive -Path (Join-Path $env:VN_SRC '*') -DestinationPath $env:VN_DST -Force",
|
||||
{ VN_SRC: store.instancePath(id), VN_DST: res.filePath });
|
||||
return { ok: true, path: res.filePath };
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Export fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Modpack-Import (Modrinth .mrpack) ----
|
||||
|
||||
async function importMrpack(buf, zip) {
|
||||
const raw = forge.readZipEntry(buf, zip['modrinth.index.json']).toString('utf8').replace(/^/, '');
|
||||
const index = JSON.parse(raw);
|
||||
const deps = index.dependencies || {};
|
||||
let loader = 'vanilla', loaderVersion = '';
|
||||
if (deps['fabric-loader']) { loader = 'fabric'; loaderVersion = deps['fabric-loader']; }
|
||||
else if (deps['quilt-loader']) { loader = 'quilt'; loaderVersion = deps['quilt-loader']; }
|
||||
else if (deps.forge) { loader = 'forge'; loaderVersion = deps.forge; }
|
||||
else if (deps.neoforge) { loader = 'neoforge'; loaderVersion = deps.neoforge; }
|
||||
|
||||
const created = store.createInstance({
|
||||
name: index.name || 'Modpack',
|
||||
minecraft: { version: deps.minecraft || '', loader, loaderVersion },
|
||||
});
|
||||
const gameDir = store.gameDir(created.id);
|
||||
const inside = (dest) => dest.startsWith(gameDir + path.sep);
|
||||
|
||||
const files = index.files || [];
|
||||
let i = 0;
|
||||
for (const f of files) {
|
||||
i++;
|
||||
if (f.env && f.env.client === 'unsupported') continue;
|
||||
const url = f.downloads && f.downloads[0];
|
||||
if (!url || !f.path) continue;
|
||||
const dest = path.join(gameDir, f.path.replace(/\//g, path.sep));
|
||||
if (!inside(dest)) continue;
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('launch:progress', { phase: 'modpack', current: i, total: files.length, detail: 'Modpack-Dateien' });
|
||||
}
|
||||
const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher/0.11' } });
|
||||
if (!res.ok) continue;
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
||||
}
|
||||
|
||||
// overrides einspielen
|
||||
for (const prefix of ['overrides/', 'client-overrides/']) {
|
||||
for (const name of Object.keys(zip)) {
|
||||
if (name.startsWith(prefix) && !name.endsWith('/')) {
|
||||
const dest = path.join(gameDir, name.slice(prefix.length).replace(/\//g, path.sep));
|
||||
if (!inside(dest)) continue;
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, forge.readZipEntry(buf, zip[name]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ok: true, instance: store.getInstance(created.id), fileCount: files.length };
|
||||
}
|
||||
|
||||
ipcMain.handle('modpack:import', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Modpack importieren',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Modpacks', extensions: ['mrpack', 'zip'] }],
|
||||
});
|
||||
if (res.canceled) return { ok: false, canceled: true };
|
||||
try {
|
||||
const buf = fs.readFileSync(res.filePaths[0]);
|
||||
const zip = forge.readZipIndex(buf);
|
||||
if (zip['modrinth.index.json']) return await importMrpack(buf, zip);
|
||||
if (zip['manifest.json']) {
|
||||
return { ok: false, message: 'CurseForge-Modpacks brauchen einen CurseForge-API-Key und werden noch nicht unterstützt. Bitte ein Modrinth-.mrpack verwenden.' };
|
||||
}
|
||||
return { ok: false, message: 'Unbekanntes Modpack-Format (keine modrinth.index.json gefunden).' };
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Modpack-Import fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('instances:import', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Instanz importieren',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'ZIP-Archiv', extensions: ['zip'] }],
|
||||
});
|
||||
if (res.canceled) return { ok: false, canceled: true };
|
||||
const tmp = path.join(app.getPath('temp'), 'vn-import-' + Date.now());
|
||||
try {
|
||||
fs.mkdirSync(tmp, { recursive: true });
|
||||
await runPwsh('Expand-Archive -Path $env:VN_SRC -DestinationPath $env:VN_DST -Force',
|
||||
{ VN_SRC: res.filePaths[0], VN_DST: tmp });
|
||||
|
||||
// instance.json finden (auch in Unterordner)
|
||||
let base = tmp;
|
||||
if (!fs.existsSync(path.join(base, 'instance.json'))) {
|
||||
const sub = fs.readdirSync(tmp).map((n) => path.join(tmp, n))
|
||||
.find((p) => { try { return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'instance.json')); } catch { return false; } });
|
||||
if (sub) base = sub;
|
||||
}
|
||||
if (!fs.existsSync(path.join(base, 'instance.json'))) {
|
||||
throw new Error('Keine gültige Instanz im Archiv (instance.json fehlt).');
|
||||
}
|
||||
const imported = JSON.parse(fs.readFileSync(path.join(base, 'instance.json'), 'utf8'));
|
||||
const created = store.createInstance({
|
||||
name: (imported.name || 'Import') + ' (Import)',
|
||||
icon: imported.icon || 'grass',
|
||||
notes: imported.notes || '',
|
||||
minecraft: imported.minecraft || {},
|
||||
});
|
||||
if (imported.java) store.updateInstance(created.id, { java: imported.java });
|
||||
|
||||
const mcSrc = path.join(base, 'minecraft');
|
||||
if (fs.existsSync(mcSrc)) fs.cpSync(mcSrc, store.gameDir(created.id), { recursive: true });
|
||||
const modsSrc = path.join(base, 'mods.json');
|
||||
if (fs.existsSync(modsSrc)) fs.copyFileSync(modsSrc, path.join(store.instancePath(created.id), 'mods.json'));
|
||||
if (imported.customImage && fs.existsSync(path.join(base, imported.customImage))) {
|
||||
fs.copyFileSync(path.join(base, imported.customImage), path.join(store.instancePath(created.id), imported.customImage));
|
||||
store.updateInstance(created.id, { customImage: imported.customImage });
|
||||
}
|
||||
return { ok: true, instance: store.getInstance(created.id) };
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Import fehlgeschlagen: ' + err.message };
|
||||
} finally {
|
||||
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 2: echter Vanilla-Download & -Start (Start selbst ist an Login gebunden).
|
||||
ipcMain.handle('instances:launch', async (_e, id, accountId) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
if (!inst.minecraft.version) {
|
||||
return { ok: false, message: 'Bitte zuerst eine Minecraft-Version wählen (Bearbeiten).' };
|
||||
}
|
||||
const settings = store.getSettings();
|
||||
const opts = {
|
||||
versionId: inst.minecraft.version,
|
||||
loader: inst.minecraft.loader || 'vanilla',
|
||||
loaderVersion: inst.minecraft.loaderVersion || '',
|
||||
gameDir: store.gameDir(id),
|
||||
sharedDir: path.join(app.getPath('userData'), 'shared'),
|
||||
userDataDir: app.getPath('userData'),
|
||||
javaPath: inst.java.path || settings.javaPath || '',
|
||||
minMemMb: inst.java.minMemMb || settings.defaultMinMemMb,
|
||||
maxMemMb: inst.java.maxMemMb || settings.defaultMaxMemMb,
|
||||
extraArgs: inst.java.extraArgs || '',
|
||||
preLaunchCommand: inst.preLaunchCommand || settings.globalPreLaunch || '',
|
||||
postExitCommand: inst.postExitCommand || settings.globalPostExit || '',
|
||||
wrapperCommand: settings.wrapperCommand || '',
|
||||
auth: null,
|
||||
};
|
||||
|
||||
// eingeloggtes Microsoft-Konto verwenden (gewähltes oder aktives) -> echter Start
|
||||
const clientId = settings.azureClientId || auth.DEFAULT_CLIENT_ID;
|
||||
const account = accountId
|
||||
? await auth.getValidAccountById(clientId, accountId)
|
||||
: await auth.getValidAccount(clientId);
|
||||
if (account && account.profile) {
|
||||
opts.auth = {
|
||||
name: account.profile.name,
|
||||
uuid: account.profile.id,
|
||||
accessToken: account.mcAccessToken,
|
||||
userType: 'msa',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let launchStart = 0;
|
||||
const res = await launcher.prepareAndLaunch(opts, (evt) => {
|
||||
if (evt.phase === 'launch') launchStart = Date.now();
|
||||
if (evt.phase === 'exit' && launchStart) {
|
||||
const secs = Math.max(0, Math.round((Date.now() - launchStart) / 1000));
|
||||
const cur = store.getInstance(id);
|
||||
if (cur) store.updateInstance(id, { totalPlaySeconds: (cur.totalPlaySeconds || 0) + secs });
|
||||
}
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('launch:progress', Object.assign({ id }, evt));
|
||||
}
|
||||
});
|
||||
if (res.ok) store.updateInstance(id, { lastPlayed: new Date().toISOString() });
|
||||
return res;
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Start fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Dialoge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ipcMain.handle('dialog:pickFolder', async (_e, title) => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: title || 'Ordner wählen',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
});
|
||||
return res.canceled ? null : res.filePaths[0];
|
||||
});
|
||||
|
||||
ipcMain.handle('dialog:pickImage', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Instanz-Bild wählen',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Bilder', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp'] }],
|
||||
});
|
||||
return res.canceled ? null : res.filePaths[0];
|
||||
});
|
||||
|
||||
// eigenes Bild einer Instanz zuweisen / entfernen
|
||||
ipcMain.handle('instances:setImage', (_e, id, sourcePath) => {
|
||||
store.setCustomImage(id, sourcePath);
|
||||
return { meta: store.getInstance(id), iconData: store.iconDataUri(id) };
|
||||
});
|
||||
ipcMain.handle('instances:clearImage', (_e, id) => store.clearCustomImage(id));
|
||||
ipcMain.handle('instances:icon', (_e, id) => store.iconDataUri(id));
|
||||
|
||||
// beliebige Bilddatei als data:-URI (für Vorschau vor dem Speichern)
|
||||
ipcMain.handle('file:dataUri', (_e, filePath) => {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size > 8 * 1024 * 1024) return null; // >8 MB nicht als Vorschau
|
||||
let ext = path.extname(filePath).slice(1).toLowerCase();
|
||||
if (ext === 'jpg') ext = 'jpeg';
|
||||
const buf = fs.readFileSync(filePath);
|
||||
return `data:image/${ext};base64,` + buf.toString('base64');
|
||||
} catch { return null; }
|
||||
});
|
||||
|
||||
ipcMain.handle('dialog:pickJava', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'java.exe wählen',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'Java', extensions: ['exe'] }],
|
||||
});
|
||||
return res.canceled ? null : res.filePaths[0];
|
||||
});
|
||||
|
||||
ipcMain.handle('app:openExternal', (_e, url) => {
|
||||
if (typeof url === 'string' && /^https?:\/\//.test(url)) shell.openExternal(url);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('app:info', () => ({
|
||||
version: app.getVersion(),
|
||||
electron: process.versions.electron,
|
||||
node: process.versions.node,
|
||||
userData: app.getPath('userData'),
|
||||
}));
|
||||
Reference in New Issue
Block a user