Upload via GUI (45 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-08 22:09:57 +02:00
parent 33e533009e
commit 87fded4cb6
10 changed files with 426 additions and 28 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules/
dist/
# beim Bauen erzeugt, enthält Adresse und Token der CurseForge-Weiterleitung
src/build-config.json
# Zugangsdaten des Weiterleitungs-Servers
server/curseforge-proxy/.env
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "aeromc",
"productName": "AeroMC",
"version": "1.0.7",
"version": "1.0.8",
"description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
"homepage": "https://m-viper.de",
"launcherWebsite": "https://aeromc.viper.ipv64.net",
@@ -23,7 +23,8 @@
"compression": "store",
"files": [
"src/**/*",
"package.json"
"package.json",
"LICENSE"
],
"win": {
"target": "nsis",
+44 -2
View File
@@ -130,6 +130,7 @@
<option value="forge">Forge</option>
<option value="neoforge">NeoForge</option>
<option value="quilt">Quilt</option>
<option value="optifine">OptiFine</option>
</select>
</label>
<label class="field">
@@ -139,6 +140,23 @@
</label>
</div>
<div id="f-optifine-tools" class="field-row hidden" style="align-items:flex-start">
<div class="field" style="flex:1">
<span>OptiFine einrichten</span>
<p class="hint" style="margin:2px 0 8px">
OptiFine bietet kein automatisches Download-API (Downloads laufen bei OptiFine bewusst über eine Werbeseite).
Lade den Installer zuerst selbst von <b>optifine.net</b> herunter, wähle ihn dann hier aus AeroMC bereitet
die passende Minecraft-Version vor und startet den echten OptiFine-Installer. Wähle darin als Zielordner den
automatisch in die Zwischenablage kopierten Pfad, klicke „Install" und bestätige danach hier.
</p>
<div class="path-row">
<button type="button" id="f-optifine-install" class="btn">Installer wählen und starten…</button>
<button type="button" id="f-optifine-confirm" class="btn btn-primary hidden">Fertig Version übernehmen</button>
</div>
<small id="f-optifine-status" class="hint"></small>
</div>
</div>
<label class="field">
<span>Notizen <small class="opt">(optional)</small></span>
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
@@ -642,6 +660,7 @@
<option value="mod">Mods</option>
<option value="resourcepack">Ressourcenpakete</option>
<option value="shader">Shader</option>
<option value="datapack">Datenpakete</option>
</select>
<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>
@@ -966,6 +985,22 @@
</div>
</div>
<!-- ===================== Dialog: Lizenz ===================== -->
<div id="modal-license" class="modal hidden">
<div class="modal-card modal-lg">
<div class="modal-head">
<h3>Lizenz</h3>
<button class="modal-close" data-close></button>
</div>
<div class="modal-body">
<pre id="license-text" class="crash-voll">Lade …</pre>
</div>
<div class="modal-foot">
<button class="btn" data-close>Schließen</button>
</div>
</div>
</div>
<!-- ===================== Dialog: Update ===================== -->
<div id="modal-update" class="modal hidden">
<div class="modal-card modal-sm">
@@ -1019,11 +1054,18 @@
<div id="import-pane-modpack" class="import-pane hidden">
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
<p class="hint">Suche läuft über Modrinth. Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json) dafür ist ein API-Key oder eine Weiterleitung in den Netzwerk-Einstellungen nötig.</p>
<p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Für CurseForge wird ein eigener API-Key in den Netzwerk-Einstellungen gebraucht (die Weiterleitung reicht nur für den Datei-Import). Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p>
<div class="field-row" style="margin-top:12px;align-items:flex-end">
<label class="field" style="width:150px">
<span>Quelle</span>
<select id="mp-source">
<option value="modrinth">Modrinth</option>
<option value="curseforge">CurseForge</option>
</select>
</label>
<label class="field" style="flex:1">
<span>Suche (Modrinth)</span>
<span>Suche</span>
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
</label>
<label class="field" style="width:140px">
+31
View File
@@ -464,6 +464,37 @@ async function prepareAndLaunch(opts, onProgress) {
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
}
effectiveVj = mergeLoader(vj, fvj);
} else if (opts.loader === 'optifine') {
/*
* OptiFine hat keine öffentliche Installations-API (Downloads laufen bewusst
* über eine Werbeseite). AeroMC lädt hier nichts selbst herunter der Nutzer
* hat den offiziellen Installer bereits einmal sichtbar laufen lassen (siehe
* optifine:setup/optifine:confirm in main.js), der dabei ein normales
* Versions-Profil unter <sharedDir>/versions/<loaderVersion>/ abgelegt hat,
* genau wie Fabric/Forge es tun. Wir lesen es nur noch ein und mergen es.
*/
const ofId = opts.loaderVersion;
if (!ofId) {
return { ok: false, message: 'Für diese Instanz ist keine OptiFine-Version hinterlegt. Bitte im Bearbeiten-Dialog einrichten.' };
}
const ofJsonPath = path.join(opts.sharedDir, 'versions', ofId, ofId + '.json');
if (!fs.existsSync(ofJsonPath)) {
return { ok: false, message: `OptiFine-Profil "${ofId}" wurde nicht gefunden. Bitte die Einrichtung im Bearbeiten-Dialog wiederholen.` };
}
let ofvj;
try { ofvj = JSON.parse(fs.readFileSync(ofJsonPath, 'utf8')); }
catch (err) { return { ok: false, message: 'OptiFine-Profil ist beschädigt: ' + err.message }; }
for (const lib of ofvj.libraries || []) {
const art = lib.downloads && lib.downloads.artifact;
const rel = (art && art.path) || forge.mavenPath(lib.name);
const dest = path.join(libDir, rel.replace(/\//g, path.sep));
if (!fs.existsSync(dest) && art && art.url) {
await downloadIfNeeded(art.url, dest, art.sha1 || null);
}
classpath.unshift(dest);
}
effectiveVj = mergeLoader(vj, ofvj);
}
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
+169 -3
View File
@@ -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.';
}
});
+5
View File
@@ -19,6 +19,9 @@ contextBridge.exposeInMainWorld('api', {
// Minecraft-Versionen (Mojang)
mcVersions: () => ipcRenderer.invoke('mc:versions'),
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
// Mods (Modrinth)
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
@@ -104,6 +107,7 @@ contextBridge.exposeInMainWorld('api', {
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
onLaunchProgress: (cb) => {
@@ -236,4 +240,5 @@ contextBridge.exposeInMainWorld('api', {
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
appInfo: () => ipcRenderer.invoke('app:info'),
appLicense: () => ipcRenderer.invoke('app:license'),
});
+132 -15
View File
@@ -467,7 +467,7 @@ function iconOf(key, size = 44) {
const LOADER_LABEL = {
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
neoforge: 'NeoForge', quilt: 'Quilt',
neoforge: 'NeoForge', quilt: 'Quilt', optifine: 'OptiFine',
};
// Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren
@@ -645,17 +645,28 @@ async function searchModpacks() {
const query = el('mp-query').value.trim();
const gameVersion = el('mp-mc').value.trim();
const sort = el('mp-sort').value || 'relevance';
const source = (el('mp-source') && el('mp-source').value) || 'modrinth';
const box = el('mp-results');
const st = el('mp-status');
st.textContent = 'Suche …';
box.innerHTML = '<div class="mods-empty">Suche läuft …</div>';
try {
const res = await window.api.modpackSearch({
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack',
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack', source,
});
if (res.error) {
st.textContent = 'Fehler: ' + res.error;
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
if (res.needKey) {
const wrap = document.createElement('div');
wrap.className = 'mods-empty';
const btn = document.createElement('button');
btn.className = 'btn btn-mini';
btn.textContent = 'Zu den Netzwerk-Einstellungen';
btn.addEventListener('click', () => { hide('modal-import'); openSettings('pane-network'); });
wrap.appendChild(btn);
box.appendChild(wrap);
}
return;
}
const hits = res.hits || [];
@@ -703,11 +714,15 @@ async function installModpackFromSearch(hit, btn) {
if (btn) { btn.disabled = true; btn.textContent = '…'; }
status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …');
try {
const res = await window.api.modpackInstallFromModrinth({
projectId: hit.projectId,
title: hit.title || hit.slug,
gameVersion,
});
const res = hit.source === 'curseforge'
? await window.api.modpackInstallFromCurseForge({
cfId: hit.cfId, projectId: hit.projectId, title: hit.title || hit.slug, gameVersion,
})
: await window.api.modpackInstallFromModrinth({
projectId: hit.projectId,
title: hit.title || hit.slug,
gameVersion,
});
status('Bereit'); el('status-right').textContent = '';
if (res.ok) {
await reload();
@@ -1786,7 +1801,44 @@ async function updateLoaderVersions(preselect) {
const hint = el('f-loader-hint');
sel.innerHTML = '<option value="">neueste (empfohlen)</option>';
const ofTools = el('f-optifine-tools');
if (ofTools) ofTools.classList.toggle('hidden', loader !== 'optifine');
if (loader === 'vanilla') { sel.disabled = true; hint.textContent = 'Vanilla hat keinen Loader.'; return; }
if (loader === 'optifine') {
// Kein Loader-API bei OptiFine die Liste zeigt bereits eingerichtete
// Fassungen (siehe f-optifine-tools weiter unten), keine automatisch
// ladbaren Versionen.
sel.innerHTML = '<option value=""> keine gewählt </option>';
sel.disabled = false;
if (el('f-optifine-status')) el('f-optifine-status').textContent = '';
if (el('f-optifine-confirm')) el('f-optifine-confirm').classList.add('hidden');
if (!editingId) {
hint.textContent = 'Erst speichern, dann lässt sich OptiFine im Bearbeiten-Dialog einrichten.';
if (el('f-optifine-install')) el('f-optifine-install').disabled = true;
return;
}
if (el('f-optifine-install')) el('f-optifine-install').disabled = false;
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
hint.textContent = 'Suche bereits eingerichtete OptiFine-Fassungen …';
try {
const found = await window.api.optifineFind(mc);
for (const v of found) {
const o = document.createElement('option');
o.value = v; o.textContent = v;
if (v === preselect) o.selected = true;
sel.appendChild(o);
}
hint.textContent = found.length
? `${found.length} eingerichtete OptiFine-Fassung(en) für ${mc}.`
: 'Noch keine OptiFine-Installation für diese Version über den Knopf unten einrichten.';
} catch {
hint.textContent = 'Suche fehlgeschlagen.';
}
return;
}
sel.disabled = false;
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
@@ -2709,14 +2761,23 @@ function renderContentList(boxId, items, sub, emoji) {
// Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader
let modArt = 'mod';
const ART_LABEL = { mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader' };
const ART_LABEL = {
mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader', datapack: 'Datenpakete',
};
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)',
datapack: 'Datenpakete durchsuchen … (z. B. Terralith, Vanilla Tweaks)',
};
// Nur Mods brauchen einen Loader Ressourcenpakete und Shader laufen auch in Vanilla
const ART_BRAUCHT_LOADER = { mod: true, resourcepack: false, shader: false };
// Nur Mods brauchen einen Loader Ressourcenpakete, Shader und Datenpakete laufen auch in Vanilla
const ART_BRAUCHT_LOADER = {
mod: true, resourcepack: false, shader: false, datapack: false,
};
// CurseForge hat für Datenpakete keinen verlässlich zugeordneten API-Klassencode in
// diesem Launcher dort deshalb nur Modrinth als Quelle anbieten, statt falsche
// Treffer (z. B. Mods) unter "Datenpakete" anzuzeigen.
const ART_NUR_MODRINTH = { datapack: true };
async function openMods() {
const inst = await window.api.getInstance(currentDetailId);
@@ -2754,6 +2815,10 @@ async function zeigeModAnsicht(inst) {
['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';
// Manche Quellen decken nicht jede Inhaltsart ab (z. B. CurseForge <-> Datenpakete)
const cfOption = el('m-source').querySelector('option[value="curseforge"]');
if (cfOption) cfOption.disabled = !!ART_NUR_MODRINTH[modArt];
if (ART_NUR_MODRINTH[modArt] && el('m-source').value === 'curseforge') el('m-source').value = 'modrinth';
el('m-query').value = '';
el('m-more').classList.add('hidden');
// Ergebnisse der vorigen Ansicht nicht mitschleppen
@@ -2813,9 +2878,13 @@ async function refreshInstalled() {
if (up) kennzeichen.push('<span class="mod-new">Update</span>');
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
// OptiFine lässt sich nicht über Modrinth/CurseForge installieren (kein API-Zugriff auf
// deren Downloads) von Hand hinzugefügt taucht es sonst wie ein beliebiger Mod auf.
const istOptiFine = modArt === 'mod' && /^optifine[_-]/i.test(m.filename || '');
if (istOptiFine) kennzeichen.push('<span class="mod-off" style="opacity:.8">OptiFine</span>');
item.innerHTML =
`<div class="mod-icon">${aus ? '🚫' : '🧩'}</div>
`<div class="mod-icon">${aus ? '🚫' : istOptiFine ? '⚡' : '🧩'}</div>
<div class="mod-info">
<div class="mod-name">${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}</div>
<div class="mod-meta">${up
@@ -3123,17 +3192,24 @@ async function openSettings(pane) {
['Autor', info.author || 'Nicht hinterlegt'],
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
['Launcher-Webseite', info.launcherWebsite || 'Nicht hinterlegt', info.launcherWebsite || ''],
].map(([label, value, url]) => {
['Lizenz', 'Nur privater, nicht-kommerzieller Gebrauch Details anzeigen', null, 'license'],
].map(([label, value, url, action]) => {
const renderedValue = url
? `<a href="#" class="info-meta-link" data-url="${escapeHtml(url)}">${escapeHtml(value)}</a>`
: escapeHtml(value || '');
: action
? `<a href="#" class="info-meta-link" data-action="${escapeHtml(action)}">${escapeHtml(value)}</a>`
: escapeHtml(value || '');
return `<div class="settings-kv-row"><span class="settings-kv-label">${escapeHtml(label)}</span><span class="settings-kv-value">${renderedValue}</span></div>`;
}).join('');
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
link.addEventListener('click', (evt) => {
link.addEventListener('click', async (evt) => {
evt.preventDefault();
const url = link.dataset.url;
if (url) window.api.openExternal(url);
if (url) { window.api.openExternal(url); return; }
if (link.dataset.action === 'license') {
show('modal-license');
el('license-text').textContent = await window.api.appLicense();
}
});
}
show('modal-settings');
@@ -3859,6 +3935,47 @@ function wire() {
el('f-version-type').addEventListener('change', () => fillVersionSelect(el('f-version').value));
el('f-loader').addEventListener('change', () => updateLoaderVersions());
el('f-version').addEventListener('change', () => updateLoaderVersions());
el('f-optifine-install').addEventListener('click', async () => {
if (!editingId) return;
const btn = el('f-optifine-install');
const status = el('f-optifine-status');
btn.disabled = true;
status.textContent = 'Minecraft wird vorbereitet …';
try {
const res = await window.api.optifineSetup(editingId);
if (res.canceled) { status.textContent = ''; return; }
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Fehlgeschlagen.'); toast(res.message || 'OptiFine-Installer konnte nicht gestartet werden.', true); return; }
status.textContent = `Installer gestartet Zielordner wurde in die Zwischenablage kopiert: ${res.sharedDir} `
+ 'im OptiFine-Fenster als Minecraft-Ordner einfügen, „Install" klicken, danach hier bestätigen.';
el('f-optifine-confirm').classList.remove('hidden');
} finally {
btn.disabled = false;
}
});
el('f-optifine-confirm').addEventListener('click', async () => {
if (!editingId) return;
const btn = el('f-optifine-confirm');
const status = el('f-optifine-status');
btn.disabled = true;
try {
const res = await window.api.optifineConfirm(editingId);
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Nicht gefunden.'); toast(res.message || 'OptiFine-Version wurde nicht gefunden.', true); return; }
toast('OptiFine ' + res.versionId + ' übernommen.');
status.textContent = '✔ Übernommen: ' + res.versionId;
const sel = el('f-loader-version');
if (![...sel.options].some((o) => o.value === res.versionId)) {
const o = document.createElement('option');
o.value = res.versionId; o.textContent = res.versionId;
sel.appendChild(o);
}
sel.value = res.versionId;
btn.classList.add('hidden');
} finally {
btn.disabled = false;
}
});
el('f-pick-image').addEventListener('click', async () => {
const p = await window.api.pickImage();
if (!p) return;
+17 -2
View File
@@ -236,6 +236,7 @@ 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 },
datapack: { projectType: 'datapack', ordner: 'datapacks', label: 'Datenpakete', loaderNoetig: false },
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
};
@@ -515,7 +516,9 @@ function forgeCdnUrl(fileId, fileName) {
}
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552 };
// classId je Inhaltsart bei gameId 432 (Minecraft). modpack=4471 ist die auf
// CurseForge selbst verwendete Kategorie-ID (siehe curseforge.com/minecraft/modpacks).
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552, modpack: 4471 };
function cfLoaderType(loader) {
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
@@ -598,7 +601,12 @@ async function curseforgeSearch(zugang, {
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
};
}
const classId = CF_CLASS[art] || CF_CLASS.mod;
if (!CF_CLASS[art]) {
// Kein zugeordneter CurseForge-Klassencode für diese Inhaltsart -> lieber gar
// keine Treffer als falsch einsortierte (z. B. Mods unter "Datenpakete").
return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht bitte Modrinth nutzen.' };
}
const classId = CF_CLASS[art];
const url = new URL(CURSEFORGE + '/mods/search');
url.searchParams.set('gameId', '432');
url.searchParams.set('classId', String(classId));
@@ -713,6 +721,12 @@ async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
};
}
// Beste Modpack-Datei eines CurseForge-Projekts auflösen (für den Katalog-Import,
// analog zu modrinthResolveModpack).
async function curseforgeResolveModpackFile(zugang, modId, gameVersion) {
return curseforgePickFile(zugang, modId, gameVersion, '', 'modpack');
}
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
const rawId = String(projectId || '').replace(/^cf-/, '');
@@ -814,4 +828,5 @@ module.exports = {
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
curseforgeResolveModpackFile,
};
+6 -2
View File
@@ -134,8 +134,12 @@ function gameDir(id) {
* "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' };
const INHALT_ORDNER = {
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
};
const INHALT_INDEX = {
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
};
function inhaltsDir(id, art = 'mod') {
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
+11 -2
View File
@@ -230,8 +230,17 @@ async function downloadAndInstall(asset, onProgress) {
const dir = updateDir();
fs.mkdirSync(dir, { recursive: true });
const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
fs.rmSync(dest, { force: true });
let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
try {
fs.rmSync(dest, { force: true });
} catch {
// Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
// Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
// EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
// cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
const parsed = path.parse(dest);
dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
}
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);