diff --git a/README.md b/README.md index 53db327..4460b8c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index 1026701..a90983d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/bedrock.js b/src/bedrock.js new file mode 100644 index 0000000..6939aa6 --- /dev/null +++ b/src/bedrock.js @@ -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, +}; diff --git a/src/forge.js b/src/forge.js index 3e44623..7c5fb15 100644 --- a/src/forge.js +++ b/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; } diff --git a/src/index.html b/src/index.html index 1835bad..50b9c6c 100644 --- a/src/index.html +++ b/src/index.html @@ -15,6 +15,7 @@ Modpack Ordner + Bedrock Konsole Skin @@ -213,6 +214,12 @@ Hier werden alle Instanzen als Unterordner (mit ihrem Namen) gespeichert. + + Instanzen übernehmen + Aus MultiMC importieren… + Übernimmt Instanzen samt Version, Loader, Gruppe und Spieldaten – du gibst nur den MultiMC-Ordner an. + + Instanzen sortieren @@ -225,6 +232,11 @@ Block-Symbole räumlich darstellen (3D) + + Bedrock-Bereich anzeigen + + Wird automatisch ausgeblendet, wenn Bedrock nicht installiert ist. + Skin-Hintergrund (optional) Aus @@ -344,8 +356,11 @@ Konten - Microsoft-Konten verwaltest du über den Konten-Button in der Toolbar. - Konten öffnen + + + + Konto hinzufügen + Verwaltung öffnen + @@ -574,6 +589,88 @@ + + + + + Instanzen aus MultiMC übernehmen + ✕ + + + + MultiMC-Ordner + + + Wählen… + Suchen + + Gib den Ordner an, in dem MultiMC.exe liegt. + + + + + + Alle auswählen + + + Spieldaten mitkopieren (Welten, Mods, Konfiguration) + + + + + + + + + Abbrechen + Übernehmen + + + + + + + + + + 🧱 + + Minecraft Bedrock + Wird geprüft … + + + ✕ + + + + ⚠ Bedrock ist nicht installiert. Es wird über den Microsoft Store bezogen – der Launcher kann es nicht selbst installieren. + + + + + ▶ Bedrock starten + Add-on / Welt importieren… + + + + Welten + Ressourcenpakete + Verhaltenspakete + Skin-Pakete + Bildschirmfotos + + + + + + + Ordner öffnen + + Schließen + + + + Symbol ändern diff --git a/src/launcher.js b/src/launcher.js index c3bf969..67a99f9 100644 --- a/src/launcher.js +++ b/src/launcher.js @@ -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) { diff --git a/src/main.js b/src/main.js index b18df02..21e1b13 100644 --- a/src/main.js +++ b/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) // --------------------------------------------------------------------------- diff --git a/src/mmcimport.js b/src/mmcimport.js new file mode 100644 index 0000000..7b0452d --- /dev/null +++ b/src/mmcimport.js @@ -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 }; diff --git a/src/preload.js b/src/preload.js index 9560fce..ee851f2 100644 --- a/src/preload.js +++ b/src/preload.js @@ -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'), diff --git a/src/renderer.js b/src/renderer.js index 95c44d6..8acb821 100644 --- a/src/renderer.js +++ b/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 = 'Keine Instanzen gefunden.'; 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 = + ` + + ${escapeHtml(inst.name)} + ${escapeHtml(meta)} + `; + 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 = 'Lade …'; + 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 = `${leer[kind] || 'Nichts vorhanden.'}`; + 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 = + `${kind === 'worlds' ? '🌍' : kind === 'screenshots' ? '🖼' : '📦'} + + ${escapeHtml(name)} + ${escapeHtml(meta)} + + `; + + 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}` + - `${escapeHtml(info.userData)}`; + `Version ${info.version}${escapeHtml(info.userData)}`; 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 = 'Noch kein Konto angemeldet.'; @@ -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(); diff --git a/src/services.js b/src/services.js index 4f79960..894dc47 100644 --- a/src/services.js +++ b/src/services.js @@ -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, }; diff --git a/src/store.js b/src/store.js index f1d5191..7418141 100644 --- a/src/store.js +++ b/src/store.js @@ -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) diff --git a/src/styles.css b/src/styles.css index 513fb08..de0257c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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}
Microsoft-Konten verwaltest du über den Konten-Button in der Toolbar.