Upload via GUI (18 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-01 07:43:21 +02:00
parent 1978358f21
commit afe02f81be
10 changed files with 721 additions and 27 deletions

View File

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

121
src/icons.js Normal file
View File

@@ -0,0 +1,121 @@
'use strict';
/*
* icons.js echte Minecraft-Item-Icons
* -------------------------------------
* Die Original-Texturen werden aus der bereits heruntergeladenen client.jar
* des Nutzers extrahiert (also aus seinen eigenen Spieldateien) und lokal
* als Icons abgelegt. Es werden keine fremden Assets mitgeliefert.
*/
const fs = require('fs');
const path = require('path');
const forge = require('./forge');
// Auswahl gut erkennbarer 16x16-Texturen (Pfade im client.jar)
const WANTED = [
['grass', 'block/grass_block_side.png', 'Grasblock'],
['dirt', 'block/dirt.png', 'Erde'],
['stone', 'block/cobblestone.png', 'Bruchstein'],
['planks', 'block/oak_planks.png', 'Holzbretter'],
['tnt', 'block/tnt_side.png', 'TNT'],
['obsidian', 'block/obsidian.png', 'Obsidian'],
['netherrack', 'block/netherrack.png', 'Netherrack'],
['crafting', 'block/crafting_table_front.png', 'Werkbank'],
['furnace', 'block/furnace_front.png', 'Ofen'],
['bookshelf', 'block/bookshelf.png', 'Bücherregal'],
['diamondblock', 'block/diamond_block.png', 'Diamantblock'],
['goldblock', 'block/gold_block.png', 'Goldblock'],
['emeraldblock', 'block/emerald_block.png', 'Smaragdblock'],
['redstoneblock', 'block/redstone_block.png', 'Redstoneblock'],
['diamond', 'item/diamond.png', 'Diamant'],
['emerald', 'item/emerald.png', 'Smaragd'],
['gold', 'item/gold_ingot.png', 'Goldbarren'],
['iron', 'item/iron_ingot.png', 'Eisenbarren'],
['redstone', 'item/redstone.png', 'Redstone'],
['coal', 'item/coal.png', 'Kohle'],
['sword', 'item/diamond_sword.png', 'Diamantschwert'],
['pickaxe', 'item/diamond_pickaxe.png', 'Diamantspitzhacke'],
['axe', 'item/diamond_axe.png', 'Diamantaxt'],
['bow', 'item/bow.png', 'Bogen'],
['apple', 'item/apple.png', 'Apfel'],
['goldenapple', 'item/golden_apple.png', 'Goldener Apfel'],
['book', 'item/book.png', 'Buch'],
['enchantedbook', 'item/enchanted_book.png', 'Verzaubertes Buch'],
['map', 'item/map.png', 'Karte'],
['star', 'item/nether_star.png', 'Netherstern'],
['endereye', 'item/ender_eye.png', 'Enderauge'],
['enderpearl', 'item/ender_pearl.png', 'Enderperle'],
['blaze', 'item/blaze_powder.png', 'Lohenstaub'],
['xp', 'item/experience_bottle.png', 'Erfahrungsfläschchen'],
['cake', 'item/cake.png', 'Kuchen'],
['bucket', 'item/water_bucket.png', 'Wassereimer'],
['lava', 'item/lava_bucket.png', 'Lavaeimer'],
['tnt_item', 'item/gunpowder.png', 'Schwarzpulver'],
];
// sucht die neueste vorhandene client.jar (auch im Datenordner vor der Umbenennung)
function findClientJars(sharedDirs) {
const jars = [];
for (const shared of sharedDirs) {
const vdir = path.join(shared, 'versions');
if (!fs.existsSync(vdir)) continue;
for (const entry of fs.readdirSync(vdir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const jar = path.join(vdir, entry.name, entry.name + '.jar');
try {
if (fs.existsSync(jar) && fs.statSync(jar).size > 1024 * 1024) {
jars.push({ version: entry.name, path: jar, mtime: fs.statSync(jar).mtimeMs });
}
} catch { /* überspringen */ }
}
}
jars.sort((a, b) => b.mtime - a.mtime);
return jars;
}
/*
* Extrahiert die Texturen in <outDir>. Gibt die Liste der gefundenen Icons zurück.
*/
function extractIcons(sharedDirs, outDir) {
const jars = findClientJars(sharedDirs);
if (!jars.length) return { ok: false, reason: 'no-jar' };
const jar = jars[0];
const buf = fs.readFileSync(jar.path);
const zip = forge.readZipIndex(buf);
fs.mkdirSync(outDir, { recursive: true });
const found = [];
for (const [key, rel, label] of WANTED) {
const entry = zip['assets/minecraft/textures/' + rel];
if (!entry) continue;
try {
const png = forge.readZipEntry(buf, entry);
// sehr große Dateien überspringen (animierte Texturen sind gestreckt)
if (png.length > 64 * 1024) continue;
const dest = path.join(outDir, key + '.png');
fs.writeFileSync(dest, png);
found.push({ key, label, file: dest });
} catch { /* Textur überspringen */ }
}
return { ok: true, version: jar.version, count: found.length, icons: found };
}
// liest die extrahierten Icons als data:-URIs (für die Anzeige)
function listIcons(outDir) {
if (!fs.existsSync(outDir)) return [];
const labels = Object.fromEntries(WANTED.map(([k, , l]) => [k, l]));
return fs.readdirSync(outDir)
.filter((f) => f.endsWith('.png'))
.map((f) => {
const key = path.basename(f, '.png');
try {
const data = fs.readFileSync(path.join(outDir, f)).toString('base64');
return { key, label: labels[key] || key, dataUri: 'data:image/png;base64,' + data };
} catch { return null; }
})
.filter(Boolean);
}
module.exports = { extractIcons, listIcons, findClientJars, WANTED };

View File

@@ -187,6 +187,7 @@
<button class="snav on" data-pane="pane-launcher">Launcher</button>
<button class="snav" data-pane="pane-minecraft">Minecraft</button>
<button class="snav" data-pane="pane-java">Java</button>
<button class="snav" data-pane="pane-console">Konsole</button>
<button class="snav" data-pane="pane-commands">Befehle</button>
<button class="snav" data-pane="pane-external">Externe Programme</button>
<button class="snav" data-pane="pane-proxy">Proxy</button>
@@ -212,6 +213,18 @@
<small class="hint">Hier werden alle Instanzen als Unterordner (mit ihrem Namen) gespeichert.</small>
</label>
<label class="field">
<span>Instanzen sortieren</span>
<select id="s-sortby">
<option value="lastPlayed">Nach letztem Start</option>
<option value="name">Nach Name</option>
</select>
</label>
<label class="checkbox-field">
<input id="s-iso3d" type="checkbox" /> <span>Block-Symbole räumlich darstellen (3D)</span>
</label>
<label class="field">
<span>Skin-Hintergrund <small class="opt">(optional)</small></span>
<select id="s-bgskin"><option value="">Aus</option></select>
@@ -248,6 +261,15 @@
</label>
</div>
<small class="hint">Gilt für alle Instanzen ohne eigene RAM-Einstellung.</small>
<h4 style="margin-top:18px">Fenstergröße</h4>
<label class="checkbox-field">
<input id="s-mc-max" type="checkbox" /> <span>Minecraft maximiert starten</span>
</label>
<div class="field-row">
<label class="field"><span>Fensterbreite</span><input id="s-mc-width" type="number" min="320" step="10" /></label>
<label class="field"><span>Fensterhöhe</span><input id="s-mc-height" type="number" min="240" step="10" /></label>
</div>
</div>
<div id="pane-java" class="spane hidden">
@@ -266,6 +288,23 @@
</label>
</div>
<div id="pane-console" class="spane hidden">
<h4>Konsole</h4>
<label class="checkbox-field">
<input id="s-console-launch" type="checkbox" /> <span>Konsole anzeigen, während das Spiel läuft</span>
</label>
<label class="checkbox-field">
<input id="s-console-autoclose" type="checkbox" /> <span>Konsole automatisch schließen, nachdem das Spiel beendet wurde</span>
</label>
<label class="checkbox-field">
<input id="s-console-crash" type="checkbox" /> <span>Konsole anzeigen, wenn das Spiel abstürzt</span>
</label>
<label class="field">
<span>Historie-Limit (Zeilen)</span>
<input id="s-console-lines" type="number" min="1000" step="1000" />
</label>
</div>
<div id="pane-commands" class="spane hidden">
<h4>Benutzerdefinierte Befehle</h4>
<label class="field">
@@ -311,6 +350,10 @@
<div id="pane-info" class="spane hidden">
<h4>Über AeroMC</h4>
<div class="field">
<button id="s-check-update" class="btn">Nach Updates suchen</button>
<small id="s-update-status" class="hint"></small>
</div>
<div class="about-brand">
<img src="assets/logo.png" alt="" class="about-logo" />
<div>
@@ -504,6 +547,58 @@
</div>
</div>
<!-- ===================== Dialog: Update ===================== -->
<div id="modal-update" class="modal hidden">
<div class="modal-card modal-sm">
<div class="modal-head">
<h3>Update verfügbar</h3>
<button class="modal-close" data-close></button>
</div>
<div class="modal-body">
<p class="upd-version">
<span id="upd-current" class="upd-old"></span>
<span class="upd-arrow"></span>
<span id="upd-new" class="upd-newv"></span>
</p>
<div id="upd-notes" class="upd-notes"></div>
<div id="upd-progress" class="upd-progress hidden">
<div class="upd-bar"><div id="upd-bar-fill" class="upd-bar-fill"></div></div>
<div id="upd-status" class="hint">Wird heruntergeladen …</div>
</div>
<p class="hint">Der Launcher lädt das Update herunter und startet die Installation automatisch.</p>
</div>
<div class="modal-foot">
<button class="btn" data-close>Später</button>
<button id="upd-install" class="btn btn-primary">Installieren</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>
<button class="ctx-item" data-act="p-edit">Umbenennen</button>
<button class="ctx-item" data-act="p-groupline">Gruppe ändern</button>
<div class="ctx-sep"></div>
<button class="ctx-item ctx-strong" data-act="p-play">Starten</button>
<button class="ctx-item" data-act="p-offline">Offline starten</button>
<div class="ctx-sep"></div>
<button class="ctx-item" data-act="p-edit">Instanz bearbeiten</button>
<button class="ctx-item" data-act="p-notes">Notizen bearbeiten</button>
<button class="ctx-item" data-act="p-mods">Mods anzeigen</button>
<button class="ctx-item" data-act="p-worlds">Welten verwalten</button>
<button class="ctx-item" data-act="p-screens">Bildschirmfotos verwalten</button>
<div class="ctx-sep"></div>
<button class="ctx-item" data-act="p-mcfolder">Minecraft-Ordner</button>
<button class="ctx-item" data-act="p-configfolder">Konfigurations-Ordner</button>
<button class="ctx-item" data-act="p-instfolder">Instanz-Ordner</button>
<div class="ctx-sep"></div>
<button class="ctx-item" data-act="p-shortcut">Verknüpfung erstellen</button>
<button class="ctx-item" data-act="p-export">Instanz exportieren</button>
<button class="ctx-item ctx-danger" data-act="p-delete">Löschen</button>
<button class="ctx-item" data-act="p-copy">Instanz kopieren</button>
</div>
<!-- Toast -->
<div id="toast" class="toast hidden"></div>

View File

@@ -411,6 +411,10 @@ async function prepareAndLaunch(opts, onProgress) {
extraArgs: opts.extraArgs || '',
});
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
if (opts.mcMaximized) args.push('--fullscreen');
else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight));
ensureDir(opts.gameDir);
// Pre-Launch-Befehl (bricht bei Fehler ab)

