Upload via GUI (45 Dateien)
This commit is contained in:
+169
-3
@@ -6,11 +6,11 @@
|
||||
* (Vanilla-Start, Microsoft-Login und Mod-Loader folgen in späteren Phasen.)
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification, Tray, Menu, nativeImage } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification, Tray, Menu, nativeImage, clipboard } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { execFile } = require('child_process');
|
||||
const { execFile, spawn } = require('child_process');
|
||||
const pkg = require('../package.json');
|
||||
const store = require('./store');
|
||||
const services = require('./services');
|
||||
@@ -556,6 +556,111 @@ ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
|
||||
catch { return []; }
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OptiFine – kein offizielles Download-API (Downloads laufen bei OptiFine
|
||||
// bewusst über eine Werbeseite, das umgehen wir nicht). Stattdessen: Nutzer
|
||||
// lädt den Installer selbst von optifine.net, AeroMC bereitet Vanilla vor,
|
||||
// startet den echten Installer sichtbar mit dem passenden Zielordner in der
|
||||
// Zwischenablage und übernimmt danach die neu entstandene Profil-Version.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const optifineSetupState = new Map(); // instanceId -> { sharedDir, before: Set<string> }
|
||||
|
||||
function scanOptifineVersions(sharedDir, mcVersion) {
|
||||
const versionsDir = path.join(sharedDir, 'versions');
|
||||
if (!fs.existsSync(versionsDir)) return [];
|
||||
const prefix = String(mcVersion || '');
|
||||
return fs.readdirSync(versionsDir, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => e.name)
|
||||
.filter((name) => /optifine/i.test(name) && (!prefix || name.startsWith(prefix)))
|
||||
.filter((name) => fs.existsSync(path.join(versionsDir, name, name + '.json')))
|
||||
.sort();
|
||||
}
|
||||
|
||||
ipcMain.handle('optifine:find', (_e, mcVersion) => {
|
||||
const sharedDir = path.join(app.getPath('userData'), 'shared');
|
||||
return scanOptifineVersions(sharedDir, mcVersion);
|
||||
});
|
||||
|
||||
ipcMain.handle('optifine:setup', async (_e, id) => {
|
||||
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.' };
|
||||
|
||||
const picked = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'OptiFine-Installer wählen (zuvor von optifine.net heruntergeladen)',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'OptiFine-Installer', extensions: ['jar'] }],
|
||||
});
|
||||
if (picked.canceled || !picked.filePaths.length) return { ok: false, canceled: true };
|
||||
const installerPath = picked.filePaths[0];
|
||||
|
||||
const settings = store.getSettings();
|
||||
const sharedDir = path.join(app.getPath('userData'), 'shared');
|
||||
const opts = {
|
||||
versionId: inst.minecraft.version,
|
||||
loader: 'vanilla',
|
||||
loaderVersion: '',
|
||||
gameDir: store.gameDir(id),
|
||||
sharedDir,
|
||||
userDataDir: app.getPath('userData'),
|
||||
javaPath: inst.java.path || settings.javaPath || '',
|
||||
minMemMb: inst.java.minMemMb || settings.defaultMinMemMb,
|
||||
maxMemMb: inst.java.maxMemMb || settings.defaultMaxMemMb,
|
||||
extraArgs: '',
|
||||
auth: null,
|
||||
ensureJava: settings.autoJava === false ? null : (major, onProgress) => javadl.ensureJava(major, onProgress),
|
||||
};
|
||||
|
||||
// Vanilla-Dateien sicherstellen (kein echter Start, da kein Konto übergeben wird)
|
||||
let prep;
|
||||
try {
|
||||
prep = await launcher.prepareAndLaunch(opts, (evt) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('launch:progress', Object.assign({ id }, evt));
|
||||
});
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Vorbereitung fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
if (!prep.needAuth && !prep.ok) return { ok: false, message: prep.message || 'Vorbereitung fehlgeschlagen.' };
|
||||
const javaExe = (prep.java && prep.java.path) || opts.javaPath || 'java';
|
||||
|
||||
optifineSetupState.set(id, {
|
||||
sharedDir,
|
||||
before: new Set(scanOptifineVersions(sharedDir, inst.minecraft.version)),
|
||||
});
|
||||
|
||||
try {
|
||||
const child = spawn(javaExe, ['-jar', installerPath], { detached: true, stdio: 'ignore' });
|
||||
child.unref();
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Installer konnte nicht gestartet werden: ' + err.message };
|
||||
}
|
||||
try { clipboard.writeText(sharedDir); } catch { /* egal */ }
|
||||
|
||||
return { ok: true, sharedDir };
|
||||
});
|
||||
|
||||
ipcMain.handle('optifine:confirm', (_e, id) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const state = optifineSetupState.get(id);
|
||||
const sharedDir = (state && state.sharedDir) || path.join(app.getPath('userData'), 'shared');
|
||||
const nachher = scanOptifineVersions(sharedDir, inst.minecraft.version);
|
||||
const vorher = (state && state.before) || new Set();
|
||||
const neu = nachher.filter((v) => !vorher.has(v));
|
||||
const treffer = neu[0] || nachher[0];
|
||||
optifineSetupState.delete(id);
|
||||
if (!treffer) {
|
||||
return {
|
||||
ok: false,
|
||||
message: 'Keine OptiFine-Version für ' + (inst.minecraft.version || '?') + ' gefunden. '
|
||||
+ 'Wurde die Installation im OptiFine-Fenster abgeschlossen und dabei der kopierte Ordner ausgewählt?',
|
||||
};
|
||||
}
|
||||
return { ok: true, versionId: treffer };
|
||||
});
|
||||
|
||||
ipcMain.handle('mods:remove', (_e, id, filename, art) => store.removeMod(id, filename, art || 'mod'));
|
||||
// prüft, ob andere installierte Mods diesen hier als Pflicht-Abhängigkeit brauchen
|
||||
ipcMain.handle('mods:dependents', (_e, id, filename, art) => store.findDependents(id, filename, art || 'mod'));
|
||||
@@ -1253,8 +1358,13 @@ async function importCurseForge(buf, zip, zugang) {
|
||||
}
|
||||
|
||||
ipcMain.handle('modpack:search', async (_e, opts) => {
|
||||
const o = opts || {};
|
||||
try {
|
||||
const q = Object.assign({}, opts || {}, { art: 'modpack' });
|
||||
if (o.source === 'curseforge') {
|
||||
const zugang = services.curseforgeZugang(store.getSettings());
|
||||
return await services.curseforgeSearch(zugang, Object.assign({}, o, { art: 'modpack' }));
|
||||
}
|
||||
const q = Object.assign({}, o, { art: 'modpack' });
|
||||
return await services.modrinthSearch(q);
|
||||
} catch (err) {
|
||||
return { total: 0, hits: [], error: err.message };
|
||||
@@ -1313,6 +1423,52 @@ ipcMain.handle('modpack:installFromModrinth', async (_e, project) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('modpack:installFromCurseForge', async (_e, project) => {
|
||||
try {
|
||||
const zugang = services.curseforgeZugang(store.getSettings());
|
||||
if (!zugang) {
|
||||
return { ok: false, needsKey: true,
|
||||
message: 'CurseForge-Modpacks brauchen einen Zugang. Trage in den Netzwerk-Einstellungen entweder deinen eigenen API-Key ein (kostenlos über console.curseforge.com) oder die Adresse einer CurseForge-Weiterleitung.' };
|
||||
}
|
||||
const rawId = String((project && (project.cfId || project.projectId || project.id)) || '').replace(/^cf-/, '');
|
||||
const modId = Number(rawId);
|
||||
if (!modId) return { ok: false, message: 'Kein Modpack gewählt.' };
|
||||
const gameVersion = (project && project.gameVersion) || '';
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('launch:progress', {
|
||||
phase: 'modpack', detail: 'Modpack-Version wird ermittelt …',
|
||||
});
|
||||
}
|
||||
const file = await services.curseforgeResolveModpackFile(zugang, modId, gameVersion);
|
||||
if (!file || !file.url) {
|
||||
return {
|
||||
ok: false,
|
||||
message: gameVersion
|
||||
? ('Keine Modpack-Version für Minecraft ' + gameVersion + ' gefunden.')
|
||||
: 'Keine passende Modpack-Datei gefunden.',
|
||||
};
|
||||
}
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('launch:progress', {
|
||||
phase: 'modpack', detail: 'Lade ' + file.filename + ' …',
|
||||
});
|
||||
}
|
||||
const dlUrl = zugang.downloadAdresse ? zugang.downloadAdresse(file.url) : file.url;
|
||||
const buf = await services.downloadBuffer(dlUrl, zugang.downloadKopfzeilen);
|
||||
const zip = forge.readZipIndex(buf);
|
||||
if (!zip['manifest.json']) {
|
||||
return { ok: false, message: 'Die geladene Datei enthält kein manifest.json – kein gültiges CurseForge-Modpack.' };
|
||||
}
|
||||
const result = await importCurseForge(buf, zip, zugang);
|
||||
if (result && result.ok && result.instance) {
|
||||
notify('Modpack installiert', `${result.instance.name} ist einsatzbereit.`);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Modpack-Installation fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Kern des Modpack-Datei-Imports, unabhängig davon ob der Pfad aus einem
|
||||
* Dateidialog oder per Drag&Drop aufs Fenster kommt.
|
||||
@@ -2081,3 +2237,13 @@ ipcMain.handle('app:info', () => ({
|
||||
homepage: pkg.homepage || '',
|
||||
launcherWebsite: pkg.launcherWebsite || '',
|
||||
}));
|
||||
|
||||
// Lizenztext für die Info-Seite in den Einstellungen. Die LICENSE-Datei liegt
|
||||
// im App-Root und wird über "files" im electron-builder-Config mitgepackt.
|
||||
ipcMain.handle('app:license', () => {
|
||||
try {
|
||||
return fs.readFileSync(path.join(app.getAppPath(), 'LICENSE'), 'utf8');
|
||||
} catch {
|
||||
return 'Lizenztext konnte nicht geladen werden.';
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user