Upload via GUI (21 Dateien)
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
180
src/bedrock.js
Normal 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,
|
||||
};
|
||||
17
src/forge.js
17
src/forge.js
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
101
src/index.html
101
src/index.html
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
71
src/main.js
71
src/main.js
@@ -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
148
src/mmcimport.js
Normal 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 };
|
||||
@@ -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'),
|
||||
|
||||
265
src/renderer.js
265
src/renderer.js
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user