Upload via GUI (143 Dateien)
This commit is contained in:
+159
-83
@@ -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 ----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user