Upload via GUI (21 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-01 19:32:20 +02:00
parent afe02f81be
commit 7a6b31fb11
13 changed files with 858 additions and 20 deletions

View File

@@ -4,7 +4,7 @@
# AeroMC
**Ein instanz-basierter Minecraft-Launcher für Windows**
**Ein instanz-basierter Minecraft-Launcher für Windows à la MultiMC / Prism.**
Verwalte beliebig viele isolierte Minecraft-Instanzen mit eigener Version, eigenen Mods und eigenem Java lade Mods per Ein-Klick von Modrinth, melde dich mit deinem Microsoft-Konto an und spiele.

View File

@@ -1,6 +1,6 @@
{
"name": "aeromc",
"version": "0.33.1",
"version": "0.37.1",
"description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
"main": "src/main.js",
"author": "AeroMC",

180
src/bedrock.js Normal file
View File

@@ -0,0 +1,180 @@
'use strict';
/*
* bedrock.js Unterstützung für Minecraft Bedrock (Windows-Store-App)
* --------------------------------------------------------------------
* Bedrock ist keine Java-Anwendung, sondern eine UWP-App. Der Launcher kann
* sie daher nicht herunterladen oder in Instanzen aufteilen wohl aber
* starten und die Spieldaten (Welten, Add-ons, Pakete, Bildschirmfotos)
* verwalten. Die Installation selbst bleibt Sache des Microsoft Store.
*/
const fs = require('fs');
const path = require('path');
const { execFile, spawn } = require('child_process');
const PKG_FAMILY = 'Microsoft.MinecraftUWP_8wekyb3d8bbwe';
const APP_ID = PKG_FAMILY + '!App';
function comMojangDir() {
return path.join(process.env.LOCALAPPDATA || '', 'Packages', PKG_FAMILY,
'LocalState', 'games', 'com.mojang');
}
// ---------------------------------------------------------------------------
// Erkennung
// ---------------------------------------------------------------------------
function runPwsh(cmd) {
return new Promise((resolve) => {
execFile('powershell', ['-NoProfile', '-NonInteractive', '-Command', cmd],
{ windowsHide: true, timeout: 15000 },
(err, stdout) => resolve(err ? '' : String(stdout || '').trim()));
});
}
async function detect() {
const dir = comMojangDir();
const version = await runPwsh(
"(Get-AppxPackage -Name 'Microsoft.MinecraftUWP' -ErrorAction SilentlyContinue).Version");
const installed = !!version;
return {
installed,
version: version || null,
dataDir: fs.existsSync(dir) ? dir : null,
};
}
// ---------------------------------------------------------------------------
// Start
// ---------------------------------------------------------------------------
/*
* Startet Bedrock direkt.
* Wichtig: Der AppsFolder-Pfad darf NICHT in Anführungszeichen stehen sonst
* öffnet der Explorer nur ein Ordnerfenster. Daher windowsVerbatimArguments.
*/
function launch() {
const target = 'shell:appsFolder\\' + APP_ID;
const versuche = [
// 1) genau der Aufruf, der sich unter Windows als zuverlässig erwiesen hat
['powershell', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command',
`Start-Process explorer.exe -ArgumentList '${target}'`], {}],
// 2) explorer direkt, ohne zusätzliche Anführungszeichen
['explorer.exe', [target], { windowsVerbatimArguments: true }],
// 3) über die Eingabeaufforderung
['cmd', ['/c', 'start', '', target], { windowsVerbatimArguments: true }],
];
for (const [cmd, args, extra] of versuche) {
try {
const p = spawn(cmd, args, Object.assign({ detached: true, stdio: 'ignore', windowsHide: true }, extra));
p.unref();
return { ok: true, method: cmd };
} catch { /* nächsten Weg versuchen */ }
}
return { ok: false, message: 'Bedrock konnte nicht gestartet werden.' };
}
// ---------------------------------------------------------------------------
// Welten / Pakete / Bildschirmfotos
// ---------------------------------------------------------------------------
function readLevelName(worldDir) {
// levelname.txt enthält den im Spiel angezeigten Namen
try {
const p = path.join(worldDir, 'levelname.txt');
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8').trim();
} catch { /* Ordnername verwenden */ }
return path.basename(worldDir);
}
function dirSize(dir, depth = 3) {
let total = 0;
if (depth < 0) return 0;
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return 0; }
for (const e of entries) {
const p = path.join(dir, e.name);
try {
if (e.isDirectory()) total += dirSize(p, depth - 1);
else total += fs.statSync(p).size;
} catch { /* überspringen */ }
}
return total;
}
function listWorlds() {
const base = path.join(comMojangDir(), 'minecraftWorlds');
if (!fs.existsSync(base)) return [];
return fs.readdirSync(base, { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => {
const dir = path.join(base, e.name);
let modified = null;
try { modified = fs.statSync(dir).mtime.toISOString(); } catch { /* egal */ }
return { id: e.name, name: readLevelName(dir), dir, modified, bytes: dirSize(dir) };
})
.sort((a, b) => String(b.modified).localeCompare(String(a.modified)));
}
// Ordnerarten, die verwaltet werden können
const FOLDERS = {
worlds: 'minecraftWorlds',
resource_packs: 'resource_packs',
behavior_packs: 'behavior_packs',
skin_packs: 'skin_packs',
screenshots: 'Screenshots',
};
function listFolder(kind) {
const sub = FOLDERS[kind];
if (!sub) return [];
const base = path.join(comMojangDir(), sub);
if (!fs.existsSync(base)) return [];
return fs.readdirSync(base, { withFileTypes: true })
.filter((e) => !e.name.startsWith('.'))
.map((e) => {
const p = path.join(base, e.name);
let bytes = 0, modified = null;
try {
const st = fs.statSync(p);
modified = st.mtime.toISOString();
bytes = e.isDirectory() ? dirSize(p, 2) : st.size;
} catch { /* egal */ }
return { name: e.name, isDir: e.isDirectory(), path: p, bytes, modified };
});
}
function folderPath(kind) {
const sub = FOLDERS[kind];
return sub ? path.join(comMojangDir(), sub) : null;
}
function removeEntry(kind, name) {
const base = folderPath(kind);
if (!base) return false;
const target = path.join(base, name);
if (!target.startsWith(base + path.sep)) return false; // Pfad-Ausbruch verhindern
if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
return true;
}
/*
* Add-on installieren: .mcpack/.mcaddon/.mcworld sind ZIP-Dateien.
* Der Doppelklick-Weg über die Shell ist der zuverlässigste, weil Minecraft
* die Pakete selbst einsortiert (inkl. Abhängigkeiten).
*/
function installAddon(filePath) {
if (!fs.existsSync(filePath)) return { ok: false, message: 'Datei nicht gefunden.' };
if (!/\.(mcpack|mcaddon|mcworld|mctemplate)$/i.test(filePath)) {
return { ok: false, message: 'Nur .mcpack, .mcaddon, .mcworld oder .mctemplate möglich.' };
}
const child = spawn('cmd', ['/c', 'start', '', filePath], { detached: true, stdio: 'ignore' });
child.unref();
return { ok: true };
}
module.exports = {
detect, launch, listWorlds, listFolder, folderPath, removeEntry, installAddon,
comMojangDir, FOLDERS,
};

View File

@@ -113,6 +113,20 @@ async function installForge(opts) {
const libDir = path.join(sharedDir, 'libraries');
const java = javaPath && javaPath.trim() ? javaPath : 'java';
// Bereits installiert? Dann Prozessoren überspringen (spart ~30 s pro Start)
const markName = `${loader}-${mcVersion}-${loaderVersion}.installed`;
const markPath = path.join(sharedDir, 'versions', markName);
if (fs.existsSync(markPath)) {
try {
const cachedId = fs.readFileSync(markPath, 'utf8').trim();
const vjFile = path.join(sharedDir, 'versions', cachedId, cachedId + '.json');
if (fs.existsSync(vjFile)) {
onProgress({ phase: 'loader', detail: `${loader} ${loaderVersion} bereits installiert` });
return JSON.parse(fs.readFileSync(vjFile, 'utf8'));
}
} catch { /* Merker unbrauchbar -> neu installieren */ }
}
onProgress({ phase: 'loader', detail: `${loader} ${loaderVersion} Installer laden …` });
const buf = await fetchBuffer(installerUrl(loader, mcVersion, loaderVersion));
const zip = readZipIndex(buf);
@@ -226,6 +240,9 @@ async function installForge(opts) {
}
}
// Installation als abgeschlossen vermerken
try { fs.writeFileSync(markPath, versionJson.id); } catch { /* egal */ }
return versionJson;
}

