Upload via GUI (42 Dateien)
This commit is contained in:
+161
-18
@@ -6,7 +6,7 @@
|
||||
* (Vanilla-Start, Microsoft-Login und Mod-Loader folgen in späteren Phasen.)
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification, Tray, Menu, nativeImage } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@@ -124,18 +124,29 @@ function instanzAusArgumenten(argv) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// wie instanzAusArgumenten, nur für die optionale Server-Adresse einer
|
||||
// Server-Verknüpfung ("--server=<ip>") – bleibt aus demselben Grund als ein
|
||||
// einziges "--server=..."-Argument zusammen
|
||||
function serverAusArgumenten(argv) {
|
||||
const args = (argv || []).map(String);
|
||||
for (const a of args) {
|
||||
if (a.startsWith('--server=')) return a.slice('--server='.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Startwunsch an die Oberfläche geben – dort hängt die gesamte Rückmeldung dran
|
||||
function starteInstanzImFenster(id) {
|
||||
function starteInstanzImFenster(id, serverIp) {
|
||||
if (!id || !mainWindow || mainWindow.isDestroyed()) return;
|
||||
mainWindow.webContents.send('instance:launch-request', id);
|
||||
mainWindow.webContents.send('instance:launch-request', { id, serverIp: serverIp || null });
|
||||
}
|
||||
|
||||
app.on('second-instance', (_e, argv) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
starteInstanzImFenster(instanzAusArgumenten(argv));
|
||||
// showMainWindow() statt nur restore()+focus(): bei aktivem Autostart kann
|
||||
// das Fenster echt versteckt sein (hide(), nicht nur minimiert) – das
|
||||
// braucht ein show(), sonst bleibt es beim zweiten Start unsichtbar
|
||||
showMainWindow();
|
||||
starteInstanzImFenster(instanzAusArgumenten(argv), serverAusArgumenten(argv));
|
||||
});
|
||||
|
||||
// Ziel für Verknüpfungen: bei der Portable-Fassung zeigt process.execPath auf
|
||||
@@ -147,6 +158,31 @@ function verknuepfungsZiel() {
|
||||
return process.execPath;
|
||||
}
|
||||
|
||||
/*
|
||||
* Baut die Argumente für eine Verknüpfung zusammen.
|
||||
* Im Entwicklungsmodus (npm start) zeigt process.execPath nur auf die nackte
|
||||
* electron.exe – die weiß ohne Pfadangabe nicht, welche App sie laden soll,
|
||||
* und zeigt stattdessen ihren eigenen Startbildschirm statt AeroMC. Im
|
||||
* gepackten Build (Setup/Portable) ist die App fest eingebaut, daher dort
|
||||
* kein Zusatz nötig.
|
||||
*/
|
||||
function verknuepfungsArgs(teile) {
|
||||
const praefix = app.isPackaged ? '' : `"${path.join(__dirname, '..')}" `;
|
||||
return praefix + teile.join(' ');
|
||||
}
|
||||
|
||||
/*
|
||||
* Icon für eine Verknüpfung. Im gepackten Build trägt die EXE selbst schon
|
||||
* das richtige Icon als Ressource (funktioniert auch aus dem asar heraus).
|
||||
* Im Entwicklungsmodus zeigt "ziel" auf die nackte electron.exe (Atom-Logo) –
|
||||
* dort stattdessen direkt die eigene .ico-Datei verwenden, die im
|
||||
* Entwicklungsordner als normale Datei vorliegt.
|
||||
*/
|
||||
function verknuepfungsIcon(ziel) {
|
||||
if (app.isPackaged) return ziel;
|
||||
return windowIcon() || ziel;
|
||||
}
|
||||
|
||||
function windowIcon() {
|
||||
const png = path.join(__dirname, 'assets', 'logo.png');
|
||||
const ico = path.join(__dirname, 'assets', 'icon.ico');
|
||||
@@ -155,7 +191,7 @@ function windowIcon() {
|
||||
return undefined; // Electron-Standard, solange kein Logo hinterlegt ist
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
function createWindow(startHidden) {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 820,
|
||||
@@ -165,6 +201,7 @@ function createWindow() {
|
||||
title: 'AeroMC',
|
||||
icon: windowIcon(),
|
||||
autoHideMenuBar: true,
|
||||
show: !startHidden,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
@@ -175,13 +212,69 @@ function createWindow() {
|
||||
|
||||
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||
|
||||
// mit "--launch <id>" gestartet -> Instanz anwerfen, sobald die Oberfläche steht
|
||||
// Bei aktivem Autostart landet das Fenster beim Schließen (X) nur im
|
||||
// Infobereich, statt AeroMC wirklich zu beenden – sonst wäre der
|
||||
// Autostart witzlos, weil man ihn beim ersten Wegklicken gleich wieder los ist.
|
||||
mainWindow.on('close', (e) => {
|
||||
if (appIsQuitting || !store.getSettings().autostart) return;
|
||||
e.preventDefault();
|
||||
mainWindow.hide();
|
||||
});
|
||||
|
||||
// mit "--launch <id>" gestartet -> Instanz anwerfen, sobald die Oberfläche
|
||||
// ihren Empfänger dafür registriert hat. WICHTIG: nicht auf "did-finish-load"
|
||||
// verlassen – das feuert, sobald die Seite geladen ist, aber die Oberfläche
|
||||
// registriert ihren IPC-Listener erst nach eigenem asynchronem Vorlauf
|
||||
// (u. a. Skin-Hintergrund laden). Kommt der Startwunsch früher an als diese
|
||||
// Registrierung, geht er kommentarlos verloren – ein Wettlauf, der je nach
|
||||
// Netzwerk-/Ladegeschwindigkeit mal gewinnt, mal nicht. Deshalb wartet der
|
||||
// Hauptprozess hier auf ein explizites Bereit-Signal der Oberfläche.
|
||||
const startId = instanzAusArgumenten(process.argv);
|
||||
const startServer = serverAusArgumenten(process.argv);
|
||||
if (startId) {
|
||||
mainWindow.webContents.once('did-finish-load', () => starteInstanzImFenster(startId));
|
||||
ipcMain.once('renderer:ready', () => starteInstanzImFenster(startId, startServer));
|
||||
}
|
||||
}
|
||||
|
||||
let tray = null;
|
||||
let appIsQuitting = false;
|
||||
|
||||
function showMainWindow() {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) { createWindow(false); return; }
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
if (tray) return;
|
||||
try {
|
||||
const iconPfad = windowIcon();
|
||||
const bild = iconPfad ? nativeImage.createFromPath(iconPfad) : nativeImage.createEmpty();
|
||||
tray = new Tray(bild.isEmpty() ? nativeImage.createEmpty() : bild.resize({ width: 16, height: 16 }));
|
||||
tray.setToolTip('AeroMC');
|
||||
tray.setContextMenu(Menu.buildFromTemplate([
|
||||
{ label: 'Öffnen', click: () => showMainWindow() },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Beenden', click: () => { appIsQuitting = true; app.quit(); } },
|
||||
]));
|
||||
tray.on('click', () => showMainWindow());
|
||||
} catch { /* z. B. wenn kein Icon vorhanden ist – Autostart läuft dann ohne Tray-Symbol weiter */ }
|
||||
}
|
||||
|
||||
function destroyTray() {
|
||||
if (tray) { tray.destroy(); tray = null; }
|
||||
}
|
||||
|
||||
// Windows-Autostart-Eintrag setzen/entfernen + Tray-Symbol passend ein-/ausblenden
|
||||
function applyAutostart(enabled) {
|
||||
try {
|
||||
app.setLoginItemSettings({ openAtLogin: !!enabled, args: enabled ? ['--hidden'] : [] });
|
||||
} catch { /* z. B. in portabler Umgebung ohne Schreibrechte auf die Registry */ }
|
||||
if (enabled) createTray();
|
||||
else destroyTray();
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
if (!hatEinzelSperre) return; // zweite Instanz beendet sich selbst
|
||||
store.init(app.getPath('userData'));
|
||||
@@ -195,7 +288,12 @@ app.whenReady().then(() => {
|
||||
decrypt: (b) => safeStorage.decryptString(Buffer.from(b, 'base64')),
|
||||
});
|
||||
applyProxy(store.getSettings());
|
||||
createWindow();
|
||||
const autostartAn = !!store.getSettings().autostart;
|
||||
// Autostart-Eintrag/Tray mit der Einstellung synchron halten (falls z. B.
|
||||
// von Hand an der Registry gedreht wurde oder nach einem Update)
|
||||
applyAutostart(autostartAn);
|
||||
const startHidden = autostartAn && process.argv.includes('--hidden');
|
||||
createWindow(startHidden);
|
||||
|
||||
// Java-Suche im Hintergrund vorbereiten -> erster Spielstart ohne Verzögerung
|
||||
setTimeout(() => { try { services.scanJava(); } catch { /* egal */ } }, 800);
|
||||
@@ -255,6 +353,8 @@ app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
app.on('before-quit', () => { appIsQuitting = true; });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Java-Erkennung (für Anzeige in den Einstellungen)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -277,7 +377,12 @@ function detectJava(javaPath) {
|
||||
|
||||
ipcMain.handle('settings:get', () => store.getSettings());
|
||||
|
||||
ipcMain.handle('settings:save', (_e, patch) => { const s = store.saveSettings(patch); applyProxy(s); return s; });
|
||||
ipcMain.handle('settings:save', (_e, patch) => {
|
||||
const s = store.saveSettings(patch);
|
||||
applyProxy(s);
|
||||
if (patch && Object.prototype.hasOwnProperty.call(patch, 'autostart')) applyAutostart(s.autostart);
|
||||
return s;
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:instancesDir', () => store.getInstancesDir());
|
||||
|
||||
@@ -1251,24 +1356,62 @@ ipcMain.handle('modpack:import', async () => {
|
||||
});
|
||||
|
||||
// Verknüpfung auf dem Desktop, die genau diese Instanz startet
|
||||
ipcMain.handle('instances:shortcut', (_e, id) => {
|
||||
// wunschName: optionaler, vom Nutzer selbst vergebener Dateiname (ohne .lnk)
|
||||
ipcMain.handle('instances:shortcut', (_e, id, wunschName) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
if (process.platform !== 'win32') {
|
||||
return { ok: false, message: 'Verknüpfungen werden nur unter Windows unterstützt.' };
|
||||
}
|
||||
try {
|
||||
const name = String(inst.name || 'Instanz').replace(/[\\/:*?"<>|]/g, '_').trim() || 'Instanz';
|
||||
const roh = (wunschName && wunschName.trim()) || inst.name || 'Instanz';
|
||||
const name = String(roh).replace(/[\\/:*?"<>|]/g, '_').trim() || 'Instanz';
|
||||
const datei = path.join(app.getPath('desktop'), name + '.lnk');
|
||||
const ziel = verknuepfungsZiel();
|
||||
const ok = shell.writeShortcutLink(datei, fs.existsSync(datei) ? 'replace' : 'create', {
|
||||
target: ziel,
|
||||
// Gleichheitsform, damit die Kennung beim Umsortieren durch Chromium
|
||||
// an ihrem Schalter bleibt (siehe instanzAusArgumenten)
|
||||
args: `--launch="${id}"`,
|
||||
args: verknuepfungsArgs([`--launch="${id}"`]),
|
||||
cwd: path.dirname(ziel),
|
||||
description: 'AeroMC – ' + inst.name,
|
||||
icon: ziel,
|
||||
icon: verknuepfungsIcon(ziel),
|
||||
iconIndex: 0,
|
||||
});
|
||||
if (!ok) return { ok: false, message: 'Verknüpfung konnte nicht geschrieben werden.' };
|
||||
return { ok: true, path: datei };
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Verknüpfung fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Desktop-Verknüpfung, die eine Instanz startet UND direkt mit einem
|
||||
* bestimmten Server verbindet (Quick Play, ab Minecraft 1.20). Technisch
|
||||
* dasselbe wie instances:shortcut, nur mit zusätzlichem "--server="-Argument.
|
||||
* wunschName: optionaler, vom Nutzer selbst vergebener Dateiname (ohne .lnk)
|
||||
*/
|
||||
ipcMain.handle('servers:shortcut', (_e, id, ip, name, wunschName) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
if (!ip) return { ok: false, message: 'Keine Serveradresse angegeben.' };
|
||||
if (process.platform !== 'win32') {
|
||||
return { ok: false, message: 'Verknüpfungen werden nur unter Windows unterstützt.' };
|
||||
}
|
||||
try {
|
||||
const rohName = (wunschName && wunschName.trim())
|
||||
|| `${inst.name || 'Instanz'} - ${name || ip}`;
|
||||
const dateiName = String(rohName).replace(/[\\/:*?"<>|]/g, '_').trim() || 'Server';
|
||||
const datei = path.join(app.getPath('desktop'), `${dateiName}.lnk`);
|
||||
const ziel = verknuepfungsZiel();
|
||||
const ok = shell.writeShortcutLink(datei, fs.existsSync(datei) ? 'replace' : 'create', {
|
||||
target: ziel,
|
||||
// Gleichheitsform für beide Kennungen, damit sie beim Umsortieren durch
|
||||
// Chromium zusammenbleiben (siehe instanzAusArgumenten)
|
||||
args: verknuepfungsArgs([`--launch="${id}"`, `--server="${ip}"`]),
|
||||
cwd: path.dirname(ziel),
|
||||
description: `AeroMC – ${inst.name} → ${name || ip}`,
|
||||
icon: verknuepfungsIcon(ziel),
|
||||
iconIndex: 0,
|
||||
});
|
||||
if (!ok) return { ok: false, message: 'Verknüpfung konnte nicht geschrieben werden.' };
|
||||
@@ -1936,5 +2079,5 @@ ipcMain.handle('app:info', () => ({
|
||||
version: app.getVersion(),
|
||||
author: pkg.author || '',
|
||||
homepage: pkg.homepage || '',
|
||||
gitpage: typeof pkg.repository === 'string' ? pkg.repository : (pkg.repository?.url || ''),
|
||||
launcherWebsite: pkg.launcherWebsite || '',
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user