Upload via GUI (143 Dateien)

This commit is contained in:
2026-10-04 18:46:00 +00:00
parent 7e1fb616aa
commit 6a0e92eb0f
17 changed files with 2786 additions and 1686 deletions
+159 -83
View File
@@ -18,6 +18,8 @@ let ffmpegPfad = null;
try { ffmpegPfad = require('ffmpeg-static'); } catch { /* egal, siehe Rückfall in recording:save */ }
const pkg = require('../package.json');
const store = require('./store');
const startanimation = require('./startanimation');
const fensteraufbau = require('./fensteraufbau');
const services = require('./services');
const launcher = require('./launcher');
const loaders = require('./loaders');
@@ -28,6 +30,7 @@ const icons = require('./icons');
const bedrock = require('./bedrock');
const backup = require('./backup');
const mmcimport = require('./mmcimport');
const kopieren = require('./kopieren');
const cfimport = require('./cfimport');
const mrimport = require('./mrimport');
const mcimport = require('./mcimport');
@@ -154,14 +157,18 @@ function broadcast(channel, payload) {
}
}
function openPopout(modalId, params) {
// von: Rechteck des angeklickten Knopfs (Bildschirmkoordinaten) – dann wächst
// das Fenster daraus hervor (Optionen → Darstellung → Animationen)
function openPopout(modalId, params, von) {
const wachsen = !!von && store.getSettings().uiAnimationen !== false;
let win = popoutWindows.get(modalId);
if (win && !win.isDestroyed()) {
win.show();
win.focus();
// erneut mit (evtl. neuen) Parametern anzeigen, ohne die Seite neu zu
// laden – sonst ginge z. B. der bisher gesammelte Konsolentext verloren
win.webContents.send('popout:reopen', params || {});
if (wachsen && !win.isVisible()) { fensteraufbau.wachsen(win, von); return; }
win.show();
win.focus();
return;
}
const query = { popout: modalId };
@@ -189,7 +196,7 @@ function openPopout(modalId, params) {
});
popoutWindows.set(modalId, win);
win.loadFile(path.join(__dirname, 'index.html'), { query });
win.once('ready-to-show', () => win.show());
win.once('ready-to-show', () => { if (wachsen) fensteraufbau.wachsen(win, von); else win.show(); });
win.webContents.on('did-fail-load', (_e, code, desc) => {
console.error('Popup-Fenster "' + modalId + '" konnte nicht laden:', code, desc);
});
@@ -212,7 +219,18 @@ let recordingGuardActive = false;
ipcMain.on('recording:guard', (_e, active) => { recordingGuardActive = !!active; });
ipcMain.on('debug:rendererError', (_e, msg) => console.error('[Renderer-Fehler]', msg));
ipcMain.on('popout:open', (_e, modalId, params) => openPopout(modalId, params));
ipcMain.on('popout:open', (e, modalId, params, knopf) => {
// Herkunft fürs Aufwachsen: angeklickter Knopf, sonst die Mitte des Fensters
let von = null;
const quelle = BrowserWindow.fromWebContents(e.sender);
if (quelle && !quelle.isDestroyed() && quelle.isVisible()) {
const cb = quelle.getContentBounds();
von = knopf && Number.isFinite(knopf.x) && Number.isFinite(knopf.y)
? { x: Math.round(cb.x + knopf.x), y: Math.round(cb.y + knopf.y), width: Math.max(8, Math.round(knopf.width)), height: Math.max(8, Math.round(knopf.height)) }
: { x: Math.round(cb.x + cb.width / 2 - 20), y: Math.round(cb.y + cb.height / 2 - 20), width: 40, height: 40 };
}
openPopout(modalId, params, von);
});
// Design gewechselt -> Fensterknöpfe oben rechts umfärben (nur Hauptfenster,
// die Popup-Fenster haben ihren eigenen Kopf)
ipcMain.on('window:titleBar', (e, design) => {
@@ -752,7 +770,7 @@ function windowIcon() {
return undefined; // Electron-Standard, solange kein Logo hinterlegt ist
}
function createWindow(startHidden) {
function createWindow(startHidden, mitStartanimation) {
mainWindow = new BrowserWindow({
width: 1200,
height: 820,
@@ -773,7 +791,7 @@ function createWindow(startHidden) {
},
});
mainWindow.loadFile(path.join(__dirname, 'index.html'));
mainWindow.loadFile(path.join(__dirname, 'index.html'), mitStartanimation ? { query: { startanimation: '1' } } : undefined);
// Bei aktivem Autostart landet das Fenster beim Schließen (X) nur im
// Infobereich, statt AeroMC wirklich zu beenden – sonst wäre der
@@ -939,8 +957,13 @@ app.whenReady().then(() => {
// Autostart-Eintrag/Tray mit der Einstellung synchron halten (falls z. B.
// von Hand an der Registry gedreht wurde oder nach einem Update)
applyAutostart(autostartAn);
// Reste von im Hintergrund gelöschten Instanzen wegräumen (siehe store.entsorgeOrdner)
store.raeumePapierkorbAuf();
const startHidden = autostartAn && process.argv.includes('--hidden');
createWindow(startHidden);
// Startanimation: Fenster erst verdeckt anlegen, die Animation zeigt es dann
const mitAnimation = !startHidden && store.getSettings().startAnimation !== false;
createWindow(startHidden || mitAnimation, mitAnimation);
if (mitAnimation) startanimation.spielen(mainWindow, hauptfensterBereit, store.getSettings().theme);
// Fenstererfassung für die Spielaufzeichnung ohne nativen Auswahldialog:
// das vom Renderer per recording:setCaptureTarget gewählte Fenster wird
@@ -2660,18 +2683,48 @@ function aktivesFreundeKonto() {
ipcMain.handle('friends:enabled', () => store.getSettings().friendsEnabled !== false);
/*
* Der Server kennt den Namen eines Spielers nur, wenn dieser schon einmal über
* AeroMC gespielt hat (Herzschlag). Wer vorher eine Anfrage schickt, käme beim
* Empfänger nur als UUID an – deshalb fehlende Namen bei Mojang nachschlagen
* (öffentliches Profil, 1 Stunde gemerkt).
*/
const spielerNamen = new Map(); // uuid -> { name, zeit }
async function spielerName(uuid) {
const id = String(uuid || '').replace(/-/g, '').toLowerCase();
if (!/^[0-9a-f]{32}$/.test(id)) return null;
const gemerkt = spielerNamen.get(id);
if (gemerkt && Date.now() - gemerkt.zeit < 60 * 60 * 1000) return gemerkt.name;
try {
const res = await fetch('https://sessionserver.mojang.com/session/minecraft/profile/' + id, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
if (!res.ok) return gemerkt ? gemerkt.name : null;
const name = (await res.json()).name || null;
spielerNamen.set(id, { name, zeit: Date.now() });
return name;
} catch {
return gemerkt ? gemerkt.name : null;
}
}
async function mitNamen(liste) {
const eintraege = Array.isArray(liste) ? liste : [];
await Promise.all(eintraege.map(async (e) => {
if (e && !e.username && e.uuid) e.username = await spielerName(e.uuid);
}));
return eintraege;
}
ipcMain.handle('friends:list', async () => {
const konto = aktivesFreundeKonto();
if (!konto) return { ok: false, message: 'Kein Konto angemeldet.' };
const res = await friends.getFriends(konto.uuid);
return res.ok ? { ok: true, list: res.data || [] } : res;
return res.ok ? { ok: true, list: await mitNamen(res.data) } : res;
});
ipcMain.handle('friends:requests', async () => {
const konto = aktivesFreundeKonto();
if (!konto) return { ok: false, message: 'Kein Konto angemeldet.' };
const res = await friends.getRequests(konto.uuid);
return res.ok ? { ok: true, list: res.data || [] } : res;
return res.ok ? { ok: true, list: await mitNamen(res.data) } : res;
});
ipcMain.handle('friends:add', async (_e, targetUsername) => {
@@ -2685,7 +2738,7 @@ ipcMain.handle('friends:sentRequests', async () => {
const konto = aktivesFreundeKonto();
if (!konto) return { ok: false, message: 'Kein Konto angemeldet.' };
const res = await friends.getSentRequests(konto.uuid);
return res.ok ? { ok: true, list: res.data || [] } : res;
return res.ok ? { ok: true, list: await mitNamen(res.data) } : res;
});
ipcMain.handle('friends:cancel', async (_e, otherUuid) => {
@@ -2758,7 +2811,12 @@ ipcMain.handle('instances:update', (_e, id, patch) => {
ipcMain.handle('instances:delete', (_e, id) => store.deleteInstance(id));
ipcMain.handle('instances:duplicate', (_e, id) => store.duplicateInstance(id));
ipcMain.handle('instances:duplicate', async (_e, id) => {
const kopie = store.duplicateInstance(id, true);
try { await kopieren.kopiere(store.gameDir(id), store.gameDir(kopie.id), null, null); }
catch { /* Quelle evtl. leer */ }
return kopie;
});
ipcMain.handle('instances:openFolder', (_e, id) => {
const p = store.instancePath(id);
@@ -3997,20 +4055,91 @@ ipcMain.handle('mmc:scan', (_e, folder) => {
catch (err) { return { ok: false, reason: err.message }; }
});
ipcMain.handle('mmc:import', (_e, entries, copyData) => {
// ---------- Instanzen aus anderen Launchern übernehmen ----------
/*
* Gemeinsamer Ablauf für alle Launcher-Importe. Früher wurde synchron kopiert –
* bei vielen Gigabyte stand der Launcher dann komplett still. Jetzt:
* 1. Größe aller ausgewählten Instanzen ermitteln (asynchron)
* 2. prüfen, ob auf dem Laufwerk genug Platz frei ist
* 3. Instanz für Instanz anlegen und im Hintergrund kopieren
* Fortschritt geht als 'import:fortschritt' an alle Fenster; 'import:abbrechen'
* stoppt den Lauf, die gerade halb kopierte Instanz wird wieder entfernt.
*/
let laufenderImport = null;
const GB = 1024 * 1024 * 1024;
const zuGb = (b) => (b / GB).toLocaleString('de-DE', { maximumFractionDigits: 1 });
ipcMain.handle('import:abbrechen', () => {
if (laufenderImport) laufenderImport.abgebrochen = true;
return !!laufenderImport;
});
async function fuehreImportAus(modul, entries, copyData, nachher) {
if (laufenderImport) return { ok: false, message: 'Es läuft bereits ein Import – bitte warten oder ihn abbrechen.' };
const job = { abgebrochen: false };
laufenderImport = job;
const liste = entries || [];
const done = [];
const failed = [];
for (const entry of entries || []) {
try {
const imported = mmcimport.importOne(store, entry, copyData !== false);
done.push(imported);
const melde = (daten) => broadcast('import:fortschritt', daten);
let fortschritt = null;
try {
// 1) Was wird kopiert und wie viel?
const plaene = liste.map((entry) => ({ entry, quellen: copyData ? modul.kopierQuellen(entry) : [] }));
let gesamt = { bytes: 0, dateien: 0 };
if (copyData) {
melde({ phase: 'zaehlen', dateien: 0, bytes: 0, instanzen: liste.length });
try {
gesamt = await kopieren.zaehle(plaene.flatMap((p) => p.quellen.map((q) => q.von)), job,
(s) => melde({ phase: 'zaehlen', dateien: s.dateien, bytes: s.bytes, instanzen: liste.length }));
} catch (err) {
if (err.abgebrochen) return { ok: true, imported: [], failed: [], abgebrochen: true, uebersprungen: [] };
throw err;
}
// 2) Reicht der Platz? (etwas Luft für Windows lassen)
const frei = await kopieren.freierPlatz(store.getInstancesDir());
if (frei !== null && gesamt.bytes > frei - 512 * 1024 * 1024) {
return { ok: false, message: `Nicht genug Speicherplatz: Die Instanzen brauchen ${zuGb(gesamt.bytes)} GB, auf dem Laufwerk sind nur ${zuGb(frei)} GB frei. Weniger Instanzen auswählen oder unter Optionen → Allgemein einen anderen Instanz-Ordner wählen.` };
}
}
catch (err) { failed.push({ name: entry.name, message: err.message }); }
broadcast('mmc:progress', { current: done.length + failed.length, total: entries.length });
// 3) Instanz für Instanz – ein gemeinsamer Zähler über alle Instanzen
let kopf = { phase: 'kopieren' };
fortschritt = kopieren.neuerFortschritt(gesamt, (p) => melde(Object.assign({}, kopf, p)));
const f = fortschritt;
for (let i = 0; i < plaene.length; i++) {
if (job.abgebrochen) break;
const { entry, quellen } = plaene[i];
kopf = { phase: 'kopieren', instanz: entry.name, nr: i + 1, von: plaene.length };
f.melden(true);
let angelegt = null;
try {
angelegt = modul.importOne(store, entry, false); // nur Angaben – kopiert wird hier
for (const q of quellen) {
await kopieren.kopiere(q.von, path.join(store.gameDir(angelegt.id), q.name), f, job);
}
angelegt.copied = quellen.length > 0;
if (nachher) await nachher(entry, angelegt, kopf);
done.push(angelegt);
} catch (err) {
// halb kopierte Instanz nicht liegen lassen
if (angelegt) { try { store.deleteInstance(angelegt.id); } catch { /* Aufräumen ist kein Muss */ } }
if (!err.abgebrochen) {
failed.push({ name: entry.name, message: err.code === 'ENOSPC' ? 'Speicherplatz voll' : err.message });
if (err.code === 'ENOSPC') break;
}
}
}
} finally {
laufenderImport = null;
}
if (done.length) broadcast('instances:changed');
return { ok: true, imported: done, failed };
});
return {
ok: true, imported: done, failed, abgebrochen: job.abgebrochen,
uebersprungen: fortschritt ? fortschritt.uebersprungen.slice(0, 50) : [],
};
}
ipcMain.handle('mmc:import', (_e, entries, copyData) => fuehreImportAus(mmcimport, entries, copyData !== false));
ipcMain.handle('cf:pickFolder', async (e) => {
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
@@ -4025,20 +4154,7 @@ ipcMain.handle('cf:scan', (_e, folder) => {
catch (err) { return { ok: false, reason: err.message }; }
});
ipcMain.handle('cf:import', (_e, entries, copyData) => {
const done = [];
const failed = [];
for (const entry of entries || []) {
try {
const imported = cfimport.importOne(store, entry, copyData !== false);
done.push(imported);
}
catch (err) { failed.push({ name: entry.name, message: err.message }); }
broadcast('cf:progress', { current: done.length + failed.length, total: entries.length });
}
if (done.length) broadcast('instances:changed');
return { ok: true, imported: done, failed };
});
ipcMain.handle('cf:import', (_e, entries, copyData) => fuehreImportAus(cfimport, entries, copyData !== false));
// ---------- Modrinth App ----------
ipcMain.handle('mr:pickFolder', async (e) => {
@@ -4100,26 +4216,12 @@ async function verknuepfeModrinthModpack(instId, link) {
return true;
}
ipcMain.handle('mr:import', async (_e, entries, copyData) => {
const done = [];
const failed = [];
const liste = entries || [];
for (const entry of liste) {
try {
const imported = mrimport.importOne(store, entry, copyData !== false);
if (entry.modpack && entry.modpack.projectId && entry.modpack.versionId) {
broadcast('mr:progress', { current: done.length + failed.length, total: liste.length, detail: 'Modpack-Verknüpfung für ' + entry.name });
try { imported.modpack = await verknuepfeModrinthModpack(imported.id, entry.modpack); }
catch { imported.modpack = false; }
}
done.push(imported);
}
catch (err) { failed.push({ name: entry.name, message: err.message }); }
broadcast('mr:progress', { current: done.length + failed.length, total: liste.length });
}
if (done.length) broadcast('instances:changed');
return { ok: true, imported: done, failed };
});
ipcMain.handle('mr:import', (_e, entries, copyData) => fuehreImportAus(mrimport, entries, copyData !== false, async (entry, imported, kopf) => {
if (!(entry.modpack && entry.modpack.projectId && entry.modpack.versionId)) return;
broadcast('import:fortschritt', Object.assign({}, kopf, { phase: 'modpack' }));
try { imported.modpack = await verknuepfeModrinthModpack(imported.id, entry.modpack); }
catch { imported.modpack = false; }
}));
ipcMain.handle('mc:pickFolder', async (e) => {
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
@@ -4134,20 +4236,7 @@ ipcMain.handle('mc:scan', (_e, folder) => {
catch (err) { return { ok: false, reason: err.message }; }
});
ipcMain.handle('mc:import', (_e, entries, copyData) => {
const done = [];
const failed = [];
for (const entry of entries || []) {
try {
const imported = mcimport.importOne(store, entry, copyData !== false);
done.push(imported);
}
catch (err) { failed.push({ name: entry.name, message: err.message }); }
broadcast('mc:progress', { current: done.length + failed.length, total: entries.length });
}
if (done.length) broadcast('instances:changed');
return { ok: true, imported: done, failed };
});
ipcMain.handle('mc:import', (_e, entries, copyData) => fuehreImportAus(mcimport, entries, copyData !== false));
ipcMain.handle('technic:pickFolder', async (e) => {
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
@@ -4162,20 +4251,7 @@ ipcMain.handle('technic:scan', (_e, folder) => {
catch (err) { return { ok: false, reason: err.message }; }
});
ipcMain.handle('technic:import', (_e, entries, copyData) => {
const done = [];
const failed = [];
for (const entry of entries || []) {
try {
const imported = technicimport.importOne(store, entry, copyData !== false);
done.push(imported);
}
catch (err) { failed.push({ name: entry.name, message: err.message }); }
broadcast('technic:progress', { current: done.length + failed.length, total: entries.length });
}
if (done.length) broadcast('instances:changed');
return { ok: true, imported: done, failed };
});
ipcMain.handle('technic:import', (_e, entries, copyData) => fuehreImportAus(technicimport, entries, copyData !== false));
// ---- Technic-Plattform: Modpack-Katalog durchsuchen und direkt installieren ----