View File

@@ -15,6 +15,7 @@
<button id="btn-modpack" class="tbtn" data-ic="package" title="Modrinth-Modpack (.mrpack)">Modpack</button>
<button id="btn-open-dir" class="tbtn" data-ic="folder" title="Ordner öffnen">Ordner</button>
<span class="tbsep"></span>
<button id="btn-bedrock" class="tbtn" data-ic="blocks" title="Minecraft Bedrock (Windows-Ausgabe)">Bedrock</button>
<button id="btn-console" class="tbtn" data-ic="terminal" title="Konsole / Log">Konsole</button>
<button id="btn-skin" class="tbtn" data-ic="user" title="Skin-Hintergrund ein/aus">Skin</button>
<span class="tbgrow"></span>
@@ -213,6 +214,12 @@
<small class="hint">Hier werden alle Instanzen als Unterordner (mit ihrem Namen) gespeichert.</small>
</label>
<label class="field">
<span>Instanzen übernehmen</span>
<button id="btn-mmc" class="btn">Aus MultiMC importieren…</button>
<small class="hint">Übernimmt Instanzen samt Version, Loader, Gruppe und Spieldaten du gibst nur den MultiMC-Ordner an.</small>
</label>
<label class="field">
<span>Instanzen sortieren</span>
<select id="s-sortby">
@@ -225,6 +232,11 @@
<input id="s-iso3d" type="checkbox" /> <span>Block-Symbole räumlich darstellen (3D)</span>
</label>
<label class="checkbox-field">
<input id="s-bedrock" type="checkbox" /> <span>Bedrock-Bereich anzeigen</span>
</label>
<small id="s-bedrock-hint" class="hint">Wird automatisch ausgeblendet, wenn Bedrock nicht installiert ist.</small>
<label class="field">
<span>Skin-Hintergrund <small class="opt">(optional)</small></span>
<select id="s-bgskin"><option value="">Aus</option></select>
@@ -344,8 +356,11 @@
<div id="pane-accounts" class="spane hidden">
<h4>Konten</h4>
<p class="hint">Microsoft-Konten verwaltest du über den Konten-Button in der Toolbar.</p>
<button id="s-open-accounts" class="btn">Konten öffnen</button>
<div id="s-accounts" class="account-list"></div>
<div class="path-row">
<button id="s-add-account" class="btn btn-primary"> Konto hinzufügen</button>
<button id="s-open-accounts" class="btn">Verwaltung öffnen</button>
</div>
</div>
<div id="pane-info" class="spane hidden">
@@ -574,6 +589,88 @@
</div>
</div>
<!-- ===================== Dialog: MultiMC-Import ===================== -->
<div id="modal-mmc" class="modal hidden">
<div class="modal-card modal-lg">
<div class="modal-head">
<h3>Instanzen aus MultiMC übernehmen</h3>
<button class="modal-close" data-close></button>
</div>
<div class="modal-body">
<label class="field">
<span>MultiMC-Ordner</span>
<div class="path-row">
<input id="mmc-path" type="text" placeholder="z. B. E:\Minecraft\MultiMC" />
<button id="mmc-pick" class="btn">Wählen…</button>
<button id="mmc-scan" class="btn btn-primary">Suchen</button>
</div>
<small id="mmc-status" class="hint">Gib den Ordner an, in dem MultiMC.exe liegt.</small>
</label>
<div id="mmc-result" class="hidden">
<div class="mmc-bar">
<label class="checkbox-field" style="margin:0">
<input id="mmc-all" type="checkbox" checked /> <span>Alle auswählen</span>
</label>
<label class="checkbox-field" style="margin:0">
<input id="mmc-copy" type="checkbox" checked /> <span>Spieldaten mitkopieren (Welten, Mods, Konfiguration)</span>
</label>
</div>
<div id="mmc-list" class="mods-list"></div>
</div>
</div>
<div class="modal-foot">
<div id="mmc-progress" class="hint"></div>
<div class="spacer"></div>
<button class="btn" data-close>Abbrechen</button>
<button id="mmc-do" class="btn btn-primary" disabled>Übernehmen</button>
</div>
</div>
</div>
<!-- ===================== Dialog: Bedrock ===================== -->
<div id="modal-bedrock" class="modal hidden">
<div class="modal-card modal-lg">
<div class="modal-head">
<div class="detail-head">
<span class="detail-icon">🧱</span>
<div>
<h3>Minecraft Bedrock</h3>
<div id="bd-sub" class="detail-sub">Wird geprüft …</div>
</div>
</div>
<button class="modal-close" data-close></button>
</div>
<div class="modal-body">
<div id="bd-missing" class="mods-warn hidden">
⚠ Bedrock ist nicht installiert. Es wird über den Microsoft Store bezogen der Launcher kann es nicht selbst installieren.
</div>
<div id="bd-content">
<div class="bd-actions">
<button id="bd-play" class="btn btn-primary btn-lg">▶ Bedrock starten</button>
<button id="bd-import" class="btn">Add-on / Welt importieren…</button>
</div>
<div class="mods-tabs">
<button class="mtab btab on" data-kind="worlds">Welten</button>
<button class="mtab btab" data-kind="resource_packs">Ressourcenpakete</button>
<button class="mtab btab" data-kind="behavior_packs">Verhaltenspakete</button>
<button class="mtab btab" data-kind="skin_packs">Skin-Pakete</button>
<button class="mtab btab" data-kind="screenshots">Bildschirmfotos</button>
</div>
<div id="bd-list" class="mods-list"></div>
</div>
</div>
<div class="modal-foot">
<button id="bd-folder" class="btn" data-ic="folder">Ordner öffnen</button>
<div class="spacer"></div>
<button class="btn" data-close>Schließen</button>
</div>
</div>
</div>
<!-- ===================== Kontextmenü (Rechtsklick auf Instanz) ===================== -->
<div id="ctx-menu" class="ctx hidden">
<button class="ctx-item" data-act="p-edit">Symbol ändern</button>

