Upload via GUI (39 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-02 20:18:36 +02:00
parent 176d20ae7b
commit 9e601b1962
8 changed files with 475 additions and 95 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "aeromc", "name": "aeromc",
"version": "1.0.1-beta", "version": "1.0.4-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "aeromc", "name": "aeromc",
"version": "1.0.1-beta", "version": "1.0.4-beta",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",

View File

@@ -1,7 +1,7 @@
{ {
"name": "aeromc", "name": "aeromc",
"productName": "AeroMC", "productName": "AeroMC",
"version": "1.0.3", "version": "1.0.4",
"description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)", "description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
"homepage": "https://m-viper.de", "homepage": "https://m-viper.de",
"main": "src/main.js", "main": "src/main.js",

View File

@@ -51,6 +51,7 @@
<button id="p-mods" class="sbtn">Mods anzeigen</button> <button id="p-mods" class="sbtn">Mods anzeigen</button>
<button id="p-worlds" class="sbtn">Welten verwalten</button> <button id="p-worlds" class="sbtn">Welten verwalten</button>
<button id="p-screens" class="sbtn">Bildschirmfotos verwalten</button> <button id="p-screens" class="sbtn">Bildschirmfotos verwalten</button>
<button id="p-modpack" class="sbtn hidden">Modpack aktualisieren</button>
<button id="p-logs" class="sbtn">Logdateien ansehen</button> <button id="p-logs" class="sbtn">Logdateien ansehen</button>
<button id="p-servers" class="sbtn">Server verwalten</button> <button id="p-servers" class="sbtn">Server verwalten</button>
<div class="hr"></div> <div class="hr"></div>
@@ -157,6 +158,17 @@
</label> </label>
</div> </div>
<small id="f-mem-hint" class="hint"></small> <small id="f-mem-hint" class="hint"></small>
<label class="field">
<span>Java (java.exe)</span>
<div class="path-row">
<input id="f-java" type="text" placeholder="leer = globale Einstellung bzw. automatisch" />
<button id="f-java-pick" class="btn" type="button">Wählen…</button>
</div>
<div class="path-row">
<select id="f-java-detected"><option value=""> gefundene Java-Installationen </option></select>
</div>
<small id="f-java-hint" class="hint"></small>
</label>
<label class="field"> <label class="field">
<span>Zusätzliche JVM-Argumente</span> <span>Zusätzliche JVM-Argumente</span>
<input id="f-extra-args" type="text" placeholder="z. B. -XX:+UseG1GC" /> <input id="f-extra-args" type="text" placeholder="z. B. -XX:+UseG1GC" />
@@ -573,6 +585,11 @@
<div id="m-warn" class="mods-warn hidden"></div> <div id="m-warn" class="mods-warn hidden"></div>
<div class="mods-search"> <div class="mods-search">
<select id="m-art" title="Was soll gesucht werden?">
<option value="mod">Mods</option>
<option value="resourcepack">Ressourcenpakete</option>
<option value="shader">Shader</option>
</select>
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" /> <input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
<button id="m-search-btn" class="btn btn-primary">Suchen</button> <button id="m-search-btn" class="btn btn-primary">Suchen</button>
</div> </div>
@@ -720,6 +737,26 @@
</div> </div>
</div> </div>
<!-- ===================== Dialog: Modpack aktualisieren ===================== -->
<div id="modal-modpack" class="modal hidden">
<div class="modal-card modal-sm">
<div class="modal-head">
<h3>Modpack aktualisieren</h3>
<button class="modal-close" data-close></button>
</div>
<div class="modal-body">
<p id="mp-status" class="hint">Suche …</p>
<div id="mp-kern" class="crash-kern hidden"></div>
<p class="hint">Welten, Bildschirmfotos und deine Optionen bleiben unangetastet. Mods und Konfigurationsdateien des Pakets werden ersetzt; was die neue Fassung nicht mehr enthält, wird entfernt.</p>
</div>
<div class="modal-foot">
<div class="spacer"></div>
<button class="btn" data-close>Schließen</button>
<button id="mp-do" class="btn btn-primary hidden">Jetzt aktualisieren</button>
</div>
</div>
</div>
<!-- ===================== Dialog: Serverliste ===================== --> <!-- ===================== Dialog: Serverliste ===================== -->
<div id="modal-servers" class="modal hidden"> <div id="modal-servers" class="modal hidden">
<div class="modal-card modal-lg"> <div class="modal-card modal-lg">

View File

@@ -274,6 +274,8 @@ ipcMain.handle('java:detect', (_e, javaPath) => detectJava(javaPath));
// automatische Java-Suche über das System // automatische Java-Suche über das System
ipcMain.handle('java:scan', () => services.scanJava(false)); // Knopf sucht immer frisch ipcMain.handle('java:scan', () => services.scanJava(false)); // Knopf sucht immer frisch
// aus dem Zwischenspeicher für Dialoge, die nur die Liste anzeigen wollen
ipcMain.handle('java:list', () => services.scanJava(true));
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// IPC Minecraft-Versionen (Mojang) // IPC Minecraft-Versionen (Mojang)
@@ -287,18 +289,23 @@ ipcMain.handle('mc:versions', () => services.getMcVersions(app.getPath('userData
ipcMain.handle('mods:search', (_e, opts) => services.modrinthSearch(opts)); ipcMain.handle('mods:search', (_e, opts) => services.modrinthSearch(opts));
ipcMain.handle('mods:list', (_e, id) => store.listMods(id)); ipcMain.handle('mods:list', (_e, id, art) => store.listMods(id, art || 'mod'));
ipcMain.handle('mods:install', async (_e, id, project) => { ipcMain.handle('mods:install', async (_e, id, project, art) => {
const inst = store.getInstance(id); const inst = store.getInstance(id);
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' }; if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
const typ = services.inhaltsArt(art || 'mod');
const gameVersion = inst.minecraft.version; const gameVersion = inst.minecraft.version;
const loader = inst.minecraft.loader; const loader = inst.minecraft.loader;
if (!gameVersion) return { ok: false, message: 'Instanz hat keine Minecraft-Version gesetzt.' }; 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).' }; // Nur Mods brauchen einen Loader Ressourcenpakete und Shader laufen auch
// in einer Vanilla-Instanz.
if (typ.loaderNoetig && (!loader || loader === 'vanilla')) {
return { ok: false, message: 'Mods brauchen einen Mod-Loader (Fabric/Forge/NeoForge/Quilt).' };
}
const modsDir = store.modsDir(id); const modsDir = store.inhaltsDir(id, art || 'mod');
const done = new Set(store.listMods(id).map((m) => m.projectId)); const done = new Set(store.listMods(id, art || 'mod').map((m) => m.projectId));
const added = []; const added = [];
let firstError = null; let firstError = null;
@@ -308,13 +315,13 @@ ipcMain.handle('mods:install', async (_e, id, project) => {
done.add(proj.projectId); done.add(proj.projectId);
try { try {
const res = await services.modrinthInstall({ const res = await services.modrinthInstall({
projectId: proj.projectId, title: proj.title, gameVersion, loader, modsDir, projectId: proj.projectId, title: proj.title, gameVersion, loader, modsDir, art: art || 'mod',
}); });
if (!res.ok) { if (!res.ok) {
if (!isDep && !firstError) firstError = res.message; if (!isDep && !firstError) firstError = res.message;
return; return;
} }
store.addMod(id, res.entry); store.addMod(id, res.entry, art || 'mod');
added.push(res.entry); added.push(res.entry);
for (const dep of res.dependencies || []) { for (const dep of res.dependencies || []) {
if (dep.dependency_type === 'required' && dep.project_id && !done.has(dep.project_id)) { if (dep.dependency_type === 'required' && dep.project_id && !done.has(dep.project_id)) {
@@ -338,7 +345,7 @@ ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
catch { return []; } catch { return []; }
}); });
ipcMain.handle('mods:remove', (_e, id, filename) => store.removeMod(id, filename)); ipcMain.handle('mods:remove', (_e, id, filename, art) => store.removeMod(id, filename, art || 'mod'));
// Vorhandener Arbeitsspeicher Grundlage für die Prüfung der RAM-Angaben // Vorhandener Arbeitsspeicher Grundlage für die Prüfung der RAM-Angaben
function gesamtSpeicherMb() { function gesamtSpeicherMb() {
@@ -421,28 +428,29 @@ ipcMain.handle('crash:openFolder', (_e, id) => {
}); });
// Mod ab-/anschalten statt löschen (Datei wird nur umbenannt) // Mod ab-/anschalten statt löschen (Datei wird nur umbenannt)
ipcMain.handle('mods:setEnabled', (_e, id, filename, enabled) => { ipcMain.handle('mods:setEnabled', (_e, id, filename, enabled, art) => {
const res = store.setModEnabled(id, filename, !!enabled); const res = store.setModEnabled(id, filename, !!enabled, art || 'mod');
if (res.ok) return res; if (res.ok) return res;
const ordner = services.inhaltsArt(art || 'mod').ordner;
const gruende = { const gruende = {
'not-found': 'Die Datei liegt nicht mehr im mods-Ordner.', 'not-found': `Die Datei liegt nicht mehr im ${ordner}-Ordner.`,
'both-exist': 'Es liegen beide Fassungen im mods-Ordner (an und abgeschaltet). Bitte eine davon von Hand entfernen.', 'both-exist': `Es liegen beide Fassungen im ${ordner}-Ordner (an und abgeschaltet). Bitte eine davon von Hand entfernen.`,
'bad-name': 'Ungültiger Dateiname.', 'bad-name': 'Ungültiger Dateiname.',
}; };
return { ok: false, message: gruende[res.reason] || res.reason }; return { ok: false, message: gruende[res.reason] || res.reason };
}); });
// sucht für die installierten Mods nach neueren Dateien (lädt noch nichts) // sucht für die installierten Mods nach neueren Dateien (lädt noch nichts)
ipcMain.handle('mods:checkUpdates', async (_e, id) => { ipcMain.handle('mods:checkUpdates', async (_e, id, art) => {
const inst = store.getInstance(id); const inst = store.getInstance(id);
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' }; if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
const gameVersion = inst.minecraft.version; const gameVersion = inst.minecraft.version;
const loader = inst.minecraft.loader; const loader = inst.minecraft.loader;
if (!gameVersion) return { ok: false, message: 'Instanz hat keine Minecraft-Version gesetzt.' }; if (!gameVersion) return { ok: false, message: 'Instanz hat keine Minecraft-Version gesetzt.' };
try { try {
const res = await services.modrinthFindUpdates(store.listMods(id), gameVersion, loader, (p) => { const res = await services.modrinthFindUpdates(store.listMods(id, art || 'mod'), gameVersion, loader, (p) => {
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('mods:updateProgress', p); if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('mods:updateProgress', p);
}); }, art || 'mod');
return { ok: true, ...res }; return { ok: true, ...res };
} catch (err) { } catch (err) {
return { ok: false, message: 'Update-Suche fehlgeschlagen: ' + err.message }; return { ok: false, message: 'Update-Suche fehlgeschlagen: ' + err.message };
@@ -450,28 +458,29 @@ ipcMain.handle('mods:checkUpdates', async (_e, id) => {
}); });
// lädt die gewählten Aktualisierungen und ersetzt die alten Dateien // lädt die gewählten Aktualisierungen und ersetzt die alten Dateien
ipcMain.handle('mods:applyUpdates', async (_e, id, projectIds) => { ipcMain.handle('mods:applyUpdates', async (_e, id, projectIds, art) => {
const inst = store.getInstance(id); const inst = store.getInstance(id);
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' }; if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
const gameVersion = inst.minecraft.version; const gameVersion = inst.minecraft.version;
const loader = inst.minecraft.loader; const loader = inst.minecraft.loader;
const modsDir = store.modsDir(id); const sorte = art || 'mod';
const modsDir = store.inhaltsDir(id, sorte);
const gewuenscht = new Set(projectIds || []); const gewuenscht = new Set(projectIds || []);
const erneuert = []; const erneuert = [];
const fehler = []; const fehler = [];
for (const mod of store.listMods(id)) { for (const mod of store.listMods(id, sorte)) {
if (!mod.projectId || !gewuenscht.has(mod.projectId)) continue; if (!mod.projectId || !gewuenscht.has(mod.projectId)) continue;
try { try {
const res = await services.modrinthInstall({ const res = await services.modrinthInstall({
projectId: mod.projectId, title: mod.title, gameVersion, loader, modsDir, projectId: mod.projectId, title: mod.title, gameVersion, loader, modsDir, art: sorte,
}); });
if (!res.ok) { fehler.push({ title: mod.title, message: res.message }); continue; } if (!res.ok) { fehler.push({ title: mod.title, message: res.message }); continue; }
store.addMod(id, res.entry); store.addMod(id, res.entry, sorte);
// alte Datei erst weg, wenn die neue liegt und nie, wenn sie gleich heißt // alte Datei erst weg, wenn die neue liegt und nie, wenn sie gleich heißt
if (mod.filename && mod.filename !== res.entry.filename) store.removeMod(id, mod.filename); if (mod.filename && mod.filename !== res.entry.filename) store.removeMod(id, mod.filename, sorte);
// war die Mod abgeschaltet, bleibt sie es auch nach dem Update // war der Eintrag abgeschaltet, bleibt er es auch nach dem Update
if (mod.enabled === false) store.setModEnabled(id, res.entry.filename, false); if (mod.enabled === false) store.setModEnabled(id, res.entry.filename, false, sorte);
erneuert.push({ title: mod.title, von: mod.versionNumber || '?', auf: res.entry.versionNumber }); erneuert.push({ title: mod.title, von: mod.versionNumber || '?', auf: res.entry.versionNumber });
} catch (err) { } catch (err) {
fehler.push({ title: mod.title, message: err.message }); fehler.push({ title: mod.title, message: err.message });
@@ -698,38 +707,203 @@ async function importMrpack(buf, zip) {
const gameDir = store.gameDir(created.id); const gameDir = store.gameDir(created.id);
const inside = (dest) => dest.startsWith(gameDir + path.sep); const inside = (dest) => dest.startsWith(gameDir + path.sep);
const files = index.files || []; const eingespielt = await spieleMrpackEin(buf, zip, index, gameDir, { schonePlayerDaten: false });
// Herkunft merken, damit sich das Paket später aktualisieren lässt
const herkunft = await modpackHerkunft(buf, index);
store.updateInstance(created.id, { modpack: Object.assign(herkunft, { dateien: eingespielt.dateien }) });
return { ok: true, instance: store.getInstance(created.id), fileCount: eingespielt.dateien.length };
}
/*
* Spielt ein .mrpack in ein Spielverzeichnis ein.
* schonePlayerDaten: bei einer Aktualisierung bleiben Welten, Bildschirmfotos
* und die Optionen unangetastet sonst wären die Einstellungen des Spielers weg.
* -> { dateien: [relative Pfade] }
*/
const SPIELERDATEN = [/^saves\//i, /^screenshots\//i, /^logs\//i, /^options\.txt$/i, /^servers\.dat$/i];
async function spieleMrpackEin(buf, zip, index, gameDir, { schonePlayerDaten }) {
const inside = (dest) => dest.startsWith(gameDir + path.sep);
const geschuetzt = (rel) => schonePlayerDaten && SPIELERDATEN.some((r) => r.test(rel));
const dateien = [];
const files = (index.files || []).filter((f) => !(f.env && f.env.client === 'unsupported'));
let i = 0; let i = 0;
for (const f of files) { for (const f of files) {
i++; i++;
if (f.env && f.env.client === 'unsupported') continue;
const url = f.downloads && f.downloads[0]; const url = f.downloads && f.downloads[0];
if (!url || !f.path) continue; if (!url || !f.path) continue;
const dest = path.join(gameDir, f.path.replace(/\//g, path.sep)); const rel = String(f.path).replace(/\\/g, '/');
if (geschuetzt(rel)) continue;
const dest = path.join(gameDir, rel.replace(/\//g, path.sep));
if (!inside(dest)) continue; if (!inside(dest)) continue;
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('launch:progress', { phase: 'modpack', current: i, total: files.length, detail: 'Modpack-Dateien' }); 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' } }); try {
if (!res.ok) continue; const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
fs.mkdirSync(path.dirname(dest), { recursive: true }); if (!res.ok) continue;
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer())); fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
dateien.push(rel);
} catch { /* einzelne Datei überspringen */ }
} }
// overrides einspielen // overrides einspielen
for (const prefix of ['overrides/', 'client-overrides/']) { for (const prefix of ['overrides/', 'client-overrides/']) {
for (const name of Object.keys(zip)) { for (const name of Object.keys(zip)) {
if (name.startsWith(prefix) && !name.endsWith('/')) { if (!name.startsWith(prefix) || name.endsWith('/')) continue;
const dest = path.join(gameDir, name.slice(prefix.length).replace(/\//g, path.sep)); const rel = name.slice(prefix.length);
if (!inside(dest)) continue; if (geschuetzt(rel)) continue;
fs.mkdirSync(path.dirname(dest), { recursive: true }); const dest = path.join(gameDir, rel.replace(/\//g, path.sep));
fs.writeFileSync(dest, forge.readZipEntry(buf, zip[name])); if (!inside(dest)) continue;
} fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, forge.readZipEntry(buf, zip[name]));
dateien.push(rel);
} }
} }
return { ok: true, instance: store.getInstance(created.id), fileCount: files.length }; return { dateien };
} }
/*
* Woher stammt das Paket? Modrinth kennt jede veröffentlichte Datei an ihrer
* Prüfsumme darüber finden wir Projekt und Fassung, ohne den Nutzer zu fragen.
*/
async function modpackHerkunft(buf, index) {
const basis = {
quelle: 'modrinth',
name: index.name || '',
versionNummer: index.versionId || '',
projectId: '',
versionId: '',
};
try {
const sha1 = require('crypto').createHash('sha1').update(buf).digest('hex');
const res = await fetch(`https://api.modrinth.com/v2/version_file/${sha1}?algorithm=sha1`,
{ headers: { 'User-Agent': 'AeroMC-Launcher', Accept: 'application/json' } });
if (res.ok) {
const v = await res.json();
basis.projectId = v.project_id || '';
basis.versionId = v.id || '';
basis.versionNummer = v.version_number || basis.versionNummer;
}
} catch { /* offline oder unbekanntes Paket dann eben ohne Herkunft */ }
return basis;
}
// ---- Modpack aktualisieren (Modrinth) ----
/*
* Sucht eine neuere Fassung des installierten Modpacks.
* -> { ok, verfuegbar, aktuelleNummer, neueNummer, ... }
*/
ipcMain.handle('modpack:check', async (_e, id) => {
const inst = store.getInstance(id);
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
const mp = inst.modpack;
if (!mp || !mp.projectId) {
return { ok: false, message: 'Für diese Instanz ist kein Modrinth-Modpack hinterlegt. '
+ 'Aktualisieren geht nur bei Instanzen, die aus einem .mrpack entstanden sind.' };
}
try {
const res = await fetch(`https://api.modrinth.com/v2/project/${mp.projectId}/version`,
{ headers: { 'User-Agent': 'AeroMC-Launcher', Accept: 'application/json' } });
if (!res.ok) return { ok: false, message: 'Modrinth antwortet mit HTTP ' + res.status };
const versionen = await res.json();
if (!Array.isArray(versionen) || !versionen.length) return { ok: true, verfuegbar: false };
const neueste = versionen[0];
const verfuegbar = neueste.id !== mp.versionId;
return {
ok: true,
verfuegbar,
name: mp.name,
aktuelleNummer: mp.versionNummer || '?',
neueNummer: neueste.version_number,
neueVersionId: neueste.id,
hinweise: neueste.changelog || '',
spielVersion: (neueste.game_versions || [])[0] || '',
};
} catch (err) {
return { ok: false, message: 'Suche fehlgeschlagen: ' + err.message };
}
});
/*
* Spielt eine neuere Fassung ein. Dateien der alten Fassung, die es nicht mehr
* gibt, werden entfernt; Welten, Bildschirmfotos und Optionen bleiben.
*/
ipcMain.handle('modpack:update', async (_e, id, versionId) => {
const inst = store.getInstance(id);
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
const mp = inst.modpack;
if (!mp || !mp.projectId) return { ok: false, message: 'Kein Modpack hinterlegt.' };
try {
const vres = await fetch(`https://api.modrinth.com/v2/version/${versionId}`,
{ headers: { 'User-Agent': 'AeroMC-Launcher', Accept: 'application/json' } });
if (!vres.ok) return { ok: false, message: 'Fassung nicht abrufbar (HTTP ' + vres.status + ').' };
const version = await vres.json();
const datei = (version.files || []).find((f) => f.primary) || (version.files || [])[0];
if (!datei || !datei.url) return { ok: false, message: 'Diese Fassung enthält keine Paketdatei.' };
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('launch:progress', { id, phase: 'modpack', detail: 'Neue Fassung wird geladen …' });
}
const dl = await fetch(datei.url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
if (!dl.ok) return { ok: false, message: 'Download fehlgeschlagen (HTTP ' + dl.status + ').' };
const buf = Buffer.from(await dl.arrayBuffer());
const zip = forge.readZipIndex(buf);
if (!zip['modrinth.index.json']) return { ok: false, message: 'Die geladene Datei ist kein .mrpack.' };
const index = JSON.parse(forge.readZipEntry(buf, zip['modrinth.index.json']).toString('utf8').replace(/^/, ''));
const gameDir = store.gameDir(id);
const neu = await spieleMrpackEin(buf, zip, index, gameDir, { schonePlayerDaten: true });
// Was die alte Fassung mitbrachte und die neue nicht mehr kennt, muss weg
// sonst bleiben alte Mods liegen und der Loader stolpert darüber.
const behalten = new Set(neu.dateien.map((d) => d.toLowerCase()));
const entfernt = [];
for (const alt of mp.dateien || []) {
if (behalten.has(String(alt).toLowerCase())) continue;
if (SPIELERDATEN.some((r) => r.test(alt))) continue;
const p = path.join(gameDir, String(alt).replace(/\//g, path.sep));
if (!p.startsWith(gameDir + path.sep)) continue;
try { if (fs.existsSync(p)) { fs.rmSync(p, { force: true }); entfernt.push(alt); } } catch { /* egal */ }
}
// Minecraft-Version und Loader können sich mit der neuen Fassung ändern
const deps = index.dependencies || {};
let loader = 'vanilla'; let 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; }
store.updateInstance(id, {
minecraft: { version: deps.minecraft || inst.minecraft.version, loader, loaderVersion },
modpack: Object.assign({}, mp, {
versionId: version.id,
versionNummer: version.version_number,
dateien: neu.dateien,
}),
});
return {
ok: true,
neueNummer: version.version_number,
eingespielt: neu.dateien.length,
entfernt: entfernt.length,
spielVersion: deps.minecraft || '',
};
} catch (err) {
return { ok: false, message: 'Aktualisierung fehlgeschlagen: ' + err.message };
}
});
// ---- Modpack-Import (CurseForge .zip mit manifest.json) ---- // ---- Modpack-Import (CurseForge .zip mit manifest.json) ----
// "forge-47.2.0" / "fabric-0.15.7" -> { loader, loaderVersion } // "forge-47.2.0" / "fabric-0.15.7" -> { loader, loaderVersion }

View File

@@ -14,6 +14,7 @@ contextBridge.exposeInMainWorld('api', {
getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'), getInstancesDir: () => ipcRenderer.invoke('settings:instancesDir'),
detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath), detectJava: (javaPath) => ipcRenderer.invoke('java:detect', javaPath),
scanJava: () => ipcRenderer.invoke('java:scan'), scanJava: () => ipcRenderer.invoke('java:scan'),
listJava: () => ipcRenderer.invoke('java:list'),
// Minecraft-Versionen (Mojang) // Minecraft-Versionen (Mojang)
mcVersions: () => ipcRenderer.invoke('mc:versions'), mcVersions: () => ipcRenderer.invoke('mc:versions'),
@@ -21,10 +22,11 @@ contextBridge.exposeInMainWorld('api', {
// Mods (Modrinth) // Mods (Modrinth)
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts), modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
modList: (id) => ipcRenderer.invoke('mods:list', id), // art: 'mod' | 'resourcepack' | 'shader'
modInstall: (id, project) => ipcRenderer.invoke('mods:install', id, project), modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
modRemove: (id, filename) => ipcRenderer.invoke('mods:remove', id, filename), modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
modSetEnabled: (id, filename, enabled) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled), modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
systemMemory: () => ipcRenderer.invoke('system:memory'), systemMemory: () => ipcRenderer.invoke('system:memory'),
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb), checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit), crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
@@ -41,8 +43,8 @@ contextBridge.exposeInMainWorld('api', {
logsList: (id) => ipcRenderer.invoke('logs:list', id), logsList: (id) => ipcRenderer.invoke('logs:list', id),
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name), logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id), logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
modCheckUpdates: (id) => ipcRenderer.invoke('mods:checkUpdates', id), modCheckUpdates: (id, art) => ipcRenderer.invoke('mods:checkUpdates', id, art),
modApplyUpdates: (id, projectIds) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds), modApplyUpdates: (id, projectIds, art) => ipcRenderer.invoke('mods:applyUpdates', id, projectIds, art),
onModUpdateProgress: (cb) => { onModUpdateProgress: (cb) => {
const l = (_e, d) => cb(d); const l = (_e, d) => cb(d);
ipcRenderer.on('mods:updateProgress', l); ipcRenderer.on('mods:updateProgress', l);
@@ -86,6 +88,8 @@ contextBridge.exposeInMainWorld('api', {
return () => ipcRenderer.removeListener('instance:launch-request', l); return () => ipcRenderer.removeListener('instance:launch-request', l);
}, },
importModpack: () => ipcRenderer.invoke('modpack:import'), importModpack: () => ipcRenderer.invoke('modpack:import'),
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
onLaunchProgress: (cb) => { onLaunchProgress: (cb) => {
const listener = (_e, data) => cb(data); const listener = (_e, data) => cb(data);
ipcRenderer.on('launch:progress', listener); ipcRenderer.on('launch:progress', listener);

View File

@@ -1214,7 +1214,7 @@ function applyIcons(root = document) {
// ---------- Instanz-Kacheln ---------- // ---------- Instanz-Kacheln ----------
const SINGLE_SELECTION_BUTTON_IDS = [ const SINGLE_SELECTION_BUTTON_IDS = [
'p-play', 'p-offline', 'p-edit', 'p-notes', 'p-mods', 'p-worlds', 'p-screens', 'p-logs', 'p-servers', 'p-play', 'p-offline', 'p-edit', 'p-notes', 'p-mods', 'p-worlds', 'p-screens', 'p-logs', 'p-servers', 'p-modpack',
'p-mcfolder', 'p-configfolder', 'p-instfolder', 'p-shortcut', 'p-export', 'p-copy', 'p-mcfolder', 'p-configfolder', 'p-instfolder', 'p-shortcut', 'p-export', 'p-copy',
]; ];
@@ -1292,6 +1292,9 @@ async function refreshSelectionPanel() {
panelE.classList.add('hidden'); panelE.classList.add('hidden');
panelC.classList.remove('hidden'); panelC.classList.remove('hidden');
// "Modpack aktualisieren" nur bei Instanzen, die aus einem Modrinth-Paket stammen
el('p-modpack').classList.toggle('hidden', !(inst.modpack && inst.modpack.projectId));
const iconData = inst.customImage ? await window.api.instanceIcon(id) : null; const iconData = inst.customImage ? await window.api.instanceIcon(id) : null;
if (iconData) el('p-icon').innerHTML = `<img src="${iconData}" alt="" />`; if (iconData) el('p-icon').innerHTML = `<img src="${iconData}" alt="" />`;
else { el('p-icon').innerHTML = iconOf(inst.icon, 60); } else { el('p-icon').innerHTML = iconOf(inst.icon, 60); }
@@ -1611,6 +1614,7 @@ async function openEdit(inst) {
el('f-min-mem').value = inst.java.minMemMb || ''; el('f-min-mem').value = inst.java.minMemMb || '';
el('f-max-mem').value = inst.java.maxMemMb || ''; el('f-max-mem').value = inst.java.maxMemMb || '';
zeigeSpeicherHinweis(); zeigeSpeicherHinweis();
fuelleInstanzJava(inst);
el('f-extra-args').value = inst.java.extraArgs || ''; el('f-extra-args').value = inst.java.extraArgs || '';
el('f-prelaunch').value = inst.preLaunchCommand || ''; el('f-prelaunch').value = inst.preLaunchCommand || '';
el('f-postexit').value = inst.postExitCommand || ''; el('f-postexit').value = inst.postExitCommand || '';
@@ -1653,6 +1657,7 @@ async function saveInstance() {
let targetId = editingId; let targetId = editingId;
if (editingId) { if (editingId) {
payload.java = { payload.java = {
path: el('f-java').value.trim(),
minMemMb: minMem, minMemMb: minMem,
maxMemMb: maxMem, maxMemMb: maxMem,
extraArgs: el('f-extra-args').value.trim(), extraArgs: el('f-extra-args').value.trim(),
@@ -1711,6 +1716,36 @@ async function launch(id, accountId) {
} }
} }
// ---------- Java je Instanz ----------
async function fuelleInstanzJava(inst) {
const feld = el('f-java');
if (!feld) return;
feld.value = (inst && inst.java && inst.java.path) || '';
const sel = el('f-java-detected');
sel.innerHTML = '<option value=""> gefundene Java-Installationen </option>';
try {
for (const j of await window.api.listJava()) {
const o = document.createElement('option');
o.value = j.path;
o.textContent = `Java ${j.major} (${j.arch}) ${j.path}`;
sel.appendChild(o);
}
} catch { /* Liste bleibt leer */ }
sel.value = feld.value && [...sel.options].some((o) => o.value === feld.value) ? feld.value : '';
// Der häufigste Stolperstein: global ist eine feste Fassung eingetragen,
// die dann auch für alte Minecraft-Versionen genommen wird.
try {
const s = await window.api.getSettings();
el('f-java-hint').textContent = s.javaPath
? `Achtung: In den Einstellungen ist ${s.javaPath} fest vorgegeben und gilt für alle Instanzen. `
+ 'Trage hier etwas ein, wenn diese Instanz eine andere Fassung braucht ältere Minecraft-Versionen verlangen oft Java 8.'
: 'Leer lassen: Der Launcher nimmt die Fassung, die diese Minecraft-Version verlangt, und lädt sie bei Bedarf selbst.';
} catch { /* egal */ }
}
// ---------- Arbeitsspeicher ---------- // ---------- Arbeitsspeicher ----------
let systemSpeicher = null; let systemSpeicher = null;
@@ -1728,6 +1763,52 @@ async function zeigeSpeicherHinweis() {
+ `(Vorschlag: ${empfohlen} MB).`; + `(Vorschlag: ${empfohlen} MB).`;
} }
// ---------- Modpack aktualisieren ----------
let modpackNeueVersion = null;
async function openModpackUpdate() {
if (!currentDetailId) return;
modpackNeueVersion = null;
el('mp-status').textContent = 'Suche nach einer neueren Fassung …';
el('mp-kern').classList.add('hidden');
el('mp-do').classList.add('hidden');
show('modal-modpack');
const res = await window.api.modpackCheck(currentDetailId);
if (!res.ok) { el('mp-status').textContent = '✖ ' + (res.message || 'Suche fehlgeschlagen.'); return; }
if (!res.verfuegbar) {
el('mp-status').textContent = `${res.name || 'Das Modpack'} ist auf dem neuesten Stand (${res.aktuelleNummer}).`;
return;
}
modpackNeueVersion = res.neueVersionId;
el('mp-status').textContent = `Für ${res.name || 'dieses Modpack'} gibt es eine neuere Fassung.`;
const zeile = (t, w) => (w ? `<div class="crash-zeile"><div class="crash-titel">${t}</div><div class="crash-wert">${escapeHtml(w)}</div></div>` : '');
el('mp-kern').innerHTML = zeile('Installiert', res.aktuelleNummer) + zeile('Neu', res.neueNummer)
+ zeile('Minecraft', res.spielVersion)
+ (res.hinweise ? zeile('Änderungen', res.hinweise.slice(0, 400)) : '');
el('mp-kern').classList.remove('hidden');
el('mp-do').classList.remove('hidden');
}
async function doModpackUpdate() {
if (!modpackNeueVersion) return;
const btn = el('mp-do');
btn.disabled = true;
el('mp-status').textContent = 'Wird aktualisiert das kann einige Minuten dauern …';
const res = await window.api.modpackUpdate(currentDetailId, modpackNeueVersion);
btn.disabled = false;
if (!res.ok) { el('mp-status').textContent = '✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.'); return; }
el('mp-status').textContent = `✔ Auf ${res.neueNummer} aktualisiert: ${res.eingespielt} Dateien eingespielt`
+ (res.entfernt ? `, ${res.entfernt} entfernt` : '') + '.';
el('mp-do').classList.add('hidden');
toast('Modpack aktualisiert.');
await reload();
await selectInstance(currentDetailId);
}
// ---------- Serverliste ---------- // ---------- Serverliste ----------
let zuUebertragenderServer = null; let zuUebertragenderServer = null;
@@ -2166,27 +2247,57 @@ function renderContentList(boxId, items, sub, emoji) {
} }
// ---------- Mod-Browser ---------- // ---------- Mod-Browser ----------
// Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader
let modArt = 'mod';
const ART_LABEL = { mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader' };
const ART_SUCHE = {
mod: 'Mods durchsuchen … (z. B. Sodium, JEI, Create)',
resourcepack: 'Ressourcenpakete durchsuchen … (z. B. Faithful, Bare Bones)',
shader: 'Shader durchsuchen … (z. B. Complementary, BSL)',
};
// Nur Mods brauchen einen Loader Ressourcenpakete und Shader laufen auch in Vanilla
const ART_BRAUCHT_LOADER = { mod: true, resourcepack: false, shader: false };
async function openMods() { async function openMods() {
const inst = await window.api.getInstance(currentDetailId); const inst = await window.api.getInstance(currentDetailId);
if (!inst) return; if (!inst) return;
el('m-title').textContent = 'Mods ' + inst.name; modArt = 'mod';
el('m-art').value = 'mod';
await zeigeModAnsicht(inst);
show('modal-mods');
}
// Wechsel der Inhaltsart: alles neu aufbauen
async function modArtGewechselt() {
modArt = el('m-art').value || 'mod';
const inst = await window.api.getInstance(currentDetailId);
if (inst) await zeigeModAnsicht(inst);
}
async function zeigeModAnsicht(inst) {
el('m-title').textContent = ART_LABEL[modArt] + ' ' + inst.name;
const loaderTxt = LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader; const loaderTxt = LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader;
el('m-sub').textContent = (inst.minecraft.version || 'keine Version') + ' · ' + loaderTxt; el('m-sub').textContent = (inst.minecraft.version || 'keine Version') + ' · ' + loaderTxt;
el('m-query').placeholder = ART_SUCHE[modArt];
const warn = el('m-warn'); const warn = el('m-warn');
const blocked = !inst.minecraft.version || inst.minecraft.loader === 'vanilla'; const brauchtLoader = ART_BRAUCHT_LOADER[modArt];
const blocked = !inst.minecraft.version || (brauchtLoader && inst.minecraft.loader === 'vanilla');
if (blocked) { if (blocked) {
warn.classList.remove('hidden'); warn.classList.remove('hidden');
warn.textContent = !inst.minecraft.version warn.textContent = !inst.minecraft.version
? '⚠ Diese Instanz hat keine Minecraft-Version. Bitte zuerst im Bearbeiten-Dialog eine Version wählen.' ? '⚠ Diese Instanz hat keine Minecraft-Version. Bitte zuerst im Bearbeiten-Dialog eine Version wählen.'
: '⚠ Vanilla-Instanzen unterstützen keine Mods. Wähle beim Bearbeiten einen Mod-Loader (Fabric/Forge/NeoForge/Quilt).'; : '⚠ Vanilla-Instanzen unterstützen keine Mods. Wähle beim Bearbeiten einen Mod-Loader (Fabric/Forge/NeoForge/Quilt). Ressourcenpakete und Shader gehen auch ohne.';
} else { } else {
warn.classList.add('hidden'); warn.classList.add('hidden');
} }
['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; }); ['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; });
// Kategorien gelten nur für Mods
el('m-category').disabled = blocked || modArt !== 'mod';
el('m-query').value = ''; el('m-query').value = '';
el('m-more').classList.add('hidden'); el('m-more').classList.add('hidden');
// Ergebnisse der vorigen Instanz nicht mitschleppen // Ergebnisse der vorigen Ansicht nicht mitschleppen
modUpdates = new Map(); modUpdates = new Map();
setUpdateStatus(''); setUpdateStatus('');
el('m-apply-updates').classList.add('hidden'); el('m-apply-updates').classList.add('hidden');
@@ -2194,15 +2305,16 @@ async function openMods() {
switchModTab('catalog'); switchModTab('catalog');
await refreshInstalled(); await refreshInstalled();
show('modal-mods');
if (blocked) { if (blocked) {
const box = el('m-results'); const box = el('m-results');
box.innerHTML = '<div class="mods-empty">Für Mods braucht diese Instanz einen Mod-Loader.</div>'; box.innerHTML = `<div class="mods-empty">${!inst.minecraft.version
? 'Ohne Minecraft-Version geht hier nichts.'
: 'Für Mods braucht diese Instanz einen Mod-Loader.'}</div>`;
const b = document.createElement('button'); const b = document.createElement('button');
b.className = 'btn btn-primary'; b.className = 'btn btn-primary';
b.style.margin = '0 auto'; b.style.margin = '0 auto';
b.style.display = 'block'; b.style.display = 'block';
b.textContent = 'Mod-Loader hinzufügen'; b.textContent = inst.minecraft.version ? 'Mod-Loader hinzufügen' : 'Version wählen';
b.addEventListener('click', async () => { b.addEventListener('click', async () => {
hide('modal-mods'); hide('modal-mods');
const i = await window.api.getInstance(currentDetailId); const i = await window.api.getInstance(currentDetailId);
@@ -2210,7 +2322,7 @@ async function openMods() {
}); });
box.appendChild(b); box.appendChild(b);
} else { } else {
await loadCatalog(false); // beliebte Mods sofort anzeigen await loadCatalog(false); // beliebte Einträge sofort anzeigen
} }
} }
@@ -2226,7 +2338,7 @@ function switchModTab(which) {
let modUpdates = new Map(); let modUpdates = new Map();
async function refreshInstalled() { async function refreshInstalled() {
const list = await window.api.modList(currentDetailId); const list = await window.api.modList(currentDetailId, modArt);
currentModsInstalled = new Set(list.map((m) => m.projectId)); currentModsInstalled = new Set(list.map((m) => m.projectId));
el('m-installed-count').textContent = String(list.length); el('m-installed-count').textContent = String(list.length);
const box = el('m-installed'); const box = el('m-installed');
@@ -2265,7 +2377,7 @@ async function refreshInstalled() {
: 'Datei auf .jar.disabled umbenennen Minecraft lädt sie dann nicht'; : 'Datei auf .jar.disabled umbenennen Minecraft lädt sie dann nicht';
schalter.addEventListener('click', async () => { schalter.addEventListener('click', async () => {
schalter.disabled = true; schalter.disabled = true;
const res = await window.api.modSetEnabled(currentDetailId, m.filename, aus); const res = await window.api.modSetEnabled(currentDetailId, m.filename, aus, modArt);
if (res.ok) toast((aus ? 'Eingeschaltet: ' : 'Ausgeschaltet: ') + m.title); if (res.ok) toast((aus ? 'Eingeschaltet: ' : 'Ausgeschaltet: ') + m.title);
else toast(res.message || 'Umschalten fehlgeschlagen.', true); else toast(res.message || 'Umschalten fehlgeschlagen.', true);
await refreshInstalled(); await refreshInstalled();
@@ -2277,7 +2389,7 @@ async function refreshInstalled() {
btn.className = 'btn btn-danger btn-mini'; btn.className = 'btn btn-danger btn-mini';
btn.textContent = 'Entfernen'; btn.textContent = 'Entfernen';
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
await window.api.modRemove(currentDetailId, m.filename); await window.api.modRemove(currentDetailId, m.filename, modArt);
toast('Entfernt: ' + m.title); toast('Entfernt: ' + m.title);
await refreshInstalled(); await refreshInstalled();
}); });
@@ -2296,7 +2408,7 @@ async function checkModUpdates() {
el('m-apply-updates').classList.add('hidden'); el('m-apply-updates').classList.add('hidden');
setUpdateStatus('Suche …'); setUpdateStatus('Suche …');
try { try {
const res = await window.api.modCheckUpdates(currentDetailId); const res = await window.api.modCheckUpdates(currentDetailId, modArt);
if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Suche fehlgeschlagen.')); return; } if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Suche fehlgeschlagen.')); return; }
modUpdates = new Map((res.treffer || []).map((t) => [t.projectId, t])); modUpdates = new Map((res.treffer || []).map((t) => [t.projectId, t]));
@@ -2321,14 +2433,15 @@ async function applyModUpdates() {
btn.disabled = true; btn.disabled = true;
setUpdateStatus('Wird aktualisiert …'); setUpdateStatus('Wird aktualisiert …');
try { try {
const res = await window.api.modApplyUpdates(currentDetailId, [...modUpdates.keys()]); const res = await window.api.modApplyUpdates(currentDetailId, [...modUpdates.keys()], modArt);
if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.')); return; } if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.')); return; }
modUpdates = new Map(); modUpdates = new Map();
await refreshInstalled(); await refreshInstalled();
const n = res.erneuert.length; const n = res.erneuert.length;
setUpdateStatus(n + (n === 1 ? ' Mod aktualisiert' : ' Mods aktualisiert') const wort = n === 1 ? 'Eintrag' : 'Einträge';
setUpdateStatus(`${n} ${wort} aktualisiert`
+ (res.fehler.length ? ' · ' + res.fehler.length + ' fehlgeschlagen' : '')); + (res.fehler.length ? ' · ' + res.fehler.length + ' fehlgeschlagen' : ''));
if (n) toast(n + (n === 1 ? ' Mod aktualisiert.' : ' Mods aktualisiert.')); if (n) toast(`${n} ${wort} aktualisiert.`);
for (const f of res.fehler) toast(f.title + ': ' + f.message, true); for (const f of res.fehler) toast(f.title + ': ' + f.message, true);
btn.classList.add('hidden'); btn.classList.add('hidden');
} finally { } finally {
@@ -2339,7 +2452,9 @@ async function applyModUpdates() {
let modOffset = 0; let modOffset = 0;
async function loadCatalog(append) { async function loadCatalog(append) {
const inst = await window.api.getInstance(currentDetailId); const inst = await window.api.getInstance(currentDetailId);
if (!inst || !inst.minecraft.version || inst.minecraft.loader === 'vanilla') return; if (!inst || !inst.minecraft.version) return;
// Loader nur für Mods verlangen
if (ART_BRAUCHT_LOADER[modArt] && inst.minecraft.loader === 'vanilla') return;
const box = el('m-results'); const box = el('m-results');
if (!append) { modOffset = 0; box.innerHTML = '<div class="mods-empty">Lade …</div>'; el('m-more').classList.add('hidden'); } if (!append) { modOffset = 0; box.innerHTML = '<div class="mods-empty">Lade …</div>'; el('m-more').classList.add('hidden'); }
try { try {
@@ -2347,10 +2462,11 @@ async function loadCatalog(append) {
query: el('m-query').value.trim(), query: el('m-query').value.trim(),
gameVersion: inst.minecraft.version, gameVersion: inst.minecraft.version,
loader: inst.minecraft.loader, loader: inst.minecraft.loader,
category: el('m-category').value, category: modArt === 'mod' ? el('m-category').value : '',
sort: el('m-sort').value, sort: el('m-sort').value,
limit: 30, limit: 30,
offset: modOffset, offset: modOffset,
art: modArt,
}); });
if (!append) box.innerHTML = ''; if (!append) box.innerHTML = '';
if (!res.hits.length && !append) { if (!res.hits.length && !append) {
@@ -2412,7 +2528,7 @@ function buildResultItem(mod) {
async function installMod(mod, btn, item) { async function installMod(mod, btn, item) {
btn.disabled = true; btn.textContent = 'Lädt …'; btn.disabled = true; btn.textContent = 'Lädt …';
if (item) { item.classList.add('installing'); item.classList.remove('clickable'); } if (item) { item.classList.add('installing'); item.classList.remove('clickable'); }
const res = await window.api.modInstall(currentDetailId, { projectId: mod.projectId, title: mod.title }); const res = await window.api.modInstall(currentDetailId, { projectId: mod.projectId, title: mod.title }, modArt);
if (item) item.classList.remove('installing'); if (item) item.classList.remove('installing');
if (res.ok) { if (res.ok) {
const dep = res.depCount ? ` (+${res.depCount} Abhängigkeit${res.depCount > 1 ? 'en' : ''})` : ''; const dep = res.depCount ? ` (+${res.depCount} Abhängigkeit${res.depCount > 1 ? 'en' : ''})` : '';
@@ -3187,6 +3303,8 @@ function wire() {
el('p-screens').addEventListener('click', openScreens); el('p-screens').addEventListener('click', openScreens);
el('p-logs').addEventListener('click', openLogs); el('p-logs').addEventListener('click', openLogs);
el('p-servers').addEventListener('click', openServers); el('p-servers').addEventListener('click', openServers);
el('p-modpack').addEventListener('click', openModpackUpdate);
el('mp-do').addEventListener('click', doModpackUpdate);
el('srv-add').addEventListener('click', addServer); el('srv-add').addEventListener('click', addServer);
el('srv-ip').addEventListener('keydown', (e) => { if (e.key === 'Enter') addServer(); }); el('srv-ip').addEventListener('keydown', (e) => { if (e.key === 'Enter') addServer(); });
el('srv-copy-do').addEventListener('click', doServerCopy); el('srv-copy-do').addEventListener('click', doServerCopy);
@@ -3266,6 +3384,7 @@ function wire() {
el('m-more').addEventListener('click', () => loadCatalog(true)); el('m-more').addEventListener('click', () => loadCatalog(true));
el('m-tab-catalog').addEventListener('click', () => switchModTab('catalog')); el('m-tab-catalog').addEventListener('click', () => switchModTab('catalog'));
el('m-tab-installed').addEventListener('click', () => switchModTab('installed')); el('m-tab-installed').addEventListener('click', () => switchModTab('installed'));
el('m-art').addEventListener('change', modArtGewechselt);
el('m-open-folder').addEventListener('click', () => window.api.openGameDir(currentDetailId)); el('m-open-folder').addEventListener('click', () => window.api.openGameDir(currentDetailId));
// Automatische Sicherung: unaufdringlich melden, Fehler deutlich // Automatische Sicherung: unaufdringlich melden, Fehler deutlich
window.api.onAutoBackup((d) => { window.api.onAutoBackup((d) => {
@@ -3314,6 +3433,13 @@ function wire() {
el('s-bgskin').addEventListener('change', previewSkin); el('s-bgskin').addEventListener('change', previewSkin);
el('s-bgskin-pose').addEventListener('change', previewSkin); el('s-bgskin-pose').addEventListener('change', previewSkin);
el('s-scan-java').addEventListener('click', scanJava); el('s-scan-java').addEventListener('click', scanJava);
el('f-java-detected').addEventListener('change', (e) => {
if (e.target.value) el('f-java').value = e.target.value;
});
el('f-java-pick').addEventListener('click', async () => {
const p = await window.api.pickJava();
if (p) { el('f-java').value = p; el('f-java-detected').value = ''; }
});
el('s-java-detected').addEventListener('change', async (e) => { el('s-java-detected').addEventListener('change', async (e) => {
if (e.target.value) { el('s-java').value = e.target.value; await refreshJavaStatus(e.target.value); } if (e.target.value) { el('s-java').value = e.target.value; await refreshJavaStatus(e.target.value); }
}); });

View File

@@ -227,10 +227,29 @@ const MODRINTH = 'https://api.modrinth.com/v2';
const MODRINTH_SORTS = ['relevance', 'downloads', 'follows', 'newest', 'updated']; const MODRINTH_SORTS = ['relevance', 'downloads', 'follows', 'newest', 'updated'];
async function modrinthSearch({ query = '', gameVersion = '', loader = '', category = '', sort = 'relevance', limit = 30, offset = 0 } = {}) { /*
const facets = [['project_type:mod']]; * Inhaltsarten, die der Launcher aus dem Modrinth-Katalog holen kann.
* ordner = Zielverzeichnis im Spielordner, loaderNoetig = braucht einen
* Mod-Loader (Ressourcenpakete laufen auch in Vanilla).
*/
const INHALTSARTEN = {
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
};
function inhaltsArt(art) {
return INHALTSARTEN[art] || INHALTSARTEN.mod;
}
async function modrinthSearch({ query = '', gameVersion = '', loader = '', category = '', sort = 'relevance', limit = 30, offset = 0, art = 'mod' } = {}) {
const typ = inhaltsArt(art);
const facets = [['project_type:' + typ.projectType]];
if (gameVersion) facets.push(['versions:' + gameVersion]); if (gameVersion) facets.push(['versions:' + gameVersion]);
if (loader && loader !== 'vanilla') facets.push(['categories:' + loader]); // Der Loader-Filter gilt nur für Mods. Ressourcenpakete und Shader werden
// bei Modrinth nicht nach Mod-Loader eingeordnet der Filter würde die
// Trefferliste leeren.
if (typ.loaderNoetig && loader && loader !== 'vanilla') facets.push(['categories:' + loader]);
if (category) facets.push(['categories:' + category]); if (category) facets.push(['categories:' + category]);
const url = new URL(MODRINTH + '/search'); const url = new URL(MODRINTH + '/search');
url.searchParams.set('query', query); url.searchParams.set('query', query);
@@ -258,10 +277,13 @@ async function modrinthSearch({ query = '', gameVersion = '', loader = '', categ
} }
// findet die passende Datei-Version für Spielversion + Loader // findet die passende Datei-Version für Spielversion + Loader
async function modrinthResolveFile({ projectId, gameVersion, loader }) { async function modrinthResolveFile({ projectId, gameVersion, loader, art = 'mod' }) {
const typ = inhaltsArt(art);
const url = new URL(MODRINTH + '/project/' + projectId + '/version'); const url = new URL(MODRINTH + '/project/' + projectId + '/version');
if (gameVersion) url.searchParams.set('game_versions', JSON.stringify([gameVersion])); if (gameVersion) url.searchParams.set('game_versions', JSON.stringify([gameVersion]));
if (loader && loader !== 'vanilla') url.searchParams.set('loaders', JSON.stringify([loader])); // Nur Mods werden nach Loader eingeordnet; bei Ressourcenpaketen und
// Shadern würde der Filter alle Dateien ausschließen.
if (typ.loaderNoetig && loader && loader !== 'vanilla') url.searchParams.set('loaders', JSON.stringify([loader]));
const res = await fetch(url, { headers: { 'User-Agent': UA } }); const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Versionsabruf fehlgeschlagen: HTTP ' + res.status); if (!res.ok) throw new Error('Versionsabruf fehlgeschlagen: HTTP ' + res.status);
const versions = await res.json(); const versions = await res.json();
@@ -291,11 +313,14 @@ async function downloadFile(url, destPath, extraHeaders) {
} }
// lädt eine Mod in <modsDir>; gibt Metadaten für die Instanz zurück // lädt eine Mod in <modsDir>; gibt Metadaten für die Instanz zurück
async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir }) { async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
const file = await modrinthResolveFile({ projectId, gameVersion, loader }); const typ = inhaltsArt(art);
const file = await modrinthResolveFile({ projectId, gameVersion, loader, art });
if (!file) { if (!file) {
return { ok: false, reason: 'no-match', return { ok: false, reason: 'no-match',
message: `Keine passende Version für ${gameVersion} / ${loader} gefunden.` }; message: typ.loaderNoetig
? `Keine passende Version für ${gameVersion} / ${loader} gefunden.`
: `Keine passende Version für ${gameVersion} gefunden.` };
} }
const dest = path.join(modsDir, file.filename); const dest = path.join(modsDir, file.filename);
const bytes = await downloadFile(file.url, dest); const bytes = await downloadFile(file.url, dest);
@@ -322,7 +347,7 @@ async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir
* für die weiß niemand, woher sie stammen. * für die weiß niemand, woher sie stammen.
* -> [{ projectId, title, alt: {...}, neu: {...} }] * -> [{ projectId, title, alt: {...}, neu: {...} }]
*/ */
async function modrinthFindUpdates(mods, gameVersion, loader, onProgress) { async function modrinthFindUpdates(mods, gameVersion, loader, onProgress, art = 'mod') {
const kandidaten = (mods || []).filter((m) => m && m.projectId); const kandidaten = (mods || []).filter((m) => m && m.projectId);
const treffer = []; const treffer = [];
let geprueft = 0; let geprueft = 0;
@@ -333,8 +358,8 @@ async function modrinthFindUpdates(mods, gameVersion, loader, onProgress) {
await Promise.all(kandidaten.slice(i, i + GRUPPE).map(async (m) => { await Promise.all(kandidaten.slice(i, i + GRUPPE).map(async (m) => {
let file = null; let file = null;
try { try {
file = await modrinthResolveFile({ projectId: m.projectId, gameVersion, loader }); file = await modrinthResolveFile({ projectId: m.projectId, gameVersion, loader, art });
} catch { /* Netzfehler -> diese Mod überspringen */ } } catch { /* Netzfehler -> diesen Eintrag überspringen */ }
geprueft++; geprueft++;
if (onProgress) onProgress({ geprueft, gesamt: kandidaten.length, titel: m.title || m.projectId }); if (onProgress) onProgress({ geprueft, gesamt: kandidaten.length, titel: m.title || m.projectId });
if (!file || !file.versionId) return; if (!file || !file.versionId) return;
@@ -480,6 +505,6 @@ module.exports = {
scanJava, probeJava, setCacheDir, setManagedJavaDir, pruefeSpeicher, scanJava, probeJava, setCacheDir, setManagedJavaDir, pruefeSpeicher,
getMcVersions, getMcVersions,
modrinthSearch, modrinthResolveFile, modrinthInstall, modrinthProjectTitle, modrinthSearch, modrinthResolveFile, modrinthInstall, modrinthProjectTitle,
modrinthFindUpdates, downloadFile, modrinthFindUpdates, downloadFile, inhaltsArt, INHALTSARTEN,
curseforgeResolveFiles, forgeCdnUrl, curseforgeZugang, curseforgeResolveFiles, forgeCdnUrl, curseforgeZugang,
}; };

View File

@@ -124,12 +124,25 @@ function gameDir(id) {
return path.join(instancePath(id), 'minecraft'); return path.join(instancePath(id), 'minecraft');
} }
function modsDir(id) { /*
return path.join(gameDir(id), 'mods'); * Inhalte je Art: Mods, Ressourcenpakete und Shader liegen in verschiedenen
* Ordnern des Spielverzeichnisses und werden getrennt verzeichnet.
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
* weiterlaufen.
*/
const INHALT_ORDNER = { mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks' };
const INHALT_INDEX = { mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json' };
function inhaltsDir(id, art = 'mod') {
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
} }
function modsIndexFile(id) { function modsDir(id, art = 'mod') {
return path.join(instancePath(id), 'mods.json'); return inhaltsDir(id, art);
}
function modsIndexFile(id, art = 'mod') {
return path.join(instancePath(id), INHALT_INDEX[art] || INHALT_INDEX.mod);
} }
// ---------- installierte Mods (Metadaten je Instanz) ---------- // ---------- installierte Mods (Metadaten je Instanz) ----------
@@ -142,8 +155,8 @@ function modsIndexFile(id) {
const MOD_ENDUNG = /\.(jar|zip)$/i; const MOD_ENDUNG = /\.(jar|zip)$/i;
const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i; const AUS_ENDUNG = /\.(jar|zip)\.disabled$/i;
function scanModsDir(id) { function scanModsDir(id, art = 'mod') {
const dir = modsDir(id); const dir = modsDir(id, art);
if (!fs.existsSync(dir)) return []; if (!fs.existsSync(dir)) return [];
const gefunden = []; const gefunden = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -163,9 +176,9 @@ function scanModsDir(id) {
return gefunden; return gefunden;
} }
function listMods(id) { function listMods(id, art = 'mod') {
const aufDerPlatte = new Map(scanModsDir(id).map((m) => [m.filename, m])); const aufDerPlatte = new Map(scanModsDir(id, art).map((m) => [m.filename, m]));
const indexed = readJson(modsIndexFile(id), []); const indexed = readJson(modsIndexFile(id, art), []);
const merged = new Map(); const merged = new Map();
for (const entry of indexed) { for (const entry of indexed) {
@@ -186,8 +199,8 @@ function listMods(id) {
* Schaltet eine Mod an oder aus, indem die Datei umbenannt wird. * Schaltet eine Mod an oder aus, indem die Datei umbenannt wird.
* -> { ok, enabled } oder { ok: false, reason } * -> { ok, enabled } oder { ok: false, reason }
*/ */
function setModEnabled(id, filename, enabled) { function setModEnabled(id, filename, enabled, art = 'mod') {
const dir = modsDir(id); const dir = modsDir(id, art);
const anPfad = path.join(dir, filename); const anPfad = path.join(dir, filename);
const ausPfad = anPfad + '.disabled'; const ausPfad = anPfad + '.disabled';
// Pfad darf den mods-Ordner nicht verlassen // Pfad darf den mods-Ordner nicht verlassen
@@ -212,18 +225,18 @@ function setModEnabled(id, filename, enabled) {
} }
} }
function addMod(id, entry) { function addMod(id, entry, art = 'mod') {
const list = listMods(id).filter((m) => m.filename !== entry.filename); const list = listMods(id, art).filter((m) => m.filename !== entry.filename);
list.push(entry); list.push(entry);
writeJson(modsIndexFile(id), list); writeJson(modsIndexFile(id, art), list);
return list; return list;
} }
function removeMod(id, filename) { function removeMod(id, filename, art = 'mod') {
const list = listMods(id).filter((m) => m.filename !== filename); const list = listMods(id, art).filter((m) => m.filename !== filename);
writeJson(modsIndexFile(id), list); writeJson(modsIndexFile(id, art), list);
// zugehörige Datei löschen auch, wenn sie gerade abgeschaltet ist // zugehörige Datei löschen auch, wenn sie gerade abgeschaltet ist
const jar = path.join(modsDir(id), filename); const jar = path.join(modsDir(id, art), filename);
for (const p of [jar, jar + '.disabled']) { for (const p of [jar, jar + '.disabled']) {
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ } try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch { /* egal */ }
} }
@@ -423,6 +436,7 @@ module.exports = {
instancePath, instancePath,
gameDir, gameDir,
modsDir, modsDir,
inhaltsDir,
iconDataUri, iconDataUri,
setCustomImage, setCustomImage,
clearCustomImage, clearCustomImage,