View File

@@ -16,6 +16,8 @@ const launcher = require('./launcher');
const loaders = require('./loaders');
const forge = require('./forge');
const auth = require('./auth');
const updater = require('./updater');
const icons = require('./icons');
// undici als globales fetch verwenden -> ermöglicht Proxy via setGlobalDispatcher
const undici = require('undici');
@@ -78,10 +80,10 @@ function windowIcon() {
function createWindow() {
mainWindow = new BrowserWindow({
width: 1100,
height: 720,
minWidth: 880,
minHeight: 560,
width: 1200,
height: 820,
minWidth: 940,
minHeight: 640,
backgroundColor: '#1b1e24',
title: 'AeroMC',
icon: windowIcon(),
@@ -108,6 +110,24 @@ app.whenReady().then(() => {
applyProxy(store.getSettings());
createWindow();
// echte Minecraft-Icons einmalig aus den eigenen Spieldateien holen
setTimeout(() => {
try {
const { shared, out } = iconDirs();
if (!fs.existsSync(out) || fs.readdirSync(out).length === 0) icons.extractIcons(shared, out);
} catch { /* noch keine Version heruntergeladen */ }
}, 1500);
// automatisch auf Updates prüfen (kurz nach dem Start)
setTimeout(async () => {
try {
const info = await updater.checkForUpdate(app.getVersion());
if (info.available && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update:available', info);
}
} catch { /* offline o. Ä. still ignorieren */ }
}, 4000);
// Anmelde-Dienste automatisch überwachen (sofort + alle 2 Minuten)
setTimeout(() => checkAuthServices(), 2500);
setInterval(() => checkAuthServices(), 120000);
@@ -514,6 +534,9 @@ ipcMain.handle('instances:launch', async (_e, id, accountId) => {
preLaunchCommand: inst.preLaunchCommand || settings.globalPreLaunch || '',
postExitCommand: inst.postExitCommand || settings.globalPostExit || '',
wrapperCommand: settings.wrapperCommand || '',
mcMaximized: !!settings.mcMaximized,
mcWidth: settings.mcWidth || 854,
mcHeight: settings.mcHeight || 480,
auth: null,
};
@@ -606,6 +629,57 @@ ipcMain.handle('app:openExternal', (_e, url) => {
return true;
});
// ---------------------------------------------------------------------------
// IPC echte Minecraft-Icons (aus den eigenen Spieldateien)
// ---------------------------------------------------------------------------
function iconDirs() {
const ud = app.getPath('userData');
const parent = path.dirname(ud);
// auch im Datenordner vor der Umbenennung suchen
const candidates = [path.join(ud, 'shared')];
for (const alt of ['viper-network', 'Viper-Network', 'aeromc', 'AeroMC']) {
const p = path.join(parent, alt, 'shared');
if (!candidates.includes(p)) candidates.push(p);
}
return { shared: candidates, out: path.join(ud, 'icons') };
}
ipcMain.handle('icons:list', () => {
const { out } = iconDirs();
return icons.listIcons(out);
});
ipcMain.handle('icons:extract', () => {
const { shared, out } = iconDirs();
const res = icons.extractIcons(shared, out);
if (!res.ok) return { ok: false, reason: res.reason };
return { ok: true, version: res.version, count: res.count, icons: icons.listIcons(out) };
});
// ---------------------------------------------------------------------------
// IPC Automatische Updates
// ---------------------------------------------------------------------------
ipcMain.handle('update:check', async () => {
try { return await updater.checkForUpdate(app.getVersion()); }
catch (err) { return { available: false, error: err.message }; }
});
// Ein Klick: herunterladen, prüfen, Installer starten und Launcher beenden
ipcMain.handle('update:install', async (_e, asset) => {
try {
const file = await updater.downloadAndInstall(asset, (p) => {
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('update:progress', p);
});
updater.runInstaller(file.path);
setTimeout(() => app.quit(), 1200); // Installer übernimmt, Launcher schließt sich
return { ok: true, path: file.path };
} catch (err) {
return { ok: false, message: err.message };
}
});
ipcMain.handle('app:info', () => ({
version: app.getVersion(),
electron: process.versions.electron,

View File

@@ -86,6 +86,24 @@ contextBridge.exposeInMainWorld('api', {
},
// Dialoge / App
// echte Minecraft-Icons
iconsList: () => ipcRenderer.invoke('icons:list'),
iconsExtract: () => ipcRenderer.invoke('icons:extract'),
// Updates
updateCheck: () => ipcRenderer.invoke('update:check'),
updateInstall: (asset) => ipcRenderer.invoke('update:install', asset),
onUpdateAvailable: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:available', l);
return () => ipcRenderer.removeListener('update:available', l);
},
onUpdateProgress: (cb) => {
const l = (_e, d) => cb(d);
ipcRenderer.on('update:progress', l);
return () => ipcRenderer.removeListener('update:progress', l);
},
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),

View File

@@ -117,7 +117,86 @@ const ICON_LABELS = {
sword: 'Schwert', pickaxe: 'Spitzhacke', potion: 'Trank', nether: 'Feuer', end: 'Endauge',
redstone: 'Redstone', gold: 'Goldbarren', book: 'Buch', map: 'Karte', star: 'Netherstern',
};
function iconOf(key, size = 44) { return pixelIconSvg(key, size); }
// echte Minecraft-Icons (aus den Spieldateien extrahiert), key -> dataUri
let mcIcons = {};
async function loadMcIcons() {
try {
let list = await window.api.iconsList();
if (!list.length) {
const res = await window.api.iconsExtract();
list = (res && res.ok) ? res.icons : [];
}
mcIcons = Object.fromEntries(list.map((i) => [i.key, i]));
await buildIsoCubes(); // räumliche Block-Symbole vorbereiten
} catch { mcIcons = {}; }
}
// erzeugt die Würfel-Darstellungen einmalig im Voraus
async function buildIsoCubes() {
const jobs = Object.values(mcIcons)
.filter((i) => BLOCK_KEYS.has(i.key))
.map((i) => new Promise((resolve) => {
const img = new Image();
img.onload = () => { isoSources[i.key] = img; resolve(); };
img.onerror = () => resolve();
img.src = i.dataUri;
}));
await Promise.all(jobs);
for (const key of Object.keys(isoSources)) renderIsoCube(key);
}
// Block-Texturen, die als räumlicher Würfel dargestellt werden
const BLOCK_KEYS = new Set(['grass', 'dirt', 'stone', 'planks', 'tnt', 'obsidian', 'netherrack',
'crafting', 'furnace', 'bookshelf', 'diamondblock', 'goldblock', 'emeraldblock', 'redstoneblock']);
let iso3dEnabled = true;
const isoCache = {}; // key -> fertiges Würfelbild
const isoSources = {}; // key -> geladenes Textur-Bild
// zeichnet aus einer 16x16-Textur einen isometrischen Würfel (Canvas -> data:-URI)
function renderIsoCube(key) {
const img = isoSources[key];
if (!img) return null;
const S = 64, c = document.createElement('canvas');
c.width = S; c.height = S;
const g = c.getContext('2d');
g.imageSmoothingEnabled = false;
const w = 26, h = 15, cx = S / 2, top = 8;
const face = (m, bright) => {
g.save();
g.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
g.drawImage(img, 0, 0, 16, 16);
g.restore();
if (bright !== 1) { // Fläche abdunkeln/aufhellen
g.save();
g.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
g.globalCompositeOperation = bright < 1 ? 'source-atop' : 'lighter';
g.fillStyle = bright < 1 ? `rgba(0,0,0,${(1 - bright).toFixed(2)})` : 'rgba(255,255,255,.12)';
g.fillRect(0, 0, 16, 16);
g.restore();
}
};
// Oberseite (Raute), linke und rechte Seitenfläche
face([w / 16, h / 16, -w / 16, h / 16, cx, top], 1);
face([w / 16, h / 16, 0, 30 / 16, cx - w, top + h], 0.72);
face([w / 16, -h / 16, 0, 30 / 16, cx, top + h * 2], 0.55);
const out = c.toDataURL('image/png');
isoCache[key] = out;
return out;
}
// liefert das Icon-Markup: echte Textur bevorzugt, sonst eigene Pixel-Grafik
function iconOf(key, size = 44) {
const real = mcIcons[key];
if (real) {
let src = real.dataUri;
if (iso3dEnabled && BLOCK_KEYS.has(key) && isoCache[key]) src = isoCache[key];
return `<img src="${src}" width="${size}" height="${size}" style="image-rendering:pixelated;display:block" alt="" />`;
}
return pixelIconSvg(key, size);
}
const LOADER_LABEL = {
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
@@ -177,6 +256,66 @@ function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark');
}
// ---------- Kontextmenü (Rechtsklick auf eine Instanz) ----------
function openContextMenu(x, y) {
const menu = el('ctx-menu');
menu.classList.remove('hidden');
// erst anzeigen, dann Größe messen und am Fensterrand ausrichten
const r = menu.getBoundingClientRect();
const px = Math.min(x, window.innerWidth - r.width - 6);
const py = Math.min(y, window.innerHeight - r.height - 6);
menu.style.left = Math.max(6, px) + 'px';
menu.style.top = Math.max(6, py) + 'px';
}
function closeContextMenu() {
el('ctx-menu').classList.add('hidden');
}
// ---------- Automatische Updates ----------
let pendingUpdate = null;
function showUpdateDialog(info) {
pendingUpdate = info;
el('upd-current').textContent = 'v' + info.currentVersion;
el('upd-new').textContent = 'v' + info.latestVersion;
el('upd-notes').textContent = (info.notes || '').trim() || 'Keine Änderungshinweise hinterlegt.';
el('upd-progress').classList.add('hidden');
el('upd-bar-fill').style.width = '0%';
el('upd-install').disabled = !info.asset;
el('upd-install').textContent = info.asset ? 'Installieren' : 'Kein Installer im Release';
show('modal-update');
}
async function startUpdate() {
if (!pendingUpdate || !pendingUpdate.asset) return;
const btn = el('upd-install');
btn.disabled = true;
btn.textContent = 'Wird installiert …';
el('upd-progress').classList.remove('hidden');
el('upd-status').textContent = 'Wird heruntergeladen …';
const res = await window.api.updateInstall(pendingUpdate.asset);
if (res.ok) {
el('upd-status').textContent = 'Installation wird gestartet der Launcher schließt sich.';
} else {
el('upd-status').textContent = '✖ ' + (res.message || 'Update fehlgeschlagen.');
btn.disabled = false;
btn.textContent = 'Erneut versuchen';
}
}
function onUpdateProgress(p) {
if (p.phase === 'download') {
el('upd-bar-fill').style.width = p.percent + '%';
const mb = (n) => (n / 1048576).toFixed(1);
el('upd-status').textContent = `Wird heruntergeladen … ${p.percent}% (${mb(p.loaded)} / ${mb(p.total)} MB)`;
} else if (p.phase === 'ready') {
el('upd-bar-fill').style.width = '100%';
el('upd-status').textContent = 'Download fertig Installation startet …';
}
}
// ---------- Zustand der Anmelde-Dienste in der Statusleiste ----------
function showAuthHealth(h) {
const box = el('status-auth');
@@ -376,6 +515,13 @@ function buildCard(inst) {
card.addEventListener('click', () => selectInstance(inst.id));
card.addEventListener('dblclick', () => launch(inst.id));
// Rechtsklick: Instanz auswählen und Kontextmenü öffnen
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
openContextMenu(e.clientX, e.clientY); // sofort anzeigen
selectInstance(inst.id); // Auswahl folgt
});
// Drag & Drop in Gruppen
card.draggable = true;
card.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', inst.id); card.classList.add('dragging'); });
@@ -530,14 +676,23 @@ async function updateLoaderVersions(preselect) {
}
// ---------- Instanz-Dialog ----------
// alle wählbaren Icons: echte Minecraft-Texturen zuerst, dann die eigenen
function availableIcons() {
const real = Object.values(mcIcons).map((i) => ({ key: i.key, label: i.label, real: true }));
const own = ICON_KEYS
.filter((k) => !mcIcons[k])
.map((k) => ({ key: k, label: ICON_LABELS[k] || k, real: false }));
return real.concat(own);
}
function fillIconSelect(selected) {
const sel = el('f-icon');
sel.innerHTML = '';
for (const key of ICON_KEYS) {
for (const it of availableIcons()) {
const o = document.createElement('option');
o.value = key;
o.textContent = ICON_LABELS[key] || key;
if (key === selected) o.selected = true;
o.value = it.key;
o.textContent = it.label;
if (it.key === selected) o.selected = true;
sel.appendChild(o);
}
renderIconChoices(selected);
@@ -548,15 +703,15 @@ function renderIconChoices(selected) {
const box = el('f-icon-grid');
if (!box) return;
box.innerHTML = '';
for (const key of ICON_KEYS) {
for (const it of availableIcons()) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'icon-choice' + (key === selected ? ' on' : '');
b.title = ICON_LABELS[key] || key;
b.innerHTML = pixelIconSvg(key, 26);
b.className = 'icon-choice' + (it.key === selected ? ' on' : '');
b.title = it.label;
b.innerHTML = iconOf(it.key, 24);
b.addEventListener('click', () => {
el('f-icon').value = key;
renderIconChoices(key);
el('f-icon').value = it.key;
renderIconChoices(it.key);
});
box.appendChild(b);
}
@@ -725,7 +880,8 @@ async function launch(id, accountId) {
if (res.ok) {
status('Läuft (PID ' + res.pid + ')');
toast('Minecraft gestartet.');
openConsole();
const s = await window.api.getSettings();
if (s.showConsoleOnLaunch) openConsole(); // nur wenn gewünscht
} else if (res.needAuth) {
status('Bereit'); el('status-right').textContent = '';
toast(res.message, false);
@@ -748,6 +904,12 @@ function onLaunchEvt(evt) {
status('Minecraft beendet (Code ' + evt.code + ')');
el('status-right').textContent = '';
appendConsole('\n[Prozess beendet, Code ' + evt.code + ']\n');
// Konsole je nach Einstellung schließen bzw. bei Absturz zeigen
window.api.getSettings().then((s) => {
const crashed = evt.code !== 0 && evt.code !== null;
if (crashed && s.showConsoleOnCrash !== false) { openConsole(); return; }
if (s.autoCloseConsole) hide('modal-console');
}).catch(() => {});
return;
}
const label = labels[evt.phase] || evt.phase;
@@ -774,9 +936,10 @@ function appendLineEl(out, line) {
div.textContent = line;
out.appendChild(div);
}
let consoleMaxChars = 300000; // aus den Einstellungen abgeleitet (Zeilen x ~80 Zeichen)
function appendConsole(text) {
consoleBuffer += text;
if (consoleBuffer.length > 300000) consoleBuffer = consoleBuffer.slice(-220000);
if (consoleBuffer.length > consoleMaxChars) consoleBuffer = consoleBuffer.slice(-Math.round(consoleMaxChars * 0.75));
consolePending += text;
const lines = consolePending.split('\n');
consolePending = lines.pop();
@@ -1057,6 +1220,15 @@ async function openSettings() {
}
sel.value = s.bgSkin || '';
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
el('s-sortby').value = s.sortBy || 'lastPlayed';
el('s-iso3d').checked = s.iso3dIcons !== false;
el('s-console-launch').checked = !!s.showConsoleOnLaunch;
el('s-console-autoclose').checked = !!s.autoCloseConsole;
el('s-console-crash').checked = s.showConsoleOnCrash !== false;
el('s-console-lines').value = s.consoleMaxLines || 100000;
el('s-mc-max').checked = !!s.mcMaximized;
el('s-mc-width').value = s.mcWidth || 854;
el('s-mc-height').value = s.mcHeight || 480;
el('s-bgskin-opacity').value = s.bgSkinOpacity ?? 55;
el('s-bgskin-val').textContent = String(s.bgSkinOpacity ?? 55);
el('s-proxy-enabled').checked = !!s.proxyEnabled;
@@ -1113,6 +1285,15 @@ async function saveSettings() {
globalPostExit: el('s-global-post').value.trim(),
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
sortBy: el('s-sortby').value,
iso3dIcons: el('s-iso3d').checked,
showConsoleOnLaunch: el('s-console-launch').checked,
autoCloseConsole: el('s-console-autoclose').checked,
showConsoleOnCrash: el('s-console-crash').checked,
consoleMaxLines: numOrNull(el('s-console-lines').value) || 100000,
mcMaximized: el('s-mc-max').checked,
mcWidth: numOrNull(el('s-mc-width').value) || 854,
mcHeight: numOrNull(el('s-mc-height').value) || 480,
bgSkinOpacity: numOrNull(el('s-bgskin-opacity').value) || 55,
wrapperCommand: el('s-wrapper').value.trim(),
proxyEnabled: el('s-proxy-enabled').checked,
@@ -1127,6 +1308,7 @@ async function saveSettings() {
};
await window.api.saveSettings(patch);
applyTheme(patch.theme);
iso3dEnabled = patch.iso3dIcons;
await applySkinBackground();
hide('modal-settings');
toast('Einstellungen gespeichert.');
@@ -1308,6 +1490,24 @@ function wire() {
await applySkinBackground();
});
// Kontextmenü: Einträge lösen die Aktionen der rechten Leiste aus
document.querySelectorAll('#ctx-menu .ctx-item').forEach((item) => {
item.addEventListener('click', () => {
const target = el(item.dataset.act);
closeContextMenu();
if (target) target.click();
});
});
// schließen bei Klick daneben, Rechtsklick ins Leere, Scrollen oder ESC
document.addEventListener('click', (e) => {
if (!e.target.closest('#ctx-menu')) closeContextMenu();
});
document.addEventListener('contextmenu', (e) => {
if (!e.target.closest('.card') && !e.target.closest('#ctx-menu')) closeContextMenu();
});
document.addEventListener('scroll', closeContextMenu, true);
window.addEventListener('blur', closeContextMenu);
// Warnung anklicken -> sofort erneut prüfen
el('status-auth').addEventListener('click', async () => {
el('status-auth').textContent = 'Prüfe Anmelde-Dienste …';
@@ -1432,6 +1632,23 @@ function wire() {
}));
el('s-open-accounts').addEventListener('click', () => { hide('modal-settings'); openLogin(); });
// Updates
el('upd-install').addEventListener('click', startUpdate);
el('s-check-update').addEventListener('click', async () => {
const st = el('s-update-status');
st.textContent = 'Suche nach Updates …';
const info = await window.api.updateCheck();
if (info.error) { st.textContent = '✖ ' + info.error; return; }
if (info.noReleases) { st.textContent = 'Noch keine Veröffentlichung vorhanden.'; return; }
if (info.available) {
st.textContent = `Version ${info.latestVersion} verfügbar.`;
hide('modal-settings');
showUpdateDialog(info);
} else {
st.textContent = `Du hast bereits die neueste Version (v${info.currentVersion}).`;
}
});
// Microsoft-Login / Kontoverwaltung
el('btn-account').addEventListener('click', openLogin);
el('login-add').addEventListener('click', addAccount);
@@ -1457,6 +1674,7 @@ function wire() {
m.addEventListener('mousedown', (e) => { if (e.target === m) m.classList.add('hidden'); }));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (!el('ctx-menu').classList.contains('hidden')) { closeContextMenu(); return; }
const open = [...document.querySelectorAll('.modal:not(.hidden)')].pop();
if (open) open.classList.add('hidden');
}
@@ -1466,13 +1684,21 @@ function wire() {
window.addEventListener('DOMContentLoaded', async () => {
wire();
applyIcons();
try { const s = await window.api.getSettings(); applyTheme(s.theme); } catch { applyTheme('dark'); }
try {
const s = await window.api.getSettings();
applyTheme(s.theme);
iso3dEnabled = s.iso3dIcons !== false;
consoleMaxChars = Math.max(50000, (s.consoleMaxLines || 100000) * 3);
} catch { applyTheme('dark'); }
await applySkinBackground();
window.api.onLaunchProgress(onLaunchEvt);
window.api.onAuthCode(onAuthCode);
window.api.onAuthRefreshed(async () => { await refreshAccountButton(); await applySkinBackground(); });
window.api.onAuthHealth(showAuthHealth);
window.api.onUpdateAvailable(showUpdateDialog);
window.api.onUpdateProgress(onUpdateProgress);
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
await refreshAccountButton();
await reload();

View File

@@ -56,6 +56,15 @@ const DEFAULT_SETTINGS = {
globalPreLaunch: '',
globalPostExit: '',
lastSelectedId: '', // zuletzt ausgewählte Instanz (beim Start wieder markieren)
showConsoleOnLaunch: false, // Konsole beim Spielstart anzeigen
autoCloseConsole: false, // Konsole nach Spielende automatisch schließen
showConsoleOnCrash: true, // Konsole anzeigen, wenn das Spiel abstürzt
consoleMaxLines: 100000, // Zeilenbegrenzung der Konsole
mcMaximized: false, // Minecraft maximiert starten
mcWidth: 854,
mcHeight: 480,
sortBy: 'lastPlayed', // lastPlayed | name
iso3dIcons: true, // Block-Symbole räumlich darstellen
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)
@@ -204,8 +213,10 @@ function listInstances() {
result.push(meta);
}
}
// zuletzt gespielt zuerst, dann alphabetisch
// Sortierung laut Einstellung: zuletzt gespielt oder nach Name
const byName = getSettings().sortBy === 'name';
result.sort((a, b) => {
if (byName) return a.name.localeCompare(b.name);
if (a.lastPlayed && b.lastPlayed) return b.lastPlayed.localeCompare(a.lastPlayed);
if (a.lastPlayed) return -1;
if (b.lastPlayed) return 1;

View File

@@ -66,6 +66,7 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
:root[data-theme="dark"] #btn-console svg{color:#c8c8cc}
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
:root[data-theme="dark"] .mods-filters select{background:#1e1e1e;color:var(--text);border-color:var(--border)}
:root[data-theme="dark"] .upd-notes{background:#1e1e1e;color:var(--text)}
:root[data-theme="dark"] .modal-close{color:#aaa}
:root[data-theme="dark"] .modal-close:hover{color:#fff}
:root[data-theme="dark"] ::-webkit-scrollbar-thumb{background:#4a4a52;border-color:var(--area)}
@@ -102,15 +103,15 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
}
.list{position:relative;z-index:1}
.list{padding:4px 8px}
.side{background:var(--win);border-left:1px solid var(--border);overflow:auto;padding:12px 8px}
.side{background:var(--win);border-left:1px solid var(--border);overflow-y:auto;overflow-x:hidden;padding:10px 8px}
.side-empty{text-align:center;color:var(--dim);padding:24px 8px}
.side-ico{width:56px;height:56px;margin:2px auto 6px;display:grid;place-items:center;font-size:40px}
.side-ico{width:52px;height:52px;margin:0 auto 5px;display:grid;place-items:center;font-size:38px}
.side-ico img{width:56px;height:56px;object-fit:cover;border-radius:3px}
.side-title{text-align:center;font-weight:600;font-size:14px;word-break:break-word}
.side-grp{text-align:center;color:var(--link);font-size:11px;margin-top:2px;cursor:pointer}
.side-grp:hover{text-decoration:underline}
.hr{height:1px;background:var(--border);margin:8px 2px}
.sbtn{display:block;width:100%;text-align:center;padding:6px;border:1px solid transparent;border-radius:3px;background:none;cursor:pointer;font-family:inherit;font-size:12px;color:var(--text)}
.hr{height:1px;background:var(--border);margin:6px 2px}
.sbtn{display:block;width:100%;text-align:center;padding:5px 4px;border:1px solid transparent;border-radius:3px;background:none;cursor:pointer;font-family:inherit;font-size:12px;color:var(--text)}
.sbtn:hover{background:#e2e6ec;border-color:var(--border2)}
.sbtn.play{font-weight:600}
.sbtn.del:hover{color:var(--danger)}
@@ -174,14 +175,16 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
.field input:focus,.field select:focus,.field textarea:focus{outline:none;border-color:var(--accent)}
.field textarea{resize:vertical}
.field-row{display:flex;gap:12px}.field-row .field{flex:1}
.field-icon{max-width:150px}
.field-icon{max-width:210px;min-width:0}
.hint{font-size:12px}
.path-row{display:flex;gap:8px}.path-row input{flex:1}
.advanced{border:1px solid var(--border);border-radius:3px;padding:12px 14px 2px;margin-top:4px;background:#fbfbfb}
.advanced legend{color:var(--dim);font-size:12px;padding:0 6px}
.version-row{display:flex;gap:8px}.version-row #f-version-type{max-width:120px}.version-row #f-version{flex:1}
.icon-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:3px;margin-top:6px;max-width:100%}
.icon-choice{display:flex;align-items:center;justify-content:center;padding:3px;border:1px solid transparent;border-radius:3px;background:none;cursor:pointer}
.icon-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(30px,1fr));gap:3px;margin-top:6px;
width:100%;max-width:100%;max-height:132px;overflow-y:auto;overflow-x:hidden;box-sizing:border-box}
.icon-choice{display:flex;align-items:center;justify-content:center;padding:2px;border:1px solid transparent;border-radius:3px;background:none;cursor:pointer;min-width:0;overflow:hidden}
.icon-choice img{width:24px;height:24px;image-rendering:pixelated;display:block}
.icon-choice:hover{background:rgba(128,128,128,.18)}
.icon-choice.on{border-color:var(--accent);background:var(--sel)}
.icon-choice svg{display:block}
@@ -292,6 +295,33 @@ img.account-av{object-fit:cover;image-rendering:pixelated}
.acct-av svg{width:18px;height:18px;color:#4b5563}
.acct-head{width:24px;height:26px;object-fit:contain;image-rendering:pixelated;margin-top:-2px;margin-bottom:-2px}
/* Update-Dialog */
.upd-version{display:flex;align-items:center;justify-content:center;gap:12px;margin:4px 0 14px;font-size:15px}
.upd-old{color:var(--dim)}
.upd-arrow{color:var(--dim)}
.upd-newv{font-weight:700;color:var(--accent)}
.upd-notes{max-height:180px;overflow:auto;background:var(--input,#fff);border:1px solid var(--border);border-radius:3px;padding:10px 12px;font-size:12px;white-space:pre-wrap;margin-bottom:12px}
.upd-progress{margin:10px 0}
.upd-bar{height:8px;background:var(--border2);border-radius:4px;overflow:hidden}
.upd-bar-fill{height:100%;width:0;background:var(--accent);transition:width .2s}
/* Kontextmenü (Rechtsklick auf Instanz) */
.ctx{
position:fixed;z-index:200;min-width:210px;padding:4px;
background:var(--win);border:1px solid var(--border);border-radius:4px;
box-shadow:0 8px 24px rgba(0,0,0,.35);
}
.ctx-item{
display:block;width:100%;text-align:left;padding:6px 12px;
border:none;border-radius:3px;background:none;color:var(--text);
font-family:inherit;font-size:12px;cursor:pointer;white-space:nowrap;
}
.ctx-item:hover{background:var(--sel);color:var(--text)}
.ctx-item:disabled{opacity:.45;cursor:default;background:none}
.ctx-strong{font-weight:600}
.ctx-danger:hover{background:var(--danger);color:#fff}
.ctx-sep{height:1px;background:var(--border);margin:4px 6px}
/* Toast */
.toast{position:fixed;bottom:42px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:10px 16px;border-radius:4px;box-shadow:0 6px 20px rgba(0,0,0,.4);z-index:100;font-size:13px}
.toast.err{background:#a5342a}

115
src/updater.js Normal file
View File

@@ -0,0 +1,115 @@
'use strict';
/*
* updater.js automatische Updates über die Gitea-Releases
* ---------------------------------------------------------
* Ablauf: Releases abfragen -> neueste Version mit der laufenden vergleichen ->
* bei Bedarf den Windows-Installer herunterladen und starten.
* Ein Klick des Nutzers genügt; Download, Prüfung und Start laufen automatisch.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const crypto = require('crypto');
const { spawn } = require('child_process');
const API = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/AeroMc-Launcher/releases';
const UA = 'AeroMC-Launcher';
// "1.2.3" -> [1,2,3]; führendes "v" und Zusätze wie "-beta" werden ignoriert
function parseVersion(v) {
const m = String(v || '').trim().replace(/^v/i, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
if (!m) return null;
return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
}
// >0 wenn a neuer als b
function compareVersions(a, b) {
const x = parseVersion(a); const y = parseVersion(b);
if (!x || !y) return 0;
for (let i = 0; i < 3; i++) {
if (x[i] !== y[i]) return x[i] - y[i];
}
return 0;
}
// Windows-Installer im Release finden (.exe bevorzugt, sonst .zip)
function pickAsset(release) {
const assets = release.assets || [];
return assets.find((a) => /\.exe$/i.test(a.name))
|| assets.find((a) => /setup.*\.exe$/i.test(a.name))
|| assets.find((a) => /\.zip$/i.test(a.name))
|| null;
}
/*
* Prüft auf Updates.
* -> { available, currentVersion, latestVersion, notes, asset, url, noReleases }
*/
async function checkForUpdate(currentVersion) {
const res = await fetch(API + '?limit=10', { headers: { 'User-Agent': UA, Accept: 'application/json' } });
if (!res.ok) throw new Error('Update-Server antwortet mit HTTP ' + res.status);
const list = await res.json();
const releases = (Array.isArray(list) ? list : []).filter((r) => !r.draft);
if (!releases.length) {
return { available: false, noReleases: true, currentVersion };
}
// neueste anhand der Versionsnummer (nicht nach Datum, damit Nachträge nicht stören)
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
const latest = releases[0];
const available = compareVersions(latest.tag_name, currentVersion) > 0;
const asset = pickAsset(latest);
return {
available,
currentVersion,
latestVersion: String(latest.tag_name || '').replace(/^v/i, ''),
notes: latest.body || '',
published: latest.published_at || latest.created_at || null,
asset: asset ? { name: asset.name, url: asset.browser_download_url, size: asset.size } : null,
url: latest.html_url,
};
}
/*
* Lädt den Installer herunter (mit Fortschritt) und startet ihn.
* onProgress: ({ phase, loaded, total, percent })
*/
async function downloadAndInstall(asset, onProgress) {
if (!asset || !asset.url) throw new Error('Kein Installer im Release gefunden.');
const dir = path.join(os.tmpdir(), 'aeromc-update');
fs.mkdirSync(dir, { recursive: true });
const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
const total = Number(res.headers.get('content-length')) || asset.size || 0;
const chunks = [];
let loaded = 0;
for await (const chunk of res.body) {
chunks.push(Buffer.from(chunk));
loaded += chunk.length;
if (onProgress) {
onProgress({ phase: 'download', loaded, total, percent: total ? Math.round((loaded / total) * 100) : 0 });
}
}
const buf = Buffer.concat(chunks);
if (total && buf.length !== total) throw new Error('Download unvollständig.');
fs.writeFileSync(dest, buf);
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
if (onProgress) onProgress({ phase: 'ready', loaded: buf.length, total: buf.length, percent: 100 });
return { path: dest, bytes: buf.length, sha256 };
}
// Startet den Installer und löst sich vom Launcher (damit er sich selbst ersetzen kann)
function runInstaller(installerPath) {
const child = spawn(installerPath, [], { detached: true, stdio: 'ignore' });
child.unref();
return true;
}
module.exports = { checkForUpdate, downloadAndInstall, runInstaller, compareVersions, parseVersion };