View File

@@ -198,6 +198,10 @@ async function prepareAssets(vj, sharedDir, onProgress) {
const ai = vj.assetIndex;
if (!ai) return;
const indexFile = path.join(sharedDir, 'assets', 'indexes', ai.id + '.json');
// Merker: einmal vollständig geprüfte Assets nicht erneut durchgehen (spart ~1,5 s pro Start)
const doneMark = path.join(sharedDir, 'assets', 'indexes', ai.id + '.complete');
if (fs.existsSync(doneMark) && fs.existsSync(indexFile)) return;
await downloadIfNeeded(ai.url, indexFile, ai.sha1);
const index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
const objects = index.objects || {};
@@ -210,6 +214,8 @@ async function prepareAssets(vj, sharedDir, onProgress) {
if (fs.existsSync(dest)) return;
await downloadIfNeeded(`${RESOURCES}/${sub}/${o.hash}`, dest, o.hash);
}, 16, (done, total) => onProgress && onProgress({ phase: 'assets', current: done, total, detail: 'Assets' }));
try { fs.writeFileSync(doneMark, new Date().toISOString()); } catch { /* egal */ }
}
// ---------------------------------------------------------------------------
@@ -347,8 +353,22 @@ async function prepareAndLaunch(opts, onProgress) {
if (opts.loader === 'fabric' || opts.loader === 'quilt') {
emit('loader', { detail: opts.loader + ' vorbereiten …' });
let lp;
try { lp = (await loaders.resolveLoaderProfile(opts.loader, vj.id, opts.loaderVersion)).profile; }
catch (err) { return { ok: false, message: err.message }; }
// Profil zwischenspeichern: spart bei festgelegter Loader-Version die Netzabfrage
const cacheFile = opts.loaderVersion
? path.join(opts.sharedDir, 'loaders', `${opts.loader}-${vj.id}-${opts.loaderVersion}.json`)
: null;
if (cacheFile && fs.existsSync(cacheFile)) {
try { lp = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); } catch { lp = null; }
}
if (!lp) {
try {
const r = await loaders.resolveLoaderProfile(opts.loader, vj.id, opts.loaderVersion);
lp = r.profile;
const target = cacheFile
|| path.join(opts.sharedDir, 'loaders', `${opts.loader}-${vj.id}-${r.loaderVersion}.json`);
try { ensureDir(path.dirname(target)); fs.writeFileSync(target, JSON.stringify(lp)); } catch { /* egal */ }
} catch (err) { return { ok: false, message: err.message }; }
}
const loaderLibs = lp.libraries || [];
let li = 0;
for (const lib of loaderLibs) {

View File

@@ -18,6 +18,8 @@ const forge = require('./forge');
const auth = require('./auth');
const updater = require('./updater');
const icons = require('./icons');
const bedrock = require('./bedrock');
const mmcimport = require('./mmcimport');
// undici als globales fetch verwenden -> ermöglicht Proxy via setGlobalDispatcher
const undici = require('undici');
@@ -101,6 +103,7 @@ function createWindow() {
app.whenReady().then(() => {
store.init(app.getPath('userData'));
services.setCacheDir(app.getPath('userData')); // Java-Suche zwischenspeichern
// Konten verschlüsselt speichern (Windows-DPAPI, an das Benutzerkonto gebunden)
auth.init(app.getPath('userData'), {
available: () => { try { return safeStorage.isEncryptionAvailable(); } catch { return false; } },
@@ -110,6 +113,9 @@ app.whenReady().then(() => {
applyProxy(store.getSettings());
createWindow();
// Java-Suche im Hintergrund vorbereiten -> erster Spielstart ohne Verzögerung
setTimeout(() => { try { services.scanJava(); } catch { /* egal */ } }, 800);
// echte Minecraft-Icons einmalig aus den eigenen Spieldateien holen
setTimeout(() => {
try {
@@ -178,7 +184,7 @@ ipcMain.handle('settings:instancesDir', () => store.getInstancesDir());
ipcMain.handle('java:detect', (_e, javaPath) => detectJava(javaPath));
// automatische Java-Suche über das System
ipcMain.handle('java:scan', () => services.scanJava());
ipcMain.handle('java:scan', () => services.scanJava(false)); // Knopf sucht immer frisch
// ---------------------------------------------------------------------------
// IPC Minecraft-Versionen (Mojang)
@@ -629,6 +635,69 @@ ipcMain.handle('app:openExternal', (_e, url) => {
return true;
});
// ---------------------------------------------------------------------------
// IPC Instanzen aus MultiMC/Prism übernehmen
// ---------------------------------------------------------------------------
ipcMain.handle('mmc:pickFolder', async () => {
const res = await dialog.showOpenDialog(mainWindow, {
title: 'MultiMC-Ordner wählen (der Ordner mit MultiMC.exe)',
properties: ['openDirectory'],
});
return res.canceled ? null : res.filePaths[0];
});
ipcMain.handle('mmc:scan', (_e, folder) => {
try { return mmcimport.scan(folder); }
catch (err) { return { ok: false, reason: err.message }; }
});
ipcMain.handle('mmc:import', (_e, entries, copyData) => {
const done = [];
const failed = [];
for (const entry of entries || []) {
try { done.push(mmcimport.importOne(store, entry, copyData !== false)); }
catch (err) { failed.push({ name: entry.name, message: err.message }); }
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('mmc:progress', { current: done.length + failed.length, total: entries.length });
}
}
return { ok: true, imported: done, failed };
});
// ---------------------------------------------------------------------------
// IPC Minecraft Bedrock (Windows-Store-App)
// ---------------------------------------------------------------------------
ipcMain.handle('bedrock:detect', () => bedrock.detect());
ipcMain.handle('bedrock:launch', () => bedrock.launch());
ipcMain.handle('bedrock:worlds', () => bedrock.listWorlds());
ipcMain.handle('bedrock:list', (_e, kind) => bedrock.listFolder(kind));
ipcMain.handle('bedrock:remove', (_e, kind, name) => bedrock.removeEntry(kind, name));
ipcMain.handle('bedrock:openFolder', (_e, kind) => {
const p = bedrock.folderPath(kind);
if (p) { fs.mkdirSync(p, { recursive: true }); shell.openPath(p); }
return true;
});
ipcMain.handle('bedrock:installAddon', async () => {
const res = await dialog.showOpenDialog(mainWindow, {
title: 'Add-on oder Welt importieren',
properties: ['openFile'],
filters: [{ name: 'Bedrock-Pakete', extensions: ['mcpack', 'mcaddon', 'mcworld', 'mctemplate'] }],
});
if (res.canceled) return { ok: false, canceled: true };
return bedrock.installAddon(res.filePaths[0]);
});
// Weltordner im Explorer zeigen
ipcMain.handle('bedrock:openWorld', (_e, dir) => {
const base = bedrock.folderPath('worlds');
if (base && dir && dir.startsWith(base) && fs.existsSync(dir)) shell.openPath(dir);
return true;
});
// ---------------------------------------------------------------------------
// IPC echte Minecraft-Icons (aus den eigenen Spieldateien)
// ---------------------------------------------------------------------------

148
src/mmcimport.js Normal file
View File

@@ -0,0 +1,148 @@
'use strict';
/*
* mmcimport.js Instanzen aus MultiMC/Prism übernehmen
* ------------------------------------------------------
* Es genügt der Pfad zum MultiMC-Ordner (oder direkt zum instances-Ordner).
* Ausgelesen werden:
* instance.cfg Name, Gruppe, Java-Pfad, Arbeitsspeicher, Notizen
* mmc-pack.json Minecraft-Version und Mod-Loader
* .minecraft/ Spielverzeichnis (Welten, Mods, Konfigurationen)
*/
const fs = require('fs');
const path = require('path');
// uid der Loader in mmc-pack.json -> unsere Bezeichnung
const LOADER_UIDS = {
'net.fabricmc.fabric-loader': 'fabric',
'org.quiltmc.quilt-loader': 'quilt',
'net.minecraftforge': 'forge',
'net.neoforged': 'neoforge',
};
function parseCfg(file) {
const out = {};
try {
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
const i = line.indexOf('=');
if (i > 0) out[line.slice(0, i).trim()] = line.slice(i + 1).trim();
}
} catch { /* Datei fehlt */ }
return out;
}
// Gruppen stehen zentral in instances/instgroups.json
function readGroups(instancesDir) {
const map = {};
try {
const raw = JSON.parse(fs.readFileSync(path.join(instancesDir, 'instgroups.json'), 'utf8'));
for (const [group, data] of Object.entries(raw.groups || {})) {
for (const inst of data.instances || []) map[inst] = group;
}
} catch { /* keine Gruppen hinterlegt */ }
return map;
}
function readPack(dir) {
const res = { version: '', loader: 'vanilla', loaderVersion: '' };
try {
const pack = JSON.parse(fs.readFileSync(path.join(dir, 'mmc-pack.json'), 'utf8'));
for (const c of pack.components || []) {
if (c.uid === 'net.minecraft') res.version = c.version || c.cachedVersion || '';
const loader = LOADER_UIDS[c.uid];
if (loader) { res.loader = loader; res.loaderVersion = c.version || ''; }
}
} catch { /* alte Instanz ohne mmc-pack.json */ }
return res;
}
// findet den instances-Ordner, egal ob MultiMC-Wurzel oder instances direkt angegeben wurde
function resolveInstancesDir(inputPath) {
if (!inputPath) return null;
const direct = path.join(inputPath, 'instances');
if (fs.existsSync(direct)) return direct;
if (path.basename(inputPath).toLowerCase() === 'instances' && fs.existsSync(inputPath)) return inputPath;
return fs.existsSync(inputPath) ? inputPath : null;
}
/*
* Listet die gefundenen Instanzen auf (ohne etwas zu kopieren).
*/
function scan(inputPath) {
const instancesDir = resolveInstancesDir(inputPath);
if (!instancesDir) return { ok: false, reason: 'not-found' };
const groups = readGroups(instancesDir);
const list = [];
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const dir = path.join(instancesDir, entry.name);
const cfgFile = path.join(dir, 'instance.cfg');
if (!fs.existsSync(cfgFile)) continue; // kein Instanzordner
const cfg = parseCfg(cfgFile);
const pack = readPack(dir);
// MultiMC nutzt .minecraft, Prism minecraft
const gameDir = ['.minecraft', 'minecraft']
.map((n) => path.join(dir, n))
.find((p) => fs.existsSync(p)) || null;
list.push({
folder: entry.name,
dir,
name: cfg.name || entry.name,
group: groups[entry.name] || '',
notes: cfg.notes || '',
version: pack.version || cfg.IntendedVersion || '',
loader: pack.loader,
loaderVersion: pack.loaderVersion,
javaPath: cfg.OverrideJavaLocation === 'true' ? (cfg.JavaPath || '') : '',
minMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MinMemAlloc) || null : null,
maxMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MaxMemAlloc) || null : null,
gameDir,
hasGameData: !!gameDir,
});
}
list.sort((a, b) => a.name.localeCompare(b.name));
return { ok: true, instancesDir, count: list.length, instances: list };
}
/*
* Übernimmt eine Instanz in den Launcher.
* store: unser Datenspeicher; copyData: Spielverzeichnis mitkopieren
*/
function importOne(store, entry, copyData = true) {
const created = store.createInstance({
name: entry.name,
group: entry.group,
notes: entry.notes,
minecraft: {
version: entry.version,
loader: entry.loader,
loaderVersion: entry.loaderVersion,
},
});
const patch = {};
if (entry.javaPath || entry.minMemMb || entry.maxMemMb) {
patch.java = {
path: entry.javaPath || '',
minMemMb: entry.minMemMb || null,
maxMemMb: entry.maxMemMb || null,
extraArgs: '',
};
}
if (Object.keys(patch).length) store.updateInstance(created.id, patch);
let copied = false;
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
try {
fs.cpSync(entry.gameDir, store.gameDir(created.id), { recursive: true });
copied = true;
} catch { /* Spieldaten konnten nicht kopiert werden */ }
}
return { id: created.id, name: created.name, copied };
}
module.exports = { scan, importOne, resolveInstancesDir };

View File

@@ -86,6 +86,26 @@ contextBridge.exposeInMainWorld('api', {
},
// Dialoge / App
// MultiMC-Import
mmcPickFolder: () => ipcRenderer.invoke('mmc:pickFolder'),
mmcScan: (folder) => ipcRenderer.invoke('mmc:scan', folder),
mmcImport: (entries, copyData) => ipcRenderer.invoke('mmc:import', entries, copyData),
onMmcProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('mmc:progress', l);
return () => ipcRenderer.removeListener('mmc:progress', l);
},
// Bedrock (Windows-Store-Ausgabe)
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
bedrockLaunch: () => ipcRenderer.invoke('bedrock:launch'),
bedrockWorlds: () => ipcRenderer.invoke('bedrock:worlds'),
bedrockList: (kind) => ipcRenderer.invoke('bedrock:list', kind),
bedrockRemove: (kind, name) => ipcRenderer.invoke('bedrock:remove', kind, name),
bedrockOpenFolder: (kind) => ipcRenderer.invoke('bedrock:openFolder', kind),
bedrockInstallAddon: () => ipcRenderer.invoke('bedrock:installAddon'),
bedrockOpenWorld: (dir) => ipcRenderer.invoke('bedrock:openWorld', dir),
// echte Minecraft-Icons
iconsList: () => ipcRenderer.invoke('icons:list'),
iconsExtract: () => ipcRenderer.invoke('icons:extract'),

View File

@@ -256,6 +256,194 @@ function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark');
}
// ---------- Instanzen aus MultiMC übernehmen ----------
let mmcFound = [];
function openMmc() {
el('mmc-result').classList.add('hidden');
el('mmc-do').disabled = true;
el('mmc-progress').textContent = '';
el('mmc-status').textContent = 'Gib den Ordner an, in dem MultiMC.exe liegt.';
show('modal-mmc');
el('mmc-path').focus();
}
async function scanMmc() {
const folder = el('mmc-path').value.trim();
if (!folder) { el('mmc-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; }
el('mmc-status').textContent = 'Suche Instanzen …';
const res = await window.api.mmcScan(folder);
if (!res.ok) {
el('mmc-status').textContent = '✖ Dort wurde kein instances-Ordner gefunden.';
el('mmc-result').classList.add('hidden');
el('mmc-do').disabled = true;
return;
}
mmcFound = res.instances;
el('mmc-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden.`;
el('mmc-result').classList.remove('hidden');
el('mmc-all').checked = true;
renderMmcList();
}
function renderMmcList() {
const box = el('mmc-list');
box.innerHTML = '';
if (!mmcFound.length) { box.innerHTML = '<div class="mods-empty">Keine Instanzen gefunden.</div>'; return; }
mmcFound.forEach((inst, idx) => {
const row = document.createElement('label');
row.className = 'mmc-item';
const loaderTxt = inst.loader === 'vanilla'
? 'Vanilla'
: (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : '');
const meta = [inst.version || 'ohne Version', loaderTxt, inst.group ? 'Gruppe: ' + inst.group : '']
.filter(Boolean).join(' · ');
row.innerHTML =
`<input type="checkbox" data-idx="${idx}" checked />
<div class="mod-info">
<div class="mmc-name">${escapeHtml(inst.name)}</div>
<div class="mmc-meta">${escapeHtml(meta)}</div>
</div>`;
box.appendChild(row);
});
updateMmcButton();
}
function selectedMmc() {
return [...document.querySelectorAll('#mmc-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => mmcFound[Number(c.dataset.idx)]);
}
function updateMmcButton() {
const n = selectedMmc().length;
el('mmc-do').disabled = n === 0;
el('mmc-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runMmcImport() {
const entries = selectedMmc();
if (!entries.length) return;
const copy = el('mmc-copy').checked;
el('mmc-do').disabled = true;
el('mmc-progress').textContent = 'Übernehme …';
const res = await window.api.mmcImport(entries, copy);
await reload();
hide('modal-mmc');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} Instanz${n === 1 ? '' : 'en'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
// ---------- Bedrock (Windows-Store-Ausgabe) ----------
let bedrockKind = 'worlds';
// Bedrock-Knopf nur zeigen, wenn aktiviert UND tatsächlich installiert
async function applyBedrockVisibility() {
const btn = el('btn-bedrock');
let show = false;
try {
const s = await window.api.getSettings();
if (s.bedrockEnabled !== false) {
const info = await window.api.bedrockDetect();
show = !!info.installed;
const hint = el('s-bedrock-hint');
if (hint) {
hint.textContent = info.installed
? 'Bedrock erkannt (Version ' + info.version + ').'
: 'Bedrock ist auf diesem PC nicht installiert der Bereich bleibt ausgeblendet.';
}
}
} catch { /* im Zweifel ausblenden */ }
btn.classList.toggle('hidden', !show);
}
async function openBedrock() {
show('modal-bedrock');
el('bd-sub').textContent = 'Wird geprüft …';
const info = await window.api.bedrockDetect();
if (!info.installed) {
el('bd-sub').textContent = 'nicht installiert';
el('bd-missing').classList.remove('hidden');
el('bd-content').classList.add('hidden');
return;
}
el('bd-missing').classList.add('hidden');
el('bd-content').classList.remove('hidden');
el('bd-sub').textContent = 'Version ' + info.version;
switchBedrockTab('worlds');
}
function switchBedrockTab(kind) {
bedrockKind = kind;
document.querySelectorAll('.btab').forEach((t) => t.classList.toggle('on', t.dataset.kind === kind));
renderBedrockList();
}
function fmtBytes(n) {
if (!n) return '';
if (n < 1024) return n + ' B';
if (n < 1048576) return (n / 1024).toFixed(0) + ' KB';
return (n / 1048576).toFixed(1) + ' MB';
}
async function renderBedrockList() {
const box = el('bd-list');
box.innerHTML = '<div class="mods-empty">Lade …</div>';
const kind = bedrockKind;
const items = kind === 'worlds' ? await window.api.bedrockWorlds() : await window.api.bedrockList(kind);
box.innerHTML = '';
if (!items.length) {
const leer = {
worlds: 'Noch keine Welten vorhanden.',
resource_packs: 'Keine Ressourcenpakete installiert.',
behavior_packs: 'Keine Verhaltenspakete installiert.',
skin_packs: 'Keine Skin-Pakete installiert.',
screenshots: 'Noch keine Bildschirmfotos.',
};
box.innerHTML = `<div class="mods-empty">${leer[kind] || 'Nichts vorhanden.'}</div>`;
return;
}
for (const it of items) {
const name = it.name || it.id;
const item = document.createElement('div');
item.className = 'mod-item';
const meta = [fmtBytes(it.bytes), it.modified ? fmtDate(it.modified) : ''].filter(Boolean).join(' · ');
item.innerHTML =
`<div class="mod-icon">${kind === 'worlds' ? '🌍' : kind === 'screenshots' ? '🖼' : '📦'}</div>
<div class="mod-info">
<div class="mod-name">${escapeHtml(name)}</div>
<div class="bd-meta">${escapeHtml(meta)}</div>
</div>
<div class="mod-action"></div>`;
const actions = item.querySelector('.mod-action');
if (kind === 'worlds') {
const open = document.createElement('button');
open.className = 'btn btn-mini';
open.textContent = 'Ordner';
open.addEventListener('click', () => window.api.bedrockOpenWorld(it.dir));
actions.appendChild(open);
}
const del = document.createElement('button');
del.className = 'btn btn-danger btn-mini';
del.textContent = 'Löschen';
del.addEventListener('click', () => askConfirm(
'Löschen', `${name}" wird unwiderruflich gelöscht. Fortfahren?`, 'Endgültig löschen',
async () => {
await window.api.bedrockRemove(kind, kind === 'worlds' ? it.id : it.name);
await renderBedrockList();
toast('Gelöscht: ' + name);
}));
actions.appendChild(del);
box.appendChild(item);
}
}
// ---------- Kontextmenü (Rechtsklick auf eine Instanz) ----------
function openContextMenu(x, y) {
const menu = el('ctx-menu');
@@ -1222,6 +1410,7 @@ async function openSettings() {
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
el('s-sortby').value = s.sortBy || 'lastPlayed';
el('s-iso3d').checked = s.iso3dIcons !== false;
el('s-bedrock').checked = s.bedrockEnabled !== false;
el('s-console-launch').checked = !!s.showConsoleOnLaunch;
el('s-console-autoclose').checked = !!s.autoCloseConsole;
el('s-console-crash').checked = s.showConsoleOnCrash !== false;
@@ -1245,16 +1434,24 @@ async function openSettings() {
const info = await window.api.appInfo();
el('s-appinfo').innerHTML =
`AeroMC ${info.version} · Electron ${info.electron} · Node ${info.node}<br>` +
`<span class="mono">${escapeHtml(info.userData)}</span>`;
`Version ${info.version}<br><span class="mono">${escapeHtml(info.userData)}</span>`;
show('modal-settings');
await renderAccounts('s-accounts'); // Konten direkt in den Einstellungen zeigen
applyBedrockVisibility(); // aktualisiert auch den Hinweistext
}
async function refreshJavaStatus(javaPath) {
el('s-java-status').textContent = 'Prüfe Java …';
const box = el('s-java-status');
box.className = 'hint';
box.textContent = 'Prüfe Java …';
const r = await window.api.detectJava(javaPath);
el('s-java-status').textContent = r.ok ? '✔ ' + r.version : '✖ Java nicht gefunden (' + (r.error || 'unbekannt') + ')';
el('s-java-status').style.color = r.ok ? 'var(--accent-hover)' : 'var(--danger-hover)';
if (r.ok) {
box.classList.add('status-ok'); // grün, wenn Java passt
box.textContent = '✔ ' + r.version;
} else {
box.classList.add('status-bad');
box.textContent = '✖ Java nicht gefunden (' + (r.error || 'unbekannt') + ')';
}
}
async function scanJava() {
@@ -1287,6 +1484,7 @@ async function saveSettings() {
bgSkinPose: el('s-bgskin-pose').value,
sortBy: el('s-sortby').value,
iso3dIcons: el('s-iso3d').checked,
bedrockEnabled: el('s-bedrock').checked,
showConsoleOnLaunch: el('s-console-launch').checked,
autoCloseConsole: el('s-console-autoclose').checked,
showConsoleOnCrash: el('s-console-crash').checked,
@@ -1309,6 +1507,7 @@ async function saveSettings() {
await window.api.saveSettings(patch);
applyTheme(patch.theme);
iso3dEnabled = patch.iso3dIcons;
await applyBedrockVisibility();
await applySkinBackground();
hide('modal-settings');
toast('Einstellungen gespeichert.');
@@ -1333,9 +1532,16 @@ async function refreshAccountButton() {
}
}
async function renderAccounts() {
// aktualisiert Kontoliste im Login-Dialog und in den Einstellungen
async function refreshAllAccountLists() {
await renderAccounts('login-accounts');
if (!el('modal-settings').classList.contains('hidden')) await renderAccounts('s-accounts');
}
async function renderAccounts(boxId = 'login-accounts') {
const accounts = await window.api.authAccounts();
const box = el('login-accounts');
const box = el(boxId);
if (!box) return;
box.innerHTML = '';
if (!accounts.length) {
box.innerHTML = '<div class="account-empty">Noch kein Konto angemeldet.</div>';
@@ -1369,7 +1575,7 @@ async function renderAccounts() {
use.textContent = 'Aktivieren';
use.addEventListener('click', async () => {
await window.api.authSetActive(a.id);
await renderAccounts();
await refreshAllAccountLists();
await refreshAccountButton();
await applySkinBackground();
toast('Aktiv: ' + a.name);
@@ -1381,7 +1587,7 @@ async function renderAccounts() {
rm.textContent = 'Entfernen';
rm.addEventListener('click', async () => {
await window.api.authRemove(a.id);
await renderAccounts();
await refreshAllAccountLists();
await refreshAccountButton();
toast('Entfernt: ' + a.name);
});
@@ -1399,7 +1605,7 @@ function resetLoginHint() {
async function openLogin() {
el('login-code').classList.add('hidden');
resetLoginHint();
await renderAccounts();
await refreshAllAccountLists();
show('modal-login');
}
@@ -1414,7 +1620,7 @@ async function addAccount() {
const res = await window.api.authLogin(); // läuft bis zur Bestätigung
el('login-code').classList.add('hidden');
if (res.ok) {
await renderAccounts();
await refreshAllAccountLists();
await refreshAccountButton();
toast('Angemeldet als ' + res.account.profile.name);
} else {
@@ -1516,6 +1722,41 @@ function wire() {
if (h.ok) toast('Anmelde-Dienste wieder erreichbar.');
});
// MultiMC-Import
el('btn-mmc').addEventListener('click', () => { hide('modal-settings'); openMmc(); });
el('mmc-pick').addEventListener('click', async () => {
const p = await window.api.mmcPickFolder();
if (p) { el('mmc-path').value = p; await scanMmc(); }
});
el('mmc-scan').addEventListener('click', scanMmc);
el('mmc-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanMmc(); });
el('mmc-all').addEventListener('change', (e) => {
document.querySelectorAll('#mmc-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateMmcButton();
});
el('mmc-list').addEventListener('change', updateMmcButton);
el('mmc-do').addEventListener('click', runMmcImport);
window.api.onMmcProgress((p) => {
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
});
// Bedrock
el('btn-bedrock').addEventListener('click', openBedrock);
el('bd-play').addEventListener('click', async () => {
const res = await window.api.bedrockLaunch();
if (res && res.ok) toast('Bedrock wird gestartet …');
else toast('Start fehlgeschlagen: ' + ((res && res.message) || 'unbekannter Fehler'), true);
});
el('bd-import').addEventListener('click', async () => {
const res = await window.api.bedrockInstallAddon();
if (res.canceled) return;
if (res.ok) toast('Import gestartet Minecraft übernimmt die Installation.');
else toast(res.message || 'Import fehlgeschlagen.', true);
});
el('bd-folder').addEventListener('click', () => window.api.bedrockOpenFolder(bedrockKind));
document.querySelectorAll('.btab').forEach((t) =>
t.addEventListener('click', () => switchBedrockTab(t.dataset.kind)));
// Konsole
el('btn-console').addEventListener('click', openConsole);
el('console-clear').addEventListener('click', () => { consoleBuffer = ''; el('console-out').textContent = ''; });
@@ -1631,6 +1872,7 @@ function wire() {
document.querySelectorAll('.spane').forEach((p) => p.classList.toggle('hidden', p.id !== b.dataset.pane));
}));
el('s-open-accounts').addEventListener('click', () => { hide('modal-settings'); openLogin(); });
el('s-add-account').addEventListener('click', () => { hide('modal-settings'); openLogin().then(addAccount); });
// Updates
el('upd-install').addEventListener('click', startUpdate);
@@ -1699,6 +1941,7 @@ window.addEventListener('DOMContentLoaded', async () => {
window.api.onUpdateProgress(onUpdateProgress);
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
await applyBedrockVisibility();
await refreshAccountButton();
await reload();

View File

@@ -82,7 +82,34 @@ function parseJavaVersion(javaPath, out) {
return { path: javaPath, version: raw || 'unbekannt', major: major || 0, arch, info: vendorLine };
}
function scanJava() {
// Ergebnis des Suchlaufs zwischenspeichern die Suche kostet ~1 s
let javaCache = null;
let javaCacheFile = null;
function setCacheDir(dir) { javaCacheFile = path.join(dir, 'java-cache.json'); }
function scanJava(useCache = true) {
const MAX_AGE = 24 * 3600 * 1000;
if (useCache) {
if (javaCache && Date.now() - javaCache.at < MAX_AGE) return javaCache.list;
// Cache von der Platte (gilt auch direkt nach dem Start)
if (javaCacheFile) {
try {
const c = JSON.parse(fs.readFileSync(javaCacheFile, 'utf8'));
if (c && Date.now() - c.at < MAX_AGE && Array.isArray(c.list)
&& c.list.every((j) => fs.existsSync(j.path))) {
javaCache = c;
return c.list;
}
} catch { /* kein oder unbrauchbarer Cache */ }
}
}
const list = scanJavaUncached();
javaCache = { at: Date.now(), list };
if (javaCacheFile) { try { fs.writeFileSync(javaCacheFile, JSON.stringify(javaCache)); } catch { /* egal */ } }
return list;
}
function scanJavaUncached() {
const found = new Set();
// 1) JAVA_HOME
@@ -257,7 +284,7 @@ async function modrinthProjectTitle(projectId) {
}
module.exports = {
scanJava, probeJava,
scanJava, probeJava, setCacheDir,
getMcVersions,
modrinthSearch, modrinthResolveFile, modrinthInstall, modrinthProjectTitle, downloadFile,
};

View File

@@ -65,6 +65,7 @@ const DEFAULT_SETTINGS = {
mcHeight: 480,
sortBy: 'lastPlayed', // lastPlayed | name
iso3dIcons: true, // Block-Symbole räumlich darstellen
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)

View File

@@ -246,6 +246,19 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
.mod-meta{font-size:11px;color:var(--dim);margin-top:2px}
.mod-action{flex:none}
/* MultiMC-Import */
.mmc-bar{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
.mmc-item{display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:3px}
.mmc-item+.mmc-item{border-top:1px solid var(--border2)}
.mmc-item:hover{background:rgba(128,128,128,.10)}
.mmc-item input{width:16px;height:16px;flex:none}
.mmc-name{font-weight:600;font-size:13px}
.mmc-meta{font-size:11px;color:var(--dim)}
/* Bedrock */
.bd-actions{display:flex;gap:8px;align-items:center;margin-bottom:14px}
.bd-meta{font-size:11px;color:var(--dim);margin-top:2px}
/* Login / Konten */
.login-code{text-align:center}
.login-link{color:var(--link);font-size:15px;display:inline-block;margin-bottom:12px;cursor:pointer;text-decoration:none}
@@ -285,6 +298,9 @@ img.account-av{object-fit:cover;image-rendering:pixelated}
.snav.on{background:var(--sel);border-color:var(--sel-br);color:var(--text)}
.settings-content{flex:1;padding:16px 18px;overflow:auto}
.spane h4{margin:0 0 14px;font-size:14px;font-weight:600}
.status-ok{color:#2e8b2e!important;font-weight:600}
.status-bad{color:var(--danger)!important;font-weight:600}
:root[data-theme="dark"] .status-ok{color:#5cd65c!important}
.checkbox-field{display:flex;align-items:center;gap:8px;margin-bottom:14px;cursor:pointer;font-size:13px}
.checkbox-field input{width:16px;height:16px}
.field-port{max-width:110px;flex:none}