Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87fded4cb6 | ||
|
|
33e533009e | ||
|
|
17f8726075 | ||
|
|
deec93e5fa | ||
|
|
fc57dfa186 |
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# beim Bauen erzeugt, enthält Adresse und Token der CurseForge-Weiterleitung
|
||||||
|
src/build-config.json
|
||||||
|
|
||||||
|
# Zugangsdaten des Weiterleitungs-Servers
|
||||||
|
server/curseforge-proxy/.env
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
AeroMC – Lizenz- und Nutzungsbedingungen
|
||||||
|
|
||||||
|
Copyright (c) M_Viper. Alle Rechte vorbehalten.
|
||||||
|
|
||||||
|
Diese Software („AeroMC") sowie sämtlicher zugehöriger Quellcode, Ressourcen und
|
||||||
|
Dokumentation sind urheberrechtlich geschützt. Mit der Nutzung dieser Software
|
||||||
|
erkennst du die folgenden Bedingungen an.
|
||||||
|
|
||||||
|
1. Erlaubte Nutzung
|
||||||
|
Die Nutzung von AeroMC ist ausschließlich für den privaten, nicht-kommerziellen
|
||||||
|
Gebrauch gestattet.
|
||||||
|
|
||||||
|
2. Untersagt ist insbesondere
|
||||||
|
- Die Weitergabe dieses Launchers oder von Teilen davon an Dritte
|
||||||
|
- Das Verändern, Dekompilieren, Zurückentwickeln (Reverse Engineering) oder
|
||||||
|
Bearbeiten des Codes
|
||||||
|
- Die Veröffentlichung – auch in veränderter oder abgeleiteter Form –
|
||||||
|
an anderer Stelle als den offiziellen Kanälen des Autors
|
||||||
|
- Der Verkauf sowie jede sonstige kommerzielle Nutzung
|
||||||
|
|
||||||
|
3. Keine Gewährleistung
|
||||||
|
Die Software wird ohne jegliche Gewährleistung bereitgestellt, weder
|
||||||
|
ausdrücklich noch stillschweigend, einschließlich, aber nicht beschränkt auf
|
||||||
|
die Gewährleistung der Marktgängigkeit, der Eignung für einen bestimmten
|
||||||
|
Zweck und der Nichtverletzung von Rechten Dritter. Der Autor haftet nicht für
|
||||||
|
Schäden, die aus der Nutzung oder Unmöglichkeit der Nutzung der Software
|
||||||
|
entstehen.
|
||||||
|
|
||||||
|
4. Rechtsfolgen bei Verstoß
|
||||||
|
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich
|
||||||
|
belangt werden. Alle Rechte, die hier nicht ausdrücklich eingeräumt werden,
|
||||||
|
bleiben beim Autor vorbehalten.
|
||||||
|
|
||||||
|
5. Markenhinweis
|
||||||
|
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft. Minecraft ist
|
||||||
|
eine Marke von Mojang AB.
|
||||||
|
|
||||||
|
Kontakt: https://m-viper.de
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
|
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
|
||||||
|
|
||||||
Aktueller Stand: Version 1.0.6
|
Aktueller Stand: Version 1.0.8
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
- Warnt beim Entfernen eines Mods, falls ein anderer installierter Mod ihn noch als Pflicht-Abhängigkeit braucht
|
- Warnt beim Entfernen eines Mods, falls ein anderer installierter Mod ihn noch als Pflicht-Abhängigkeit braucht
|
||||||
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
||||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||||
- Serverliste mit Live-Status (Spieleranzahl, MOTD, Version) und Übernahme einzelner Einträge in andere Instanzen
|
- Serverliste mit Live-Status (Spieleranzahl, MOTD, Version), Übernahme einzelner Einträge in andere Instanzen sowie Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt, ab MC 1.20)
|
||||||
- Online- und Offline-Start
|
- Online- und Offline-Start
|
||||||
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
||||||
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
||||||
@@ -45,6 +45,7 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
|
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
|
||||||
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows, inkl. Installation zusätzlicher Add-ons
|
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows, inkl. Installation zusätzlicher Add-ons
|
||||||
- Update-Suche läuft automatisch beim Start, dazu manuelle Prüfung in den Einstellungen
|
- Update-Suche läuft automatisch beim Start, dazu manuelle Prüfung in den Einstellungen
|
||||||
|
- Optionaler Autostart mit Windows, dann im Infobereich statt als Fenster
|
||||||
- Design Hell/Dunkel und Sortierung der Instanzen
|
- Design Hell/Dunkel und Sortierung der Instanzen
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -88,7 +89,7 @@ Jede Instanz hat:
|
|||||||
- Instanz und Notizen bearbeiten
|
- Instanz und Notizen bearbeiten
|
||||||
- Mods, Ressourcenpakete und Shader
|
- Mods, Ressourcenpakete und Shader
|
||||||
- Welten (inkl. Schnellsicherung einzelner Welten), Datenpakete und Bildschirmfotos
|
- Welten (inkl. Schnellsicherung einzelner Welten), Datenpakete und Bildschirmfotos
|
||||||
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen)
|
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen, Desktop-Verknüpfung pro Server)
|
||||||
- Logdateien und Absturzberichte ansehen
|
- Logdateien und Absturzberichte ansehen
|
||||||
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
||||||
- Desktop-Verknüpfung erstellen
|
- Desktop-Verknüpfung erstellen
|
||||||
@@ -174,6 +175,7 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
|||||||
- Instanz-Verzeichnis
|
- Instanz-Verzeichnis
|
||||||
- Bedrock-Bereich anzeigen
|
- Bedrock-Bereich anzeigen
|
||||||
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
||||||
|
- Autostart: mit Windows starten, im Infobereich statt als Fenster – solange aktiv, versteckt „Schließen" (X) den Launcher nur, statt ihn zu beenden (über das Symbol im Infobereich wieder öffnen oder wirklich beenden)
|
||||||
|
|
||||||
### Darstellung
|
### Darstellung
|
||||||
- Block-Symbole räumlich (3D)
|
- Block-Symbole räumlich (3D)
|
||||||
@@ -222,7 +224,7 @@ Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
|
|||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Webseite: https://m-viper.de
|
- Webseite: https://m-viper.de
|
||||||
- Gitseite: https://git.viper.ipv64.net
|
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
||||||
- Autor: M_Viper
|
- Autor: M_Viper
|
||||||
|
|
||||||
## Rechtliches
|
## Rechtliches
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
|
|||||||
+6
-7
@@ -1,16 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||||
"homepage": "https://m-viper.de",
|
"homepage": "https://m-viper.de",
|
||||||
|
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"author": "M_Viper",
|
"author": "M_Viper",
|
||||||
"repository": {
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"type": "git",
|
"private": true,
|
||||||
"url": "https://git.viper.ipv64.net"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"prebuild": "node scripts/write-build-config.js",
|
"prebuild": "node scripts/write-build-config.js",
|
||||||
@@ -25,7 +23,8 @@
|
|||||||
"compression": "store",
|
"compression": "store",
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"package.json"
|
"package.json",
|
||||||
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
|
|||||||
+52
-2
@@ -130,6 +130,7 @@
|
|||||||
<option value="forge">Forge</option>
|
<option value="forge">Forge</option>
|
||||||
<option value="neoforge">NeoForge</option>
|
<option value="neoforge">NeoForge</option>
|
||||||
<option value="quilt">Quilt</option>
|
<option value="quilt">Quilt</option>
|
||||||
|
<option value="optifine">OptiFine</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -139,6 +140,23 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="f-optifine-tools" class="field-row hidden" style="align-items:flex-start">
|
||||||
|
<div class="field" style="flex:1">
|
||||||
|
<span>OptiFine einrichten</span>
|
||||||
|
<p class="hint" style="margin:2px 0 8px">
|
||||||
|
OptiFine bietet kein automatisches Download-API (Downloads laufen bei OptiFine bewusst über eine Werbeseite).
|
||||||
|
Lade den Installer zuerst selbst von <b>optifine.net</b> herunter, wähle ihn dann hier aus – AeroMC bereitet
|
||||||
|
die passende Minecraft-Version vor und startet den echten OptiFine-Installer. Wähle darin als Zielordner den
|
||||||
|
automatisch in die Zwischenablage kopierten Pfad, klicke „Install" und bestätige danach hier.
|
||||||
|
</p>
|
||||||
|
<div class="path-row">
|
||||||
|
<button type="button" id="f-optifine-install" class="btn">Installer wählen und starten…</button>
|
||||||
|
<button type="button" id="f-optifine-confirm" class="btn btn-primary hidden">Fertig – Version übernehmen</button>
|
||||||
|
</div>
|
||||||
|
<small id="f-optifine-status" class="hint"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Notizen <small class="opt">(optional)</small></span>
|
<span>Notizen <small class="opt">(optional)</small></span>
|
||||||
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
||||||
@@ -265,6 +283,14 @@
|
|||||||
</label>
|
</label>
|
||||||
<small class="hint">Erscheint nur, während das Launcher-Fenster nicht im Vordergrund ist.</small>
|
<small class="hint">Erscheint nur, während das Launcher-Fenster nicht im Vordergrund ist.</small>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card">
|
||||||
|
<h5>Autostart</h5>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="s-autostart" type="checkbox" /> <span>Mit Windows starten (im Infobereich)</span>
|
||||||
|
</label>
|
||||||
|
<small class="hint">Startet AeroMC beim Anmelden automatisch im Hintergrund. Solange aktiv, landet der Launcher beim Schließen (X) nur im Infobereich neben der Uhr, statt sich zu beenden – über das Symbol dort lässt er sich jederzeit wieder öffnen oder wirklich beenden.</small>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -634,6 +660,7 @@
|
|||||||
<option value="mod">Mods</option>
|
<option value="mod">Mods</option>
|
||||||
<option value="resourcepack">Ressourcenpakete</option>
|
<option value="resourcepack">Ressourcenpakete</option>
|
||||||
<option value="shader">Shader</option>
|
<option value="shader">Shader</option>
|
||||||
|
<option value="datapack">Datenpakete</option>
|
||||||
</select>
|
</select>
|
||||||
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
||||||
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
||||||
@@ -958,6 +985,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Lizenz ===================== -->
|
||||||
|
<div id="modal-license" class="modal hidden">
|
||||||
|
<div class="modal-card modal-lg">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>Lizenz</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre id="license-text" class="crash-voll">Lade …</pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn" data-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Dialog: Update ===================== -->
|
<!-- ===================== Dialog: Update ===================== -->
|
||||||
<div id="modal-update" class="modal hidden">
|
<div id="modal-update" class="modal hidden">
|
||||||
<div class="modal-card modal-sm">
|
<div class="modal-card modal-sm">
|
||||||
@@ -1011,11 +1054,18 @@
|
|||||||
|
|
||||||
<div id="import-pane-modpack" class="import-pane hidden">
|
<div id="import-pane-modpack" class="import-pane hidden">
|
||||||
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
||||||
<p class="hint">Suche läuft über Modrinth. Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json) – dafür ist ein API-Key oder eine Weiterleitung in den Netzwerk-Einstellungen nötig.</p>
|
<p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Für CurseForge wird ein eigener API-Key in den Netzwerk-Einstellungen gebraucht (die Weiterleitung reicht nur für den Datei-Import). Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p>
|
||||||
|
|
||||||
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
||||||
|
<label class="field" style="width:150px">
|
||||||
|
<span>Quelle</span>
|
||||||
|
<select id="mp-source">
|
||||||
|
<option value="modrinth">Modrinth</option>
|
||||||
|
<option value="curseforge">CurseForge</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="field" style="flex:1">
|
<label class="field" style="flex:1">
|
||||||
<span>Suche (Modrinth)</span>
|
<span>Suche</span>
|
||||||
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field" style="width:140px">
|
<label class="field" style="width:140px">
|
||||||
|
|||||||
@@ -464,6 +464,37 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
||||||
}
|
}
|
||||||
effectiveVj = mergeLoader(vj, fvj);
|
effectiveVj = mergeLoader(vj, fvj);
|
||||||
|
|
||||||
|
} else if (opts.loader === 'optifine') {
|
||||||
|
/*
|
||||||
|
* OptiFine hat keine öffentliche Installations-API (Downloads laufen bewusst
|
||||||
|
* über eine Werbeseite). AeroMC lädt hier nichts selbst herunter – der Nutzer
|
||||||
|
* hat den offiziellen Installer bereits einmal sichtbar laufen lassen (siehe
|
||||||
|
* optifine:setup/optifine:confirm in main.js), der dabei ein normales
|
||||||
|
* Versions-Profil unter <sharedDir>/versions/<loaderVersion>/ abgelegt hat,
|
||||||
|
* genau wie Fabric/Forge es tun. Wir lesen es nur noch ein und mergen es.
|
||||||
|
*/
|
||||||
|
const ofId = opts.loaderVersion;
|
||||||
|
if (!ofId) {
|
||||||
|
return { ok: false, message: 'Für diese Instanz ist keine OptiFine-Version hinterlegt. Bitte im Bearbeiten-Dialog einrichten.' };
|
||||||
|
}
|
||||||
|
const ofJsonPath = path.join(opts.sharedDir, 'versions', ofId, ofId + '.json');
|
||||||
|
if (!fs.existsSync(ofJsonPath)) {
|
||||||
|
return { ok: false, message: `OptiFine-Profil "${ofId}" wurde nicht gefunden. Bitte die Einrichtung im Bearbeiten-Dialog wiederholen.` };
|
||||||
|
}
|
||||||
|
let ofvj;
|
||||||
|
try { ofvj = JSON.parse(fs.readFileSync(ofJsonPath, 'utf8')); }
|
||||||
|
catch (err) { return { ok: false, message: 'OptiFine-Profil ist beschädigt: ' + err.message }; }
|
||||||
|
for (const lib of ofvj.libraries || []) {
|
||||||
|
const art = lib.downloads && lib.downloads.artifact;
|
||||||
|
const rel = (art && art.path) || forge.mavenPath(lib.name);
|
||||||
|
const dest = path.join(libDir, rel.replace(/\//g, path.sep));
|
||||||
|
if (!fs.existsSync(dest) && art && art.url) {
|
||||||
|
await downloadIfNeeded(art.url, dest, art.sha1 || null);
|
||||||
|
}
|
||||||
|
classpath.unshift(dest);
|
||||||
|
}
|
||||||
|
effectiveVj = mergeLoader(vj, ofvj);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
||||||
|
|||||||
+329
-20
@@ -6,11 +6,11 @@
|
|||||||
* (Vanilla-Start, Microsoft-Login und Mod-Loader folgen in späteren Phasen.)
|
* (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, clipboard } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { execFile } = require('child_process');
|
const { execFile, spawn } = require('child_process');
|
||||||
const pkg = require('../package.json');
|
const pkg = require('../package.json');
|
||||||
const store = require('./store');
|
const store = require('./store');
|
||||||
const services = require('./services');
|
const services = require('./services');
|
||||||
@@ -124,18 +124,29 @@ function instanzAusArgumenten(argv) {
|
|||||||
return null;
|
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
|
// 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;
|
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) => {
|
app.on('second-instance', (_e, argv) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
// showMainWindow() statt nur restore()+focus(): bei aktivem Autostart kann
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
// das Fenster echt versteckt sein (hide(), nicht nur minimiert) – das
|
||||||
mainWindow.focus();
|
// braucht ein show(), sonst bleibt es beim zweiten Start unsichtbar
|
||||||
}
|
showMainWindow();
|
||||||
starteInstanzImFenster(instanzAusArgumenten(argv));
|
starteInstanzImFenster(instanzAusArgumenten(argv), serverAusArgumenten(argv));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ziel für Verknüpfungen: bei der Portable-Fassung zeigt process.execPath auf
|
// Ziel für Verknüpfungen: bei der Portable-Fassung zeigt process.execPath auf
|
||||||
@@ -147,6 +158,31 @@ function verknuepfungsZiel() {
|
|||||||
return process.execPath;
|
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() {
|
function windowIcon() {
|
||||||
const png = path.join(__dirname, 'assets', 'logo.png');
|
const png = path.join(__dirname, 'assets', 'logo.png');
|
||||||
const ico = path.join(__dirname, 'assets', 'icon.ico');
|
const ico = path.join(__dirname, 'assets', 'icon.ico');
|
||||||
@@ -155,7 +191,7 @@ function windowIcon() {
|
|||||||
return undefined; // Electron-Standard, solange kein Logo hinterlegt ist
|
return undefined; // Electron-Standard, solange kein Logo hinterlegt ist
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow(startHidden) {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 820,
|
height: 820,
|
||||||
@@ -165,6 +201,7 @@ function createWindow() {
|
|||||||
title: 'AeroMC',
|
title: 'AeroMC',
|
||||||
icon: windowIcon(),
|
icon: windowIcon(),
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
|
show: !startHidden,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
@@ -175,13 +212,69 @@ function createWindow() {
|
|||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
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 startId = instanzAusArgumenten(process.argv);
|
||||||
|
const startServer = serverAusArgumenten(process.argv);
|
||||||
if (startId) {
|
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(() => {
|
app.whenReady().then(() => {
|
||||||
if (!hatEinzelSperre) return; // zweite Instanz beendet sich selbst
|
if (!hatEinzelSperre) return; // zweite Instanz beendet sich selbst
|
||||||
store.init(app.getPath('userData'));
|
store.init(app.getPath('userData'));
|
||||||
@@ -195,7 +288,12 @@ app.whenReady().then(() => {
|
|||||||
decrypt: (b) => safeStorage.decryptString(Buffer.from(b, 'base64')),
|
decrypt: (b) => safeStorage.decryptString(Buffer.from(b, 'base64')),
|
||||||
});
|
});
|
||||||
applyProxy(store.getSettings());
|
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
|
// Java-Suche im Hintergrund vorbereiten -> erster Spielstart ohne Verzögerung
|
||||||
setTimeout(() => { try { services.scanJava(); } catch { /* egal */ } }, 800);
|
setTimeout(() => { try { services.scanJava(); } catch { /* egal */ } }, 800);
|
||||||
@@ -255,6 +353,8 @@ app.on('window-all-closed', () => {
|
|||||||
if (process.platform !== 'darwin') app.quit();
|
if (process.platform !== 'darwin') app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => { appIsQuitting = true; });
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Java-Erkennung (für Anzeige in den Einstellungen)
|
// Java-Erkennung (für Anzeige in den Einstellungen)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -277,7 +377,12 @@ function detectJava(javaPath) {
|
|||||||
|
|
||||||
ipcMain.handle('settings:get', () => store.getSettings());
|
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());
|
ipcMain.handle('settings:instancesDir', () => store.getInstancesDir());
|
||||||
|
|
||||||
@@ -451,6 +556,111 @@ ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
|
|||||||
catch { return []; }
|
catch { return []; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OptiFine – kein offizielles Download-API (Downloads laufen bei OptiFine
|
||||||
|
// bewusst über eine Werbeseite, das umgehen wir nicht). Stattdessen: Nutzer
|
||||||
|
// lädt den Installer selbst von optifine.net, AeroMC bereitet Vanilla vor,
|
||||||
|
// startet den echten Installer sichtbar mit dem passenden Zielordner in der
|
||||||
|
// Zwischenablage und übernimmt danach die neu entstandene Profil-Version.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const optifineSetupState = new Map(); // instanceId -> { sharedDir, before: Set<string> }
|
||||||
|
|
||||||
|
function scanOptifineVersions(sharedDir, mcVersion) {
|
||||||
|
const versionsDir = path.join(sharedDir, 'versions');
|
||||||
|
if (!fs.existsSync(versionsDir)) return [];
|
||||||
|
const prefix = String(mcVersion || '');
|
||||||
|
return fs.readdirSync(versionsDir, { withFileTypes: true })
|
||||||
|
.filter((e) => e.isDirectory())
|
||||||
|
.map((e) => e.name)
|
||||||
|
.filter((name) => /optifine/i.test(name) && (!prefix || name.startsWith(prefix)))
|
||||||
|
.filter((name) => fs.existsSync(path.join(versionsDir, name, name + '.json')))
|
||||||
|
.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('optifine:find', (_e, mcVersion) => {
|
||||||
|
const sharedDir = path.join(app.getPath('userData'), 'shared');
|
||||||
|
return scanOptifineVersions(sharedDir, mcVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('optifine:setup', async (_e, id) => {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
if (!inst.minecraft.version) return { ok: false, message: 'Bitte zuerst eine Minecraft-Version wählen.' };
|
||||||
|
|
||||||
|
const picked = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
title: 'OptiFine-Installer wählen (zuvor von optifine.net heruntergeladen)',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{ name: 'OptiFine-Installer', extensions: ['jar'] }],
|
||||||
|
});
|
||||||
|
if (picked.canceled || !picked.filePaths.length) return { ok: false, canceled: true };
|
||||||
|
const installerPath = picked.filePaths[0];
|
||||||
|
|
||||||
|
const settings = store.getSettings();
|
||||||
|
const sharedDir = path.join(app.getPath('userData'), 'shared');
|
||||||
|
const opts = {
|
||||||
|
versionId: inst.minecraft.version,
|
||||||
|
loader: 'vanilla',
|
||||||
|
loaderVersion: '',
|
||||||
|
gameDir: store.gameDir(id),
|
||||||
|
sharedDir,
|
||||||
|
userDataDir: app.getPath('userData'),
|
||||||
|
javaPath: inst.java.path || settings.javaPath || '',
|
||||||
|
minMemMb: inst.java.minMemMb || settings.defaultMinMemMb,
|
||||||
|
maxMemMb: inst.java.maxMemMb || settings.defaultMaxMemMb,
|
||||||
|
extraArgs: '',
|
||||||
|
auth: null,
|
||||||
|
ensureJava: settings.autoJava === false ? null : (major, onProgress) => javadl.ensureJava(major, onProgress),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vanilla-Dateien sicherstellen (kein echter Start, da kein Konto übergeben wird)
|
||||||
|
let prep;
|
||||||
|
try {
|
||||||
|
prep = await launcher.prepareAndLaunch(opts, (evt) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('launch:progress', Object.assign({ id }, evt));
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Vorbereitung fehlgeschlagen: ' + err.message };
|
||||||
|
}
|
||||||
|
if (!prep.needAuth && !prep.ok) return { ok: false, message: prep.message || 'Vorbereitung fehlgeschlagen.' };
|
||||||
|
const javaExe = (prep.java && prep.java.path) || opts.javaPath || 'java';
|
||||||
|
|
||||||
|
optifineSetupState.set(id, {
|
||||||
|
sharedDir,
|
||||||
|
before: new Set(scanOptifineVersions(sharedDir, inst.minecraft.version)),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const child = spawn(javaExe, ['-jar', installerPath], { detached: true, stdio: 'ignore' });
|
||||||
|
child.unref();
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Installer konnte nicht gestartet werden: ' + err.message };
|
||||||
|
}
|
||||||
|
try { clipboard.writeText(sharedDir); } catch { /* egal */ }
|
||||||
|
|
||||||
|
return { ok: true, sharedDir };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('optifine:confirm', (_e, id) => {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
const state = optifineSetupState.get(id);
|
||||||
|
const sharedDir = (state && state.sharedDir) || path.join(app.getPath('userData'), 'shared');
|
||||||
|
const nachher = scanOptifineVersions(sharedDir, inst.minecraft.version);
|
||||||
|
const vorher = (state && state.before) || new Set();
|
||||||
|
const neu = nachher.filter((v) => !vorher.has(v));
|
||||||
|
const treffer = neu[0] || nachher[0];
|
||||||
|
optifineSetupState.delete(id);
|
||||||
|
if (!treffer) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: 'Keine OptiFine-Version für ' + (inst.minecraft.version || '?') + ' gefunden. '
|
||||||
|
+ 'Wurde die Installation im OptiFine-Fenster abgeschlossen und dabei der kopierte Ordner ausgewählt?',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true, versionId: treffer };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('mods:remove', (_e, id, filename, art) => store.removeMod(id, filename, art || 'mod'));
|
ipcMain.handle('mods:remove', (_e, id, filename, art) => store.removeMod(id, filename, art || 'mod'));
|
||||||
// prüft, ob andere installierte Mods diesen hier als Pflicht-Abhängigkeit brauchen
|
// prüft, ob andere installierte Mods diesen hier als Pflicht-Abhängigkeit brauchen
|
||||||
ipcMain.handle('mods:dependents', (_e, id, filename, art) => store.findDependents(id, filename, art || 'mod'));
|
ipcMain.handle('mods:dependents', (_e, id, filename, art) => store.findDependents(id, filename, art || 'mod'));
|
||||||
@@ -1148,8 +1358,13 @@ async function importCurseForge(buf, zip, zugang) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle('modpack:search', async (_e, opts) => {
|
ipcMain.handle('modpack:search', async (_e, opts) => {
|
||||||
|
const o = opts || {};
|
||||||
try {
|
try {
|
||||||
const q = Object.assign({}, opts || {}, { art: 'modpack' });
|
if (o.source === 'curseforge') {
|
||||||
|
const zugang = services.curseforgeZugang(store.getSettings());
|
||||||
|
return await services.curseforgeSearch(zugang, Object.assign({}, o, { art: 'modpack' }));
|
||||||
|
}
|
||||||
|
const q = Object.assign({}, o, { art: 'modpack' });
|
||||||
return await services.modrinthSearch(q);
|
return await services.modrinthSearch(q);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { total: 0, hits: [], error: err.message };
|
return { total: 0, hits: [], error: err.message };
|
||||||
@@ -1208,6 +1423,52 @@ ipcMain.handle('modpack:installFromModrinth', async (_e, project) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('modpack:installFromCurseForge', async (_e, project) => {
|
||||||
|
try {
|
||||||
|
const zugang = services.curseforgeZugang(store.getSettings());
|
||||||
|
if (!zugang) {
|
||||||
|
return { ok: false, needsKey: true,
|
||||||
|
message: 'CurseForge-Modpacks brauchen einen Zugang. Trage in den Netzwerk-Einstellungen entweder deinen eigenen API-Key ein (kostenlos über console.curseforge.com) oder die Adresse einer CurseForge-Weiterleitung.' };
|
||||||
|
}
|
||||||
|
const rawId = String((project && (project.cfId || project.projectId || project.id)) || '').replace(/^cf-/, '');
|
||||||
|
const modId = Number(rawId);
|
||||||
|
if (!modId) return { ok: false, message: 'Kein Modpack gewählt.' };
|
||||||
|
const gameVersion = (project && project.gameVersion) || '';
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('launch:progress', {
|
||||||
|
phase: 'modpack', detail: 'Modpack-Version wird ermittelt …',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const file = await services.curseforgeResolveModpackFile(zugang, modId, gameVersion);
|
||||||
|
if (!file || !file.url) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: gameVersion
|
||||||
|
? ('Keine Modpack-Version für Minecraft ' + gameVersion + ' gefunden.')
|
||||||
|
: 'Keine passende Modpack-Datei gefunden.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('launch:progress', {
|
||||||
|
phase: 'modpack', detail: 'Lade ' + file.filename + ' …',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const dlUrl = zugang.downloadAdresse ? zugang.downloadAdresse(file.url) : file.url;
|
||||||
|
const buf = await services.downloadBuffer(dlUrl, zugang.downloadKopfzeilen);
|
||||||
|
const zip = forge.readZipIndex(buf);
|
||||||
|
if (!zip['manifest.json']) {
|
||||||
|
return { ok: false, message: 'Die geladene Datei enthält kein manifest.json – kein gültiges CurseForge-Modpack.' };
|
||||||
|
}
|
||||||
|
const result = await importCurseForge(buf, zip, zugang);
|
||||||
|
if (result && result.ok && result.instance) {
|
||||||
|
notify('Modpack installiert', `${result.instance.name} ist einsatzbereit.`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Modpack-Installation fehlgeschlagen: ' + err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Kern des Modpack-Datei-Imports, unabhängig davon ob der Pfad aus einem
|
* Kern des Modpack-Datei-Imports, unabhängig davon ob der Pfad aus einem
|
||||||
* Dateidialog oder per Drag&Drop aufs Fenster kommt.
|
* Dateidialog oder per Drag&Drop aufs Fenster kommt.
|
||||||
@@ -1251,24 +1512,62 @@ ipcMain.handle('modpack:import', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verknüpfung auf dem Desktop, die genau diese Instanz startet
|
// 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);
|
const inst = store.getInstance(id);
|
||||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
return { ok: false, message: 'Verknüpfungen werden nur unter Windows unterstützt.' };
|
return { ok: false, message: 'Verknüpfungen werden nur unter Windows unterstützt.' };
|
||||||
}
|
}
|
||||||
try {
|
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 datei = path.join(app.getPath('desktop'), name + '.lnk');
|
||||||
const ziel = verknuepfungsZiel();
|
const ziel = verknuepfungsZiel();
|
||||||
const ok = shell.writeShortcutLink(datei, fs.existsSync(datei) ? 'replace' : 'create', {
|
const ok = shell.writeShortcutLink(datei, fs.existsSync(datei) ? 'replace' : 'create', {
|
||||||
target: ziel,
|
target: ziel,
|
||||||
// Gleichheitsform, damit die Kennung beim Umsortieren durch Chromium
|
// Gleichheitsform, damit die Kennung beim Umsortieren durch Chromium
|
||||||
// an ihrem Schalter bleibt (siehe instanzAusArgumenten)
|
// an ihrem Schalter bleibt (siehe instanzAusArgumenten)
|
||||||
args: `--launch="${id}"`,
|
args: verknuepfungsArgs([`--launch="${id}"`]),
|
||||||
cwd: path.dirname(ziel),
|
cwd: path.dirname(ziel),
|
||||||
description: 'AeroMC – ' + inst.name,
|
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,
|
iconIndex: 0,
|
||||||
});
|
});
|
||||||
if (!ok) return { ok: false, message: 'Verknüpfung konnte nicht geschrieben werden.' };
|
if (!ok) return { ok: false, message: 'Verknüpfung konnte nicht geschrieben werden.' };
|
||||||
@@ -1936,5 +2235,15 @@ ipcMain.handle('app:info', () => ({
|
|||||||
version: app.getVersion(),
|
version: app.getVersion(),
|
||||||
author: pkg.author || '',
|
author: pkg.author || '',
|
||||||
homepage: pkg.homepage || '',
|
homepage: pkg.homepage || '',
|
||||||
gitpage: typeof pkg.repository === 'string' ? pkg.repository : (pkg.repository?.url || ''),
|
launcherWebsite: pkg.launcherWebsite || '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Lizenztext für die Info-Seite in den Einstellungen. Die LICENSE-Datei liegt
|
||||||
|
// im App-Root und wird über "files" im electron-builder-Config mitgepackt.
|
||||||
|
ipcMain.handle('app:license', () => {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path.join(app.getAppPath(), 'LICENSE'), 'utf8');
|
||||||
|
} catch {
|
||||||
|
return 'Lizenztext konnte nicht geladen werden.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
+13
-3
@@ -19,6 +19,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// Minecraft-Versionen (Mojang)
|
// Minecraft-Versionen (Mojang)
|
||||||
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
||||||
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
||||||
|
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
||||||
|
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
||||||
|
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
||||||
|
|
||||||
// Mods (Modrinth)
|
// Mods (Modrinth)
|
||||||
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
||||||
@@ -87,18 +90,24 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
return () => ipcRenderer.removeListener('backup:progress', l);
|
return () => ipcRenderer.removeListener('backup:progress', l);
|
||||||
},
|
},
|
||||||
importInstance: () => ipcRenderer.invoke('instances:import'),
|
importInstance: () => ipcRenderer.invoke('instances:import'),
|
||||||
createShortcut: (id) => ipcRenderer.invoke('instances:shortcut', id),
|
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
||||||
// Start über eine Desktop-Verknüpfung ("--launch <id>")
|
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
||||||
|
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
||||||
onLaunchRequest: (cb) => {
|
onLaunchRequest: (cb) => {
|
||||||
const l = (_e, id) => cb(id);
|
const l = (_e, payload) => cb(payload);
|
||||||
ipcRenderer.on('instance:launch-request', l);
|
ipcRenderer.on('instance:launch-request', l);
|
||||||
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
||||||
},
|
},
|
||||||
|
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
|
||||||
|
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
|
||||||
|
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
|
||||||
|
notifyReady: () => ipcRenderer.send('renderer:ready'),
|
||||||
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
||||||
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
||||||
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
||||||
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
||||||
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
||||||
|
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
||||||
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
||||||
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
||||||
onLaunchProgress: (cb) => {
|
onLaunchProgress: (cb) => {
|
||||||
@@ -231,4 +240,5 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
||||||
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
||||||
appInfo: () => ipcRenderer.invoke('app:info'),
|
appInfo: () => ipcRenderer.invoke('app:info'),
|
||||||
|
appLicense: () => ipcRenderer.invoke('app:license'),
|
||||||
});
|
});
|
||||||
|
|||||||
+174
-19
@@ -467,7 +467,7 @@ function iconOf(key, size = 44) {
|
|||||||
|
|
||||||
const LOADER_LABEL = {
|
const LOADER_LABEL = {
|
||||||
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
|
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
|
||||||
neoforge: 'NeoForge', quilt: 'Quilt',
|
neoforge: 'NeoForge', quilt: 'Quilt', optifine: 'OptiFine',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren
|
// Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren
|
||||||
@@ -645,17 +645,28 @@ async function searchModpacks() {
|
|||||||
const query = el('mp-query').value.trim();
|
const query = el('mp-query').value.trim();
|
||||||
const gameVersion = el('mp-mc').value.trim();
|
const gameVersion = el('mp-mc').value.trim();
|
||||||
const sort = el('mp-sort').value || 'relevance';
|
const sort = el('mp-sort').value || 'relevance';
|
||||||
|
const source = (el('mp-source') && el('mp-source').value) || 'modrinth';
|
||||||
const box = el('mp-results');
|
const box = el('mp-results');
|
||||||
const st = el('mp-status');
|
const st = el('mp-status');
|
||||||
st.textContent = 'Suche …';
|
st.textContent = 'Suche …';
|
||||||
box.innerHTML = '<div class="mods-empty">Suche läuft …</div>';
|
box.innerHTML = '<div class="mods-empty">Suche läuft …</div>';
|
||||||
try {
|
try {
|
||||||
const res = await window.api.modpackSearch({
|
const res = await window.api.modpackSearch({
|
||||||
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack',
|
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack', source,
|
||||||
});
|
});
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
st.textContent = 'Fehler: ' + res.error;
|
st.textContent = 'Fehler: ' + res.error;
|
||||||
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
|
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
|
||||||
|
if (res.needKey) {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'mods-empty';
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'btn btn-mini';
|
||||||
|
btn.textContent = 'Zu den Netzwerk-Einstellungen';
|
||||||
|
btn.addEventListener('click', () => { hide('modal-import'); openSettings('pane-network'); });
|
||||||
|
wrap.appendChild(btn);
|
||||||
|
box.appendChild(wrap);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hits = res.hits || [];
|
const hits = res.hits || [];
|
||||||
@@ -703,11 +714,15 @@ async function installModpackFromSearch(hit, btn) {
|
|||||||
if (btn) { btn.disabled = true; btn.textContent = '…'; }
|
if (btn) { btn.disabled = true; btn.textContent = '…'; }
|
||||||
status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …');
|
status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …');
|
||||||
try {
|
try {
|
||||||
const res = await window.api.modpackInstallFromModrinth({
|
const res = hit.source === 'curseforge'
|
||||||
projectId: hit.projectId,
|
? await window.api.modpackInstallFromCurseForge({
|
||||||
title: hit.title || hit.slug,
|
cfId: hit.cfId, projectId: hit.projectId, title: hit.title || hit.slug, gameVersion,
|
||||||
gameVersion,
|
})
|
||||||
});
|
: await window.api.modpackInstallFromModrinth({
|
||||||
|
projectId: hit.projectId,
|
||||||
|
title: hit.title || hit.slug,
|
||||||
|
gameVersion,
|
||||||
|
});
|
||||||
status('Bereit'); el('status-right').textContent = '';
|
status('Bereit'); el('status-right').textContent = '';
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await reload();
|
await reload();
|
||||||
@@ -1786,7 +1801,44 @@ async function updateLoaderVersions(preselect) {
|
|||||||
const hint = el('f-loader-hint');
|
const hint = el('f-loader-hint');
|
||||||
sel.innerHTML = '<option value="">neueste (empfohlen)</option>';
|
sel.innerHTML = '<option value="">neueste (empfohlen)</option>';
|
||||||
|
|
||||||
|
const ofTools = el('f-optifine-tools');
|
||||||
|
if (ofTools) ofTools.classList.toggle('hidden', loader !== 'optifine');
|
||||||
|
|
||||||
if (loader === 'vanilla') { sel.disabled = true; hint.textContent = 'Vanilla hat keinen Loader.'; return; }
|
if (loader === 'vanilla') { sel.disabled = true; hint.textContent = 'Vanilla hat keinen Loader.'; return; }
|
||||||
|
|
||||||
|
if (loader === 'optifine') {
|
||||||
|
// Kein Loader-API bei OptiFine – die Liste zeigt bereits eingerichtete
|
||||||
|
// Fassungen (siehe f-optifine-tools weiter unten), keine automatisch
|
||||||
|
// ladbaren Versionen.
|
||||||
|
sel.innerHTML = '<option value="">– keine gewählt –</option>';
|
||||||
|
sel.disabled = false;
|
||||||
|
if (el('f-optifine-status')) el('f-optifine-status').textContent = '';
|
||||||
|
if (el('f-optifine-confirm')) el('f-optifine-confirm').classList.add('hidden');
|
||||||
|
if (!editingId) {
|
||||||
|
hint.textContent = 'Erst speichern, dann lässt sich OptiFine im Bearbeiten-Dialog einrichten.';
|
||||||
|
if (el('f-optifine-install')) el('f-optifine-install').disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el('f-optifine-install')) el('f-optifine-install').disabled = false;
|
||||||
|
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
|
||||||
|
hint.textContent = 'Suche bereits eingerichtete OptiFine-Fassungen …';
|
||||||
|
try {
|
||||||
|
const found = await window.api.optifineFind(mc);
|
||||||
|
for (const v of found) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = v; o.textContent = v;
|
||||||
|
if (v === preselect) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
hint.textContent = found.length
|
||||||
|
? `${found.length} eingerichtete OptiFine-Fassung(en) für ${mc}.`
|
||||||
|
: 'Noch keine OptiFine-Installation für diese Version – über den Knopf unten einrichten.';
|
||||||
|
} catch {
|
||||||
|
hint.textContent = 'Suche fehlgeschlagen.';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sel.disabled = false;
|
sel.disabled = false;
|
||||||
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
|
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
|
||||||
|
|
||||||
@@ -2155,6 +2207,27 @@ async function renderServers() {
|
|||||||
});
|
});
|
||||||
aktionen.appendChild(starten);
|
aktionen.appendChild(starten);
|
||||||
|
|
||||||
|
const favorit = document.createElement('button');
|
||||||
|
favorit.className = 'btn btn-mini';
|
||||||
|
favorit.textContent = '★ Verknüpfung';
|
||||||
|
favorit.title = 'Desktop-Verknüpfung erstellen, die Minecraft startet und direkt mit diesem Server verbindet (Quick Play, ab MC 1.20)';
|
||||||
|
favorit.addEventListener('click', async () => {
|
||||||
|
const inst = await window.api.getInstance(currentDetailId);
|
||||||
|
const vorschlag = `${inst ? inst.name : 'Instanz'} - ${s.name || s.ip}`;
|
||||||
|
const eingabe = await askPrompt({
|
||||||
|
title: 'Server-Verknüpfung erstellen',
|
||||||
|
text: 'Name der Desktop-Verknüpfung (ohne .lnk):',
|
||||||
|
label: 'Name',
|
||||||
|
value: vorschlag,
|
||||||
|
okLabel: 'Erstellen',
|
||||||
|
});
|
||||||
|
if (eingabe == null) return; // abgebrochen
|
||||||
|
const res = await window.api.serversShortcut(currentDetailId, s.ip, s.name, eingabe.trim());
|
||||||
|
if (res.ok) toast('Verknüpfung auf dem Desktop erstellt: ' + (s.name || s.ip));
|
||||||
|
else toast(res.message || 'Verknüpfung fehlgeschlagen.', true);
|
||||||
|
});
|
||||||
|
aktionen.appendChild(favorit);
|
||||||
|
|
||||||
const umbenennen = document.createElement('button');
|
const umbenennen = document.createElement('button');
|
||||||
umbenennen.className = 'btn btn-mini';
|
umbenennen.className = 'btn btn-mini';
|
||||||
umbenennen.textContent = 'Umbenennen';
|
umbenennen.textContent = 'Umbenennen';
|
||||||
@@ -2688,14 +2761,23 @@ function renderContentList(boxId, items, sub, emoji) {
|
|||||||
|
|
||||||
// Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader
|
// Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader
|
||||||
let modArt = 'mod';
|
let modArt = 'mod';
|
||||||
const ART_LABEL = { mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader' };
|
const ART_LABEL = {
|
||||||
|
mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader', datapack: 'Datenpakete',
|
||||||
|
};
|
||||||
const ART_SUCHE = {
|
const ART_SUCHE = {
|
||||||
mod: 'Mods durchsuchen … (z. B. Sodium, JEI, Create)',
|
mod: 'Mods durchsuchen … (z. B. Sodium, JEI, Create)',
|
||||||
resourcepack: 'Ressourcenpakete durchsuchen … (z. B. Faithful, Bare Bones)',
|
resourcepack: 'Ressourcenpakete durchsuchen … (z. B. Faithful, Bare Bones)',
|
||||||
shader: 'Shader durchsuchen … (z. B. Complementary, BSL)',
|
shader: 'Shader durchsuchen … (z. B. Complementary, BSL)',
|
||||||
|
datapack: 'Datenpakete durchsuchen … (z. B. Terralith, Vanilla Tweaks)',
|
||||||
};
|
};
|
||||||
// Nur Mods brauchen einen Loader – Ressourcenpakete und Shader laufen auch in Vanilla
|
// Nur Mods brauchen einen Loader – Ressourcenpakete, Shader und Datenpakete laufen auch in Vanilla
|
||||||
const ART_BRAUCHT_LOADER = { mod: true, resourcepack: false, shader: false };
|
const ART_BRAUCHT_LOADER = {
|
||||||
|
mod: true, resourcepack: false, shader: false, datapack: false,
|
||||||
|
};
|
||||||
|
// CurseForge hat für Datenpakete keinen verlässlich zugeordneten API-Klassencode in
|
||||||
|
// diesem Launcher – dort deshalb nur Modrinth als Quelle anbieten, statt falsche
|
||||||
|
// Treffer (z. B. Mods) unter "Datenpakete" anzuzeigen.
|
||||||
|
const ART_NUR_MODRINTH = { datapack: true };
|
||||||
|
|
||||||
async function openMods() {
|
async function openMods() {
|
||||||
const inst = await window.api.getInstance(currentDetailId);
|
const inst = await window.api.getInstance(currentDetailId);
|
||||||
@@ -2733,6 +2815,10 @@ async function zeigeModAnsicht(inst) {
|
|||||||
['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; });
|
['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; });
|
||||||
// Kategorien gelten nur für Mods
|
// Kategorien gelten nur für Mods
|
||||||
el('m-category').disabled = blocked || modArt !== 'mod';
|
el('m-category').disabled = blocked || modArt !== 'mod';
|
||||||
|
// Manche Quellen decken nicht jede Inhaltsart ab (z. B. CurseForge <-> Datenpakete)
|
||||||
|
const cfOption = el('m-source').querySelector('option[value="curseforge"]');
|
||||||
|
if (cfOption) cfOption.disabled = !!ART_NUR_MODRINTH[modArt];
|
||||||
|
if (ART_NUR_MODRINTH[modArt] && el('m-source').value === 'curseforge') el('m-source').value = 'modrinth';
|
||||||
el('m-query').value = '';
|
el('m-query').value = '';
|
||||||
el('m-more').classList.add('hidden');
|
el('m-more').classList.add('hidden');
|
||||||
// Ergebnisse der vorigen Ansicht nicht mitschleppen
|
// Ergebnisse der vorigen Ansicht nicht mitschleppen
|
||||||
@@ -2792,9 +2878,13 @@ async function refreshInstalled() {
|
|||||||
if (up) kennzeichen.push('<span class="mod-new">Update</span>');
|
if (up) kennzeichen.push('<span class="mod-new">Update</span>');
|
||||||
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
|
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
|
||||||
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
|
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
|
||||||
|
// OptiFine lässt sich nicht über Modrinth/CurseForge installieren (kein API-Zugriff auf
|
||||||
|
// deren Downloads) – von Hand hinzugefügt taucht es sonst wie ein beliebiger Mod auf.
|
||||||
|
const istOptiFine = modArt === 'mod' && /^optifine[_-]/i.test(m.filename || '');
|
||||||
|
if (istOptiFine) kennzeichen.push('<span class="mod-off" style="opacity:.8">OptiFine</span>');
|
||||||
|
|
||||||
item.innerHTML =
|
item.innerHTML =
|
||||||
`<div class="mod-icon">${aus ? '🚫' : '🧩'}</div>
|
`<div class="mod-icon">${aus ? '🚫' : istOptiFine ? '⚡' : '🧩'}</div>
|
||||||
<div class="mod-info">
|
<div class="mod-info">
|
||||||
<div class="mod-name">${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}</div>
|
<div class="mod-name">${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}</div>
|
||||||
<div class="mod-meta">${up
|
<div class="mod-meta">${up
|
||||||
@@ -3054,6 +3144,7 @@ async function openSettings(pane) {
|
|||||||
el('s-iso3d').checked = s.iso3dIcons !== false;
|
el('s-iso3d').checked = s.iso3dIcons !== false;
|
||||||
el('s-bedrock').checked = s.bedrockEnabled !== false;
|
el('s-bedrock').checked = s.bedrockEnabled !== false;
|
||||||
el('s-notify').checked = s.desktopNotifications !== false;
|
el('s-notify').checked = s.desktopNotifications !== false;
|
||||||
|
el('s-autostart').checked = !!s.autostart;
|
||||||
el('s-auto-java').checked = s.autoJava !== false;
|
el('s-auto-java').checked = s.autoJava !== false;
|
||||||
el('s-autobackup').checked = !!s.autoBackupEnabled;
|
el('s-autobackup').checked = !!s.autoBackupEnabled;
|
||||||
el('s-autobackup-every').value = s.autoBackupEvery || 'daily';
|
el('s-autobackup-every').value = s.autoBackupEvery || 'daily';
|
||||||
@@ -3100,18 +3191,25 @@ async function openSettings(pane) {
|
|||||||
['Version', formatVersionLabel(info.version)],
|
['Version', formatVersionLabel(info.version)],
|
||||||
['Autor', info.author || 'Nicht hinterlegt'],
|
['Autor', info.author || 'Nicht hinterlegt'],
|
||||||
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
|
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
|
||||||
['Gitseite', info.gitpage || 'Nicht hinterlegt', info.gitpage || ''],
|
['Launcher-Webseite', info.launcherWebsite || 'Nicht hinterlegt', info.launcherWebsite || ''],
|
||||||
].map(([label, value, url]) => {
|
['Lizenz', 'Nur privater, nicht-kommerzieller Gebrauch – Details anzeigen', null, 'license'],
|
||||||
|
].map(([label, value, url, action]) => {
|
||||||
const renderedValue = url
|
const renderedValue = url
|
||||||
? `<a href="#" class="info-meta-link" data-url="${escapeHtml(url)}">${escapeHtml(value)}</a>`
|
? `<a href="#" class="info-meta-link" data-url="${escapeHtml(url)}">${escapeHtml(value)}</a>`
|
||||||
: escapeHtml(value || '–');
|
: action
|
||||||
|
? `<a href="#" class="info-meta-link" data-action="${escapeHtml(action)}">${escapeHtml(value)}</a>`
|
||||||
|
: escapeHtml(value || '–');
|
||||||
return `<div class="settings-kv-row"><span class="settings-kv-label">${escapeHtml(label)}</span><span class="settings-kv-value">${renderedValue}</span></div>`;
|
return `<div class="settings-kv-row"><span class="settings-kv-label">${escapeHtml(label)}</span><span class="settings-kv-value">${renderedValue}</span></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
|
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
|
||||||
link.addEventListener('click', (evt) => {
|
link.addEventListener('click', async (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const url = link.dataset.url;
|
const url = link.dataset.url;
|
||||||
if (url) window.api.openExternal(url);
|
if (url) { window.api.openExternal(url); return; }
|
||||||
|
if (link.dataset.action === 'license') {
|
||||||
|
show('modal-license');
|
||||||
|
el('license-text').textContent = await window.api.appLicense();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
show('modal-settings');
|
show('modal-settings');
|
||||||
@@ -3174,6 +3272,7 @@ async function saveSettings() {
|
|||||||
iso3dIcons: el('s-iso3d').checked,
|
iso3dIcons: el('s-iso3d').checked,
|
||||||
bedrockEnabled: el('s-bedrock').checked,
|
bedrockEnabled: el('s-bedrock').checked,
|
||||||
desktopNotifications: el('s-notify').checked,
|
desktopNotifications: el('s-notify').checked,
|
||||||
|
autostart: el('s-autostart').checked,
|
||||||
autoJava: el('s-auto-java').checked,
|
autoJava: el('s-auto-java').checked,
|
||||||
autoBackupEnabled: el('s-autobackup').checked,
|
autoBackupEnabled: el('s-autobackup').checked,
|
||||||
autoBackupEvery: el('s-autobackup-every').value,
|
autoBackupEvery: el('s-autobackup-every').value,
|
||||||
@@ -3836,6 +3935,47 @@ function wire() {
|
|||||||
el('f-version-type').addEventListener('change', () => fillVersionSelect(el('f-version').value));
|
el('f-version-type').addEventListener('change', () => fillVersionSelect(el('f-version').value));
|
||||||
el('f-loader').addEventListener('change', () => updateLoaderVersions());
|
el('f-loader').addEventListener('change', () => updateLoaderVersions());
|
||||||
el('f-version').addEventListener('change', () => updateLoaderVersions());
|
el('f-version').addEventListener('change', () => updateLoaderVersions());
|
||||||
|
|
||||||
|
el('f-optifine-install').addEventListener('click', async () => {
|
||||||
|
if (!editingId) return;
|
||||||
|
const btn = el('f-optifine-install');
|
||||||
|
const status = el('f-optifine-status');
|
||||||
|
btn.disabled = true;
|
||||||
|
status.textContent = 'Minecraft wird vorbereitet …';
|
||||||
|
try {
|
||||||
|
const res = await window.api.optifineSetup(editingId);
|
||||||
|
if (res.canceled) { status.textContent = ''; return; }
|
||||||
|
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Fehlgeschlagen.'); toast(res.message || 'OptiFine-Installer konnte nicht gestartet werden.', true); return; }
|
||||||
|
status.textContent = `Installer gestartet – Zielordner wurde in die Zwischenablage kopiert: ${res.sharedDir} – `
|
||||||
|
+ 'im OptiFine-Fenster als Minecraft-Ordner einfügen, „Install" klicken, danach hier bestätigen.';
|
||||||
|
el('f-optifine-confirm').classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el('f-optifine-confirm').addEventListener('click', async () => {
|
||||||
|
if (!editingId) return;
|
||||||
|
const btn = el('f-optifine-confirm');
|
||||||
|
const status = el('f-optifine-status');
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await window.api.optifineConfirm(editingId);
|
||||||
|
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Nicht gefunden.'); toast(res.message || 'OptiFine-Version wurde nicht gefunden.', true); return; }
|
||||||
|
toast('OptiFine ' + res.versionId + ' übernommen.');
|
||||||
|
status.textContent = '✔ Übernommen: ' + res.versionId;
|
||||||
|
const sel = el('f-loader-version');
|
||||||
|
if (![...sel.options].some((o) => o.value === res.versionId)) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = res.versionId; o.textContent = res.versionId;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
sel.value = res.versionId;
|
||||||
|
btn.classList.add('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
el('f-pick-image').addEventListener('click', async () => {
|
el('f-pick-image').addEventListener('click', async () => {
|
||||||
const p = await window.api.pickImage();
|
const p = await window.api.pickImage();
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
@@ -3855,7 +3995,16 @@ function wire() {
|
|||||||
el('p-offline').addEventListener('click', () => currentDetailId && launch(currentDetailId));
|
el('p-offline').addEventListener('click', () => currentDetailId && launch(currentDetailId));
|
||||||
el('p-shortcut').addEventListener('click', async () => {
|
el('p-shortcut').addEventListener('click', async () => {
|
||||||
if (!currentDetailId) return;
|
if (!currentDetailId) return;
|
||||||
const res = await window.api.createShortcut(currentDetailId);
|
const inst = await window.api.getInstance(currentDetailId);
|
||||||
|
const eingabe = await askPrompt({
|
||||||
|
title: 'Verknüpfung erstellen',
|
||||||
|
text: 'Name der Desktop-Verknüpfung (ohne .lnk):',
|
||||||
|
label: 'Name',
|
||||||
|
value: inst ? inst.name : '',
|
||||||
|
okLabel: 'Erstellen',
|
||||||
|
});
|
||||||
|
if (eingabe == null) return; // abgebrochen
|
||||||
|
const res = await window.api.createShortcut(currentDetailId, eingabe.trim());
|
||||||
if (res.ok) toast('Verknüpfung auf dem Desktop erstellt.');
|
if (res.ok) toast('Verknüpfung auf dem Desktop erstellt.');
|
||||||
else toast(res.message || 'Verknüpfung fehlgeschlagen.', true);
|
else toast(res.message || 'Verknüpfung fehlgeschlagen.', true);
|
||||||
});
|
});
|
||||||
@@ -4252,10 +4401,16 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Spiel gestartet oder beendet -> "spielt gerade" in den Kontenlisten nachziehen
|
// Spiel gestartet oder beendet -> "spielt gerade" in den Kontenlisten nachziehen
|
||||||
window.api.onAccountsChanged(() => { refreshAllAccountLists(); });
|
window.api.onAccountsChanged(() => { refreshAllAccountLists(); });
|
||||||
// über eine Desktop-Verknüpfung gestartet -> Instanz auswählen und anwerfen
|
// über eine Desktop-Verknüpfung gestartet -> Instanz auswählen und anwerfen
|
||||||
window.api.onLaunchRequest(async (id) => {
|
// (bei einer Server-Verknüpfung zusätzlich direkt mit dem Server verbinden)
|
||||||
|
window.api.onLaunchRequest(async (payload) => {
|
||||||
|
const id = payload && typeof payload === 'object' ? payload.id : payload;
|
||||||
|
const serverIp = payload && typeof payload === 'object' ? payload.serverIp : null;
|
||||||
await selectInstance(id);
|
await selectInstance(id);
|
||||||
launch(id);
|
launch(id, null, serverIp ? { quickPlayServer: serverIp } : null);
|
||||||
});
|
});
|
||||||
|
// Empfänger steht jetzt – dem Hauptprozess Bescheid geben, damit ein über
|
||||||
|
// eine Verknüpfung mitgegebener Startwunsch sicher zugestellt werden kann
|
||||||
|
window.api.notifyReady();
|
||||||
window.api.onUpdateAvailable(showUpdateDialog);
|
window.api.onUpdateAvailable(showUpdateDialog);
|
||||||
window.api.onUpdateProgress(onUpdateProgress);
|
window.api.onUpdateProgress(onUpdateProgress);
|
||||||
window.api.onUpdateChecked(renderUpdateStatus);
|
window.api.onUpdateChecked(renderUpdateStatus);
|
||||||
|
|||||||
+17
-2
@@ -236,6 +236,7 @@ const INHALTSARTEN = {
|
|||||||
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
||||||
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
||||||
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
||||||
|
datapack: { projectType: 'datapack', ordner: 'datapacks', label: 'Datenpakete', loaderNoetig: false },
|
||||||
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -515,7 +516,9 @@ function forgeCdnUrl(fileId, fileName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
||||||
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552 };
|
// classId je Inhaltsart bei gameId 432 (Minecraft). modpack=4471 ist die auf
|
||||||
|
// CurseForge selbst verwendete Kategorie-ID (siehe curseforge.com/minecraft/modpacks).
|
||||||
|
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552, modpack: 4471 };
|
||||||
|
|
||||||
function cfLoaderType(loader) {
|
function cfLoaderType(loader) {
|
||||||
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
||||||
@@ -598,7 +601,12 @@ async function curseforgeSearch(zugang, {
|
|||||||
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const classId = CF_CLASS[art] || CF_CLASS.mod;
|
if (!CF_CLASS[art]) {
|
||||||
|
// Kein zugeordneter CurseForge-Klassencode für diese Inhaltsart -> lieber gar
|
||||||
|
// keine Treffer als falsch einsortierte (z. B. Mods unter "Datenpakete").
|
||||||
|
return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' };
|
||||||
|
}
|
||||||
|
const classId = CF_CLASS[art];
|
||||||
const url = new URL(CURSEFORGE + '/mods/search');
|
const url = new URL(CURSEFORGE + '/mods/search');
|
||||||
url.searchParams.set('gameId', '432');
|
url.searchParams.set('gameId', '432');
|
||||||
url.searchParams.set('classId', String(classId));
|
url.searchParams.set('classId', String(classId));
|
||||||
@@ -713,6 +721,12 @@ async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beste Modpack-Datei eines CurseForge-Projekts auflösen (für den Katalog-Import,
|
||||||
|
// analog zu modrinthResolveModpack).
|
||||||
|
async function curseforgeResolveModpackFile(zugang, modId, gameVersion) {
|
||||||
|
return curseforgePickFile(zugang, modId, gameVersion, '', 'modpack');
|
||||||
|
}
|
||||||
|
|
||||||
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
||||||
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
||||||
const rawId = String(projectId || '').replace(/^cf-/, '');
|
const rawId = String(projectId || '').replace(/^cf-/, '');
|
||||||
@@ -814,4 +828,5 @@ module.exports = {
|
|||||||
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
||||||
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
||||||
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
||||||
|
curseforgeResolveModpackFile,
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-2
@@ -76,6 +76,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
||||||
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
|
bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert)
|
||||||
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
||||||
|
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
||||||
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID
|
||||||
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in %
|
||||||
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)
|
||||||
@@ -133,8 +134,12 @@ function gameDir(id) {
|
|||||||
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
||||||
* weiterlaufen.
|
* weiterlaufen.
|
||||||
*/
|
*/
|
||||||
const INHALT_ORDNER = { mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks' };
|
const INHALT_ORDNER = {
|
||||||
const INHALT_INDEX = { mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json' };
|
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
||||||
|
};
|
||||||
|
const INHALT_INDEX = {
|
||||||
|
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
||||||
|
};
|
||||||
|
|
||||||
function inhaltsDir(id, art = 'mod') {
|
function inhaltsDir(id, art = 'mod') {
|
||||||
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
||||||
|
|||||||
+11
-2
@@ -230,8 +230,17 @@ async function downloadAndInstall(asset, onProgress) {
|
|||||||
|
|
||||||
const dir = updateDir();
|
const dir = updateDir();
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
||||||
fs.rmSync(dest, { force: true });
|
try {
|
||||||
|
fs.rmSync(dest, { force: true });
|
||||||
|
} catch {
|
||||||
|
// Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
|
||||||
|
// Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
|
||||||
|
// EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
|
||||||
|
// cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
|
||||||
|
const parsed = path.parse(dest);
|
||||||
|
dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
||||||
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
||||||
|
|||||||
Reference in New Issue
Block a user