Upload via GUI (43 Dateien)
This commit is contained in:
+284
-28
@@ -6,7 +6,7 @@
|
||||
* (Vanilla-Start, Microsoft-Login und Mod-Loader folgen in späteren Phasen.)
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@@ -25,10 +25,13 @@ const backup = require('./backup');
|
||||
const mmcimport = require('./mmcimport');
|
||||
const cfimport = require('./cfimport');
|
||||
const mcimport = require('./mcimport');
|
||||
const technicimport = require('./technicimport');
|
||||
const technicdiscover = require('./technicdiscover');
|
||||
const javadl = require('./javadl');
|
||||
const crashreports = require('./crashreports');
|
||||
const skins = require('./skins');
|
||||
const servers = require('./servers');
|
||||
const mcping = require('./mcping');
|
||||
|
||||
// undici als globales fetch verwenden -> ermöglicht Proxy via setGlobalDispatcher
|
||||
const undici = require('undici');
|
||||
@@ -217,10 +220,18 @@ app.whenReady().then(() => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const info = await updater.checkForUpdate(app.getVersion());
|
||||
if (info.available && mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('update:available', info);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
// Ergebnis immer melden, damit die Info-Seite auch "aktuell" anzeigen
|
||||
// kann – nicht erst wenn der Nutzer manuell auf "Suchen" klickt
|
||||
mainWindow.webContents.send('update:checked', info);
|
||||
if (info.available) mainWindow.webContents.send('update:available', info);
|
||||
}
|
||||
} catch { /* offline o. Ä. – still ignorieren */ }
|
||||
} catch (err) {
|
||||
// offline o. Ä. – kein Absturz, aber die Info-Seite darf den Fehler zeigen
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('update:checked', { error: (err && err.message) || String(err) });
|
||||
}
|
||||
}
|
||||
}, 4000);
|
||||
|
||||
// Anmelde-Dienste automatisch überwachen (sofort + alle 2 Minuten)
|
||||
@@ -348,6 +359,10 @@ ipcMain.handle('mods:install', async (_e, id, project, art) => {
|
||||
if (!isDep && !firstError) firstError = res.message;
|
||||
return;
|
||||
}
|
||||
// merken, welche Pflicht-Abhängigkeiten dieser Mod mitbringt – damit
|
||||
// wir beim Entfernen warnen können, wenn ein anderer Mod ihn braucht
|
||||
res.entry.requiredDeps = (res.dependencies || [])
|
||||
.filter((d) => d.required && d.projectId).map((d) => d.projectId);
|
||||
store.addMod(id, res.entry, art || 'mod');
|
||||
added.push(res.entry);
|
||||
for (const dep of res.dependencies || []) {
|
||||
@@ -363,6 +378,8 @@ ipcMain.handle('mods:install', async (_e, id, project, art) => {
|
||||
if (!isDep && !firstError) firstError = res.message;
|
||||
return;
|
||||
}
|
||||
res.entry.requiredDeps = (res.dependencies || [])
|
||||
.filter((d) => d.dependency_type === 'required' && d.project_id).map((d) => d.project_id);
|
||||
store.addMod(id, res.entry, art || 'mod');
|
||||
added.push(res.entry);
|
||||
for (const dep of res.dependencies || []) {
|
||||
@@ -382,6 +399,52 @@ ipcMain.handle('mods:install', async (_e, id, project, art) => {
|
||||
return { ok: true, entry: added[0], added, depCount: added.length - 1 };
|
||||
});
|
||||
|
||||
/*
|
||||
* Lokale Datei(en) direkt in den Inhaltsordner kopieren – für Mods, die
|
||||
* nicht über Modrinth/CurseForge zu finden sind (Dev-Build, privater Mod,
|
||||
* per Hand zugeschickt). Die Datei taucht danach ganz normal in der
|
||||
* "Installiert"-Liste auf, weil listMods() den Ordner ohnehin mitscannt.
|
||||
*/
|
||||
ipcMain.handle('mods:addFile', async (_e, id, art) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const typ = services.inhaltsArt(art || 'mod');
|
||||
const filters = (art || 'mod') === 'mod'
|
||||
? [{ name: 'Mod-Datei', extensions: ['jar', 'zip'] }]
|
||||
: [{ name: typ.label || 'Datei', extensions: ['zip'] }];
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Datei hinzufügen',
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: filters.concat([{ name: 'Alle Dateien', extensions: ['*'] }]),
|
||||
});
|
||||
if (res.canceled || !res.filePaths.length) return { ok: false, canceled: true };
|
||||
|
||||
const dir = store.inhaltsDir(id, art || 'mod');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const added = [];
|
||||
const failed = [];
|
||||
for (const src of res.filePaths) {
|
||||
try {
|
||||
if (!/\.(jar|zip)$/i.test(src)) {
|
||||
failed.push({ name: path.basename(src), message: 'Nur .jar/.zip-Dateien werden unterstützt.' });
|
||||
continue;
|
||||
}
|
||||
const name = path.basename(src);
|
||||
const ext = path.extname(name);
|
||||
const base = name.slice(0, -ext.length);
|
||||
let dest = path.join(dir, name);
|
||||
// Namenskollision -> durchnummerieren statt eine vorhandene Datei zu überschreiben
|
||||
let n = 2;
|
||||
while (fs.existsSync(dest)) { dest = path.join(dir, `${base} (${n})${ext}`); n++; }
|
||||
fs.copyFileSync(src, dest);
|
||||
added.push(path.basename(dest));
|
||||
} catch (err) {
|
||||
failed.push({ name: path.basename(src), message: err.message });
|
||||
}
|
||||
}
|
||||
return { ok: true, added, failed };
|
||||
});
|
||||
|
||||
// verfügbare Loader-Versionen (Fabric/Quilt) für eine MC-Version
|
||||
ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
|
||||
try { return await loaders.listLoaderVersions(loader, mcVersion); }
|
||||
@@ -389,6 +452,8 @@ ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
|
||||
});
|
||||
|
||||
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
|
||||
ipcMain.handle('mods:dependents', (_e, id, filename, art) => store.findDependents(id, filename, art || 'mod'));
|
||||
|
||||
// Vorhandener Arbeitsspeicher – Grundlage für die Prüfung der RAM-Angaben
|
||||
function gesamtSpeicherMb() {
|
||||
@@ -432,6 +497,18 @@ ipcMain.handle('servers:rename', (_e, id, ip, neuerName) => {
|
||||
return servers.benenneUm(store.gameDir(id), ip, neuerName);
|
||||
});
|
||||
|
||||
// Live-Status eines Servers abfragen (MOTD/Spielerzahl/Version) – wie der
|
||||
// Vanilla-Mehrspieler-Bildschirm, ohne sich wirklich zu verbinden
|
||||
ipcMain.handle('servers:ping', async (_e, ip) => {
|
||||
const s = String(ip || '').trim();
|
||||
const m = s.match(/^([^:]+)(?::(\d+))?$/);
|
||||
if (!m) return { ok: false, message: 'Ungültige Adresse.' };
|
||||
const host = m[1];
|
||||
const port = m[2] ? Number(m[2]) : 25565;
|
||||
try { return await mcping.pingServer(host, port); }
|
||||
catch (err) { return { ok: false, message: err.message }; }
|
||||
});
|
||||
|
||||
// denselben Server in weitere Instanzen übertragen
|
||||
ipcMain.handle('servers:copyTo', (_e, zielIds, eintrag) => {
|
||||
const erledigt = [];
|
||||
@@ -737,6 +814,36 @@ ipcMain.handle('instances:export', async (_e, id) => {
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Schnellsicherung EINER Welt als ZIP – unabhängig vom globalen
|
||||
* Backup-System, landet in einem eigenen Unterordner der Dokumente.
|
||||
*/
|
||||
ipcMain.handle('world:backup', async (_e, id, worldName) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const worldDir = path.join(store.gameDir(id), 'saves', String(worldName || ''));
|
||||
if (path.dirname(worldDir) !== path.join(store.gameDir(id), 'saves') || !fs.existsSync(worldDir)) {
|
||||
return { ok: false, message: 'Welt nicht gefunden.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const zielOrdner = path.join(app.getPath('documents'), 'AeroMC Launcher', 'Welten-Sicherungen',
|
||||
(inst.name || 'Instanz').replace(/[^\w.-]+/g, '_'));
|
||||
fs.mkdirSync(zielOrdner, { recursive: true });
|
||||
const zeitstempel = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 16);
|
||||
const safeWorld = String(worldName).replace(/[^\w.-]+/g, '_');
|
||||
const zielDatei = path.join(zielOrdner, `${safeWorld}_${zeitstempel}.zip`);
|
||||
if (fs.existsSync(zielDatei)) fs.unlinkSync(zielDatei);
|
||||
|
||||
await runPwsh("Compress-Archive -Path (Join-Path $env:VN_SRC '*') -DestinationPath $env:VN_DST -Force",
|
||||
{ VN_SRC: worldDir, VN_DST: zielDatei });
|
||||
notify('Welt gesichert', `${worldName} (${inst.name}) wurde gesichert.`);
|
||||
return { ok: true, path: zielDatei };
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Sicherung fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Modpack-Import (Modrinth .mrpack) ----
|
||||
|
||||
async function importMrpack(buf, zip) {
|
||||
@@ -947,6 +1054,7 @@ ipcMain.handle('modpack:update', async (_e, id, versionId) => {
|
||||
}),
|
||||
});
|
||||
|
||||
notify('Modpack aktualisiert', `${inst.name} ist auf Version ${version.version_number} aktualisiert.`);
|
||||
return {
|
||||
ok: true,
|
||||
neueNummer: version.version_number,
|
||||
@@ -1091,12 +1199,47 @@ ipcMain.handle('modpack:installFromModrinth', async (_e, project) => {
|
||||
store.updateInstance(inst.id, { modpack: mp });
|
||||
result.instance = store.getInstance(inst.id);
|
||||
}
|
||||
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
|
||||
* Dateidialog oder per Drag&Drop aufs Fenster kommt.
|
||||
* notModpack: true heißt "kein Modpack-Format erkannt" – der Aufrufer kann
|
||||
* dann noch andere ZIP-Interpretationen probieren (z. B. Instanz-Export).
|
||||
*/
|
||||
async function importModpackFromPath(filePath) {
|
||||
try {
|
||||
const buf = fs.readFileSync(filePath);
|
||||
const zip = forge.readZipIndex(buf);
|
||||
let result = null;
|
||||
if (zip['modrinth.index.json']) {
|
||||
result = await importMrpack(buf, zip);
|
||||
} else if (zip['manifest.json']) {
|
||||
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.' };
|
||||
}
|
||||
result = await importCurseForge(buf, zip, zugang);
|
||||
} else {
|
||||
return { ok: false, notModpack: true, message: 'Unbekanntes Modpack-Format (keine modrinth.index.json gefunden).' };
|
||||
}
|
||||
if (result && result.ok && result.instance) {
|
||||
notify('Modpack installiert', `${result.instance.name} ist einsatzbereit.`);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
return { ok: false, notModpack: true, message: 'Modpack-Import fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('modpack:import', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Modpack importieren',
|
||||
@@ -1104,22 +1247,7 @@ ipcMain.handle('modpack:import', async () => {
|
||||
filters: [{ name: 'Modpacks', extensions: ['mrpack', 'zip'] }],
|
||||
});
|
||||
if (res.canceled) return { ok: false, canceled: true };
|
||||
try {
|
||||
const buf = fs.readFileSync(res.filePaths[0]);
|
||||
const zip = forge.readZipIndex(buf);
|
||||
if (zip['modrinth.index.json']) return await importMrpack(buf, zip);
|
||||
if (zip['manifest.json']) {
|
||||
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.' };
|
||||
}
|
||||
return await importCurseForge(buf, zip, zugang);
|
||||
}
|
||||
return { ok: false, message: 'Unbekanntes Modpack-Format (keine modrinth.index.json gefunden).' };
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Modpack-Import fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
return importModpackFromPath(res.filePaths[0]);
|
||||
});
|
||||
|
||||
// Verknüpfung auf dem Desktop, die genau diese Instanz startet
|
||||
@@ -1150,18 +1278,17 @@ ipcMain.handle('instances:shortcut', (_e, id) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('instances:import', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Instanz importieren',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'ZIP-Archiv', extensions: ['zip'] }],
|
||||
});
|
||||
if (res.canceled) return { ok: false, canceled: true };
|
||||
/*
|
||||
* Kern des ZIP-Instanz-Imports (AeroMC-Export oder einzelne MultiMC/Prism-
|
||||
* Instanz), unabhängig davon ob der Pfad aus einem Dateidialog oder per
|
||||
* Drag&Drop aufs Fenster kommt.
|
||||
*/
|
||||
async function importInstanceZipFromPath(filePath) {
|
||||
const tmp = path.join(app.getPath('temp'), 'vn-import-' + Date.now());
|
||||
try {
|
||||
fs.mkdirSync(tmp, { recursive: true });
|
||||
await runPwsh('Expand-Archive -Path $env:VN_SRC -DestinationPath $env:VN_DST -Force',
|
||||
{ VN_SRC: res.filePaths[0], VN_DST: tmp });
|
||||
{ VN_SRC: filePath, VN_DST: tmp });
|
||||
|
||||
// instance.json finden (auch in Unterordner)
|
||||
let base = tmp;
|
||||
@@ -1203,8 +1330,88 @@ ipcMain.handle('instances:import', async () => {
|
||||
} finally {
|
||||
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('instances:import', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Instanz importieren',
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'ZIP-Archiv', extensions: ['zip'] }],
|
||||
});
|
||||
if (res.canceled) return { ok: false, canceled: true };
|
||||
return importInstanceZipFromPath(res.filePaths[0]);
|
||||
});
|
||||
|
||||
/*
|
||||
* Datei per Drag&Drop aufs Fenster gezogen. Anhand der Endung/des Inhalts
|
||||
* wird entschieden, was damit passieren soll:
|
||||
* .jar -> als Mod/Ressourcenpaket/Shader in die übergebene
|
||||
* Instanz kopieren (targetId/targetArt vom Renderer,
|
||||
* je nachdem was gerade offen bzw. ausgewählt ist)
|
||||
* .mrpack / .zip -> zuerst als Modpack versuchen (Modrinth/CurseForge),
|
||||
* sonst als AeroMC-/MultiMC-Instanz-Export
|
||||
*/
|
||||
ipcMain.handle('files:drop', async (_e, filePath, targetId, targetArt) => {
|
||||
if (!filePath || !fs.existsSync(filePath)) return { ok: false, message: 'Datei nicht gefunden.' };
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (ext === '.jar') {
|
||||
if (!targetId) {
|
||||
return { ok: false, message: 'Bitte zuerst eine Instanz öffnen (Mods-Dialog), dann die Datei dort hineinziehen.' };
|
||||
}
|
||||
const inst = store.getInstance(targetId);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const art = targetArt || 'mod';
|
||||
const dir = store.inhaltsDir(targetId, art);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
try {
|
||||
const name = path.basename(filePath);
|
||||
const ext2 = path.extname(name);
|
||||
const base = name.slice(0, -ext2.length);
|
||||
let dest = path.join(dir, name);
|
||||
let n = 2;
|
||||
while (fs.existsSync(dest)) { dest = path.join(dir, `${base} (${n})${ext2}`); n++; }
|
||||
fs.copyFileSync(filePath, dest);
|
||||
return { ok: true, kind: 'mod', added: [path.basename(dest)] };
|
||||
} catch (err) {
|
||||
return { ok: false, message: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
if (ext === '.mrpack' || ext === '.zip') {
|
||||
const modpack = await importModpackFromPath(filePath);
|
||||
// .mrpack ist immer ein Modpack – bei Fehlschlag direkt die Meldung
|
||||
// zeigen, statt es zusätzlich noch als Instanz-Export zu probieren
|
||||
if (!modpack.notModpack || ext === '.mrpack') return Object.assign({ kind: 'modpack' }, modpack);
|
||||
// .zip ohne erkanntes Modpack-Format -> als Instanz-Export/-Import versuchen
|
||||
const instanceRes = await importInstanceZipFromPath(filePath);
|
||||
return Object.assign({ kind: 'instance' }, instanceRes);
|
||||
}
|
||||
|
||||
return { ok: false, message: 'Nicht unterstützter Dateityp: ' + (ext || '(ohne Endung)') };
|
||||
});
|
||||
|
||||
/*
|
||||
* Windows-Benachrichtigung für abgeschlossene Hintergrund-Aktionen (Backup,
|
||||
* größere Downloads) – nur wenn die Einstellung an ist UND das Fenster
|
||||
* gerade nicht im Vordergrund ist (sonst sieht man's eh schon live).
|
||||
*/
|
||||
function notify(title, body) {
|
||||
try {
|
||||
if (store.getSettings().desktopNotifications === false) return;
|
||||
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isFocused()) return;
|
||||
if (!Notification.isSupported()) return;
|
||||
const n = new Notification({ title, body, silent: false });
|
||||
n.on('click', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
n.show();
|
||||
} catch { /* rein informativ – darf nie einen Ablauf stören */ }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sicherungen: von Hand und nach Zeitplan
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1232,6 +1439,7 @@ async function erstelleBackup({ still } = {}) {
|
||||
if (!still && mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('backup:progress', { mode: 'create', percent: 100, detail: 'Backup abgeschlossen' });
|
||||
}
|
||||
if (result && result.ok) notify('Backup abgeschlossen', 'Die Sicherung aller Instanzen ist fertig.');
|
||||
return result;
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Backup fehlgeschlagen: ' + err.message };
|
||||
@@ -1590,6 +1798,54 @@ ipcMain.handle('mc:import', (_e, entries, copyData) => {
|
||||
return { ok: true, imported: done, failed };
|
||||
});
|
||||
|
||||
ipcMain.handle('technic:pickFolder', async () => {
|
||||
const res = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Technic-Ordner wählen (oder direkt den modpacks-Ordner)',
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
return res.canceled ? null : res.filePaths[0];
|
||||
});
|
||||
|
||||
ipcMain.handle('technic:scan', (_e, folder) => {
|
||||
try { return technicimport.scan(folder); }
|
||||
catch (err) { return { ok: false, reason: err.message }; }
|
||||
});
|
||||
|
||||
ipcMain.handle('technic:import', (_e, entries, copyData) => {
|
||||
const done = [];
|
||||
const failed = [];
|
||||
for (const entry of entries || []) {
|
||||
try {
|
||||
const imported = technicimport.importOne(store, entry, copyData !== false);
|
||||
done.push(imported);
|
||||
}
|
||||
catch (err) { failed.push({ name: entry.name, message: err.message }); }
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('technic:progress', { current: done.length + failed.length, total: entries.length });
|
||||
}
|
||||
}
|
||||
return { ok: true, imported: done, failed };
|
||||
});
|
||||
|
||||
// ---- Technic-Plattform: Modpack-Katalog durchsuchen und direkt installieren ----
|
||||
|
||||
ipcMain.handle('technicPlatform:search', async (_e, query) => {
|
||||
try { return { ok: true, hits: await technicdiscover.search(query) }; }
|
||||
catch (err) { return { ok: false, message: err.message }; }
|
||||
});
|
||||
|
||||
ipcMain.handle('technicPlatform:install', async (_e, slug) => {
|
||||
try {
|
||||
const res = await technicdiscover.installFromPlatform(store, slug, (p) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('technicPlatform:progress', p);
|
||||
});
|
||||
if (res.ok && res.instance) notify('Modpack installiert', `${res.instance.name} ist einsatzbereit.`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
return { ok: false, message: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Minecraft Bedrock (Windows-Store-App)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user