Upload via GUI (93 Dateien)
This commit is contained in:
@@ -0,0 +1,218 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* crashhelp.js – Absturz-Assistent
|
||||||
|
* ---------------------------------
|
||||||
|
* Liest Absturzbericht, latest.log und die letzte Konsolenausgabe eines Starts
|
||||||
|
* und erkennt die häufigsten Ursachen. Jeder Fund bekommt eine verständliche
|
||||||
|
* Erklärung und – wo möglich – eine Aktion, die der Launcher mit einem Klick
|
||||||
|
* ausführen kann:
|
||||||
|
*
|
||||||
|
* { typ: 'mod-installieren', projekt, name } fehlende Abhängigkeit von Modrinth holen
|
||||||
|
* { typ: 'mod-deaktivieren', dateiname, name } Mod abschalten (umbenennen, nichts gelöscht)
|
||||||
|
* { typ: 'ram', mb } Arbeitsspeicher der Instanz ändern
|
||||||
|
* { typ: 'java-auto' } passendes Java automatisch wählen lassen
|
||||||
|
* { typ: 'mods-ordner' } Mods-Ordner öffnen
|
||||||
|
*
|
||||||
|
* Bewusst nur Muster, die eindeutig sind – lieber keinen Fund als einen falschen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mod-IDs, die keine installierbaren Mods sind
|
||||||
|
const KEINE_MODS = new Set(['minecraft', 'java', 'fabricloader', 'fabric-loader', 'forge', 'neoforge', 'quilt_loader', 'quilt-loader', 'fml', 'mixinextras']);
|
||||||
|
|
||||||
|
// Mod-ID -> Modrinth-Projekt, wo sich beides unterscheidet
|
||||||
|
const PROJEKT_FUER_ID = {
|
||||||
|
fabric: 'fabric-api',
|
||||||
|
'fabric-api-base': 'fabric-api',
|
||||||
|
quilted_fabric_api: 'qsl',
|
||||||
|
cloth_config: 'cloth-config',
|
||||||
|
'cloth-config2': 'cloth-config',
|
||||||
|
architectury: 'architectury-api',
|
||||||
|
geckolib3: 'geckolib',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Java-Hauptversion aus der "class file version" (52 = Java 8, 61 = 17, 65 = 21 …)
|
||||||
|
const javaAusKlassenversion = (v) => Number(v) - 44;
|
||||||
|
|
||||||
|
function projektFuer(modId) {
|
||||||
|
const id = String(modId || '').toLowerCase();
|
||||||
|
return PROJEKT_FUER_ID[id] || id.replace(/_/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
// installierte Mod zu einer ID/einem Namen finden (für "abschalten")
|
||||||
|
function findeMod(mods, modId, name) {
|
||||||
|
const id = String(modId || '').toLowerCase();
|
||||||
|
const kurz = id.replace(/[-_]/g, '');
|
||||||
|
const n = String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
return (mods || []).find((m) => {
|
||||||
|
if (m.enabled === false) return false;
|
||||||
|
const datei = String(m.filename || '').toLowerCase();
|
||||||
|
const titel = String(m.title || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
return (id && (datei.startsWith(id) || datei.replace(/[-_]/g, '').startsWith(kurz)))
|
||||||
|
|| (n && titel && (titel === n || datei.replace(/[^a-z0-9]/g, '').startsWith(n)));
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* text: zusammengeführter Text aus Bericht, Log und Konsole
|
||||||
|
* ctx: { loader, version, maxMemMb, gesamtMb, mods: [{ filename, title, projectId, enabled }] }
|
||||||
|
* -> [{ schluessel, titel, text, aktion? }]
|
||||||
|
*/
|
||||||
|
function analysiere(text, ctx = {}) {
|
||||||
|
const t = String(text || '');
|
||||||
|
const funde = [];
|
||||||
|
const schonDa = new Set();
|
||||||
|
const fund = (schluessel, titel, erklaerung, aktion) => {
|
||||||
|
if (schonDa.has(schluessel)) return;
|
||||||
|
schonDa.add(schluessel);
|
||||||
|
funde.push({ schluessel, titel, text: erklaerung, aktion: aktion || null });
|
||||||
|
};
|
||||||
|
const mods = ctx.mods || [];
|
||||||
|
|
||||||
|
// --- Fehlende Abhängigkeiten (Fabric/Quilt) ---
|
||||||
|
// "Mod 'Mod Menu' (modmenu) 7.2.2 requires any version of fabric-api, which is missing!"
|
||||||
|
// neuere Fassung: "... requires any version of mod 'Fabric API' (fabric-api), which is missing!"
|
||||||
|
for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version ([^\n,]+?)|any version) of (?:mod '([^']+)' \(([\w.-]+)\)|([\w.-]+)), which is missing/gi)) {
|
||||||
|
const [, name, , version, fehltName, fehltIdNeu, fehltIdAlt] = m;
|
||||||
|
const fehlt = fehltIdNeu || fehltIdAlt;
|
||||||
|
if (KEINE_MODS.has(fehlt.toLowerCase())) continue;
|
||||||
|
const anzeige = fehltName || fehlt;
|
||||||
|
fund(`fehlt:${fehlt}`, `„${name}“ braucht ${anzeige}`,
|
||||||
|
`Der Mod „${name}“ funktioniert nur zusammen mit „${anzeige}“${version ? ` (Version ${version.trim()})` : ''} – und der fehlt in dieser Instanz.`,
|
||||||
|
{ typ: 'mod-installieren', projekt: projektFuer(fehlt), name: anzeige });
|
||||||
|
}
|
||||||
|
// Lösungsvorschlag von Fabric: "- Install fabric-api, any version."
|
||||||
|
for (const m of t.matchAll(/^\s*-\s*Install ([\w.-]+), (?:any version|version [^\n.]+)/gim)) {
|
||||||
|
const fehlt = m[1];
|
||||||
|
if (KEINE_MODS.has(fehlt.toLowerCase())) continue;
|
||||||
|
fund(`fehlt:${fehlt}`, `Es fehlt „${fehlt}“`,
|
||||||
|
`Ein installierter Mod braucht „${fehlt}“, der in dieser Instanz nicht installiert ist.`,
|
||||||
|
{ typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fehlende Abhängigkeiten (Forge/NeoForge) ---
|
||||||
|
// "Mod ID: 'architectury', Requested by: 'rei', Expected range: '[9.1.12,)', Actual version: '[MISSING]'"
|
||||||
|
for (const m of t.matchAll(/Mod ID: '([\w.-]+)', Requested by: '([\w.-]+)', Expected range: '([^']*)', Actual version: '([^']*)'/gi)) {
|
||||||
|
const [, fehlt, von, bereich, ist] = m;
|
||||||
|
if (fehlt.toLowerCase() === 'minecraft') {
|
||||||
|
const mod = findeMod(mods, von);
|
||||||
|
fund(`mcversion:${von}`, `„${von}“ passt nicht zu dieser Minecraft-Version`,
|
||||||
|
`Der Mod „${von}“ ist für Minecraft ${bereich} gemacht, deine Instanz hat ${ctx.version || ist}.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || von } : null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (KEINE_MODS.has(fehlt.toLowerCase())) continue;
|
||||||
|
if (/MISSING/i.test(ist)) {
|
||||||
|
fund(`fehlt:${fehlt}`, `„${von}“ braucht ${fehlt}`,
|
||||||
|
`Der Mod „${von}“ funktioniert nur zusammen mit „${fehlt}“ – und der fehlt in dieser Instanz.`,
|
||||||
|
{ typ: 'mod-installieren', projekt: projektFuer(fehlt), name: fehlt });
|
||||||
|
} else {
|
||||||
|
fund(`alt:${fehlt}`, `„${fehlt}“ ist zu alt oder zu neu`,
|
||||||
|
`„${von}“ braucht „${fehlt}“ in der Version ${bereich}, installiert ist ${ist}. Aktualisiere „${fehlt}“ im Inhalts-Dialog („Nach Updates suchen“).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mod für eine andere Minecraft-Version (Fabric) ---
|
||||||
|
// "Mod 'X' (x) 1.0 requires version 1.20.4 of minecraft, but only the wrong version is present: 1.21!"
|
||||||
|
for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? requires (?:version |any version of )?([^\n]*?) of (?:mod )?'?minecraft'?/gi)) {
|
||||||
|
const [, name, id, fuer] = m;
|
||||||
|
const mod = findeMod(mods, id, name);
|
||||||
|
fund(`mcversion:${id}`, `„${name}“ passt nicht zu dieser Minecraft-Version`,
|
||||||
|
`„${name}“ ist für Minecraft ${fuer.replace(/^version\s+/i, '').trim() || 'eine andere Version'} gemacht, deine Instanz hat ${ctx.version || 'eine andere'}. Abschalten oder eine passende Version des Mods installieren.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null);
|
||||||
|
}
|
||||||
|
// Lösungsvorschlag: "- Remove mod 'X' (x)."
|
||||||
|
for (const m of t.matchAll(/^\s*-\s*Remove mod '([^']+)' \(([\w.-]+)\)/gim)) {
|
||||||
|
const [, name, id] = m;
|
||||||
|
const mod = findeMod(mods, id, name);
|
||||||
|
fund(`entfernen:${id}`, `„${name}“ verursacht den Fehler`,
|
||||||
|
`Der Mod-Loader schlägt vor, „${name}“ zu entfernen – er verträgt sich nicht mit den anderen Mods oder dieser Version.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name } : null);
|
||||||
|
}
|
||||||
|
// "Mod 'A' (a) is incompatible with any version of mod 'B' (b)"
|
||||||
|
for (const m of t.matchAll(/Mod '([^']+)' \(([\w.-]+)\)[^\n]*? is incompatible with [^\n]*?mod '([^']+)' \(([\w.-]+)\)/gi)) {
|
||||||
|
const [, a, idA, b] = m;
|
||||||
|
const mod = findeMod(mods, idA, a);
|
||||||
|
fund(`inkompatibel:${idA}`, `„${a}“ und „${b}“ vertragen sich nicht`,
|
||||||
|
`Diese beiden Mods können nicht zusammen installiert sein. Schalte einen der beiden ab.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: a } : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Doppelte Mods ---
|
||||||
|
if (/Duplicate mods found|Found duplicate mods|is provided by multiple mods|Mod ID ['"]?[\w.-]+['"]? is (?:provided|present) multiple times/i.test(t)) {
|
||||||
|
fund('doppelt', 'Ein Mod ist doppelt installiert',
|
||||||
|
'Mindestens ein Mod liegt zweimal im Mods-Ordner (z. B. zwei Versionen desselben Mods). Lösche die ältere Datei.',
|
||||||
|
{ typ: 'mods-ordner' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Falsche Java-Version ---
|
||||||
|
const klasse = t.match(/UnsupportedClassVersionError[\s\S]{0,300}?class file version (\d+)\.\d+/i);
|
||||||
|
const javaPflicht = t.match(/requires (?:at least )?Java (\d+)|Java (\d+) (?:or (?:newer|later|above)) is required|Unsupported Java detected \((\d+)/i);
|
||||||
|
if (klasse || javaPflicht) {
|
||||||
|
const noetig = klasse ? javaAusKlassenversion(klasse[1]) : Number(javaPflicht[1] || javaPflicht[2] || 0);
|
||||||
|
fund('java', noetig ? `Diese Instanz braucht Java ${noetig}` : 'Falsche Java-Version',
|
||||||
|
`Minecraft oder ein Mod wurde für ${noetig ? `Java ${noetig}` : 'eine andere Java-Version'} gebaut, gestartet wurde mit einer älteren. AeroMC kann das passende Java automatisch wählen und bei Bedarf herunterladen.`,
|
||||||
|
{ typ: 'java-auto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Zu wenig Arbeitsspeicher ---
|
||||||
|
if (/java\.lang\.OutOfMemoryError|There is insufficient memory for the Java Runtime/i.test(t)) {
|
||||||
|
const jetzt = Number(ctx.maxMemMb) || 2048;
|
||||||
|
const grenze = Math.max(2048, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.6));
|
||||||
|
const neu = Math.min(grenze, jetzt + 2048);
|
||||||
|
fund('ram', 'Minecraft hatte zu wenig Arbeitsspeicher',
|
||||||
|
`Der Instanz waren ${Math.round(jetzt / 102.4) / 10} GB zugewiesen – zu wenig für diese Mods oder Welt.`
|
||||||
|
+ (neu > jetzt ? ` Empfohlen: ${Math.round(neu / 102.4) / 10} GB.` : ' Mehr lässt dein PC kaum zu – weniger Mods oder eine kleinere Sichtweite helfen.'),
|
||||||
|
neu > jetzt ? { typ: 'ram', mb: neu } : null);
|
||||||
|
}
|
||||||
|
if (/Could not reserve enough space for (?:\d+KB )?object heap|Invalid maximum heap size|Initial heap size set to a larger value than the maximum/i.test(t)) {
|
||||||
|
const jetzt = Number(ctx.maxMemMb) || 4096;
|
||||||
|
const neu = Math.max(2048, Math.min(jetzt - 1024, Math.floor((Number(ctx.gesamtMb) || 8192) * 0.5)));
|
||||||
|
fund('ram-zuviel', 'Zu viel Arbeitsspeicher eingestellt',
|
||||||
|
`Java konnte die eingestellten ${Math.round(jetzt / 102.4) / 10} GB nicht reservieren – entweder ist gerade nicht genug frei oder es läuft ein 32-Bit-Java.`,
|
||||||
|
neu < jetzt ? { typ: 'ram', mb: neu } : { typ: 'java-auto' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mixin-Fehler: ein bestimmter Mod passt nicht ---
|
||||||
|
for (const m of t.matchAll(/Mixin (?:apply|transformation)[^\n]*? (?:for|from) mod ([\w.-]+) failed|from mod ([\w.-]+) failed injection check|mixins?\.json:[^\n]*? from mod ([\w.-]+)/gi)) {
|
||||||
|
const id = m[1] || m[2] || m[3];
|
||||||
|
if (!id || KEINE_MODS.has(id.toLowerCase())) continue;
|
||||||
|
const mod = findeMod(mods, id);
|
||||||
|
fund(`mixin:${id}`, `„${mod ? (mod.title || id) : id}“ passt nicht zu dieser Version`,
|
||||||
|
`Der Mod „${id}“ greift tief in Minecraft ein und ist mit dieser Minecraft-Version oder einem anderen Mod nicht kompatibel. Schalte ihn ab oder installiere eine passende Version.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || id } : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Grafiktreiber ---
|
||||||
|
if (/Pixel format not accelerated|WGL: The driver does not appear to support OpenGL|GLFW error 65542|OpenGL 3\.2 (?:is )?not supported|Couldn't set pixel format|No OpenGL context found/i.test(t)) {
|
||||||
|
fund('treiber', 'Problem mit dem Grafiktreiber',
|
||||||
|
'Minecraft konnte OpenGL nicht starten. Meist hilft es, den Grafiktreiber direkt vom Hersteller (NVIDIA, AMD oder Intel) zu aktualisieren. Bei Laptops: in den Windows-Grafikeinstellungen Java auf „Hohe Leistung“ stellen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bekannte Unverträglichkeiten anhand der installierten Mods ---
|
||||||
|
const aktive = mods.filter((m) => m.enabled !== false);
|
||||||
|
const hat = (re) => aktive.find((m) => re.test(String(m.filename || '') + ' ' + String(m.title || '')));
|
||||||
|
const optifine = hat(/optifine|optifabric/i);
|
||||||
|
const sodium = hat(/sodium|embeddium|rubidium/i);
|
||||||
|
if (optifine && sodium) {
|
||||||
|
fund('optifine-sodium', 'OptiFine und Sodium zusammen installiert',
|
||||||
|
'OptiFine und Sodium (bzw. Embeddium/Rubidium) machen dasselbe und vertragen sich nicht. Schalte OptiFine ab – für Shader gibt es Iris als Ergänzung zu Sodium.',
|
||||||
|
{ typ: 'mod-deaktivieren', dateiname: optifine.filename, name: optifine.title || 'OptiFine' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Forge nennt einen Verdächtigen, aber sonst nichts Eindeutiges ---
|
||||||
|
if (!funde.length) {
|
||||||
|
const verdacht = t.match(/^Suspected Mods?:\s*([^\n]+)$/im);
|
||||||
|
if (verdacht && !/^\s*NONE\s*$/i.test(verdacht[1])) {
|
||||||
|
const name = verdacht[1].split(/[,(]/)[0].trim();
|
||||||
|
const mod = findeMod(mods, '', name);
|
||||||
|
fund('verdacht', `Vermutlich verursacht von „${name}“`,
|
||||||
|
`Laut Absturzbericht steckt wahrscheinlich „${verdacht[1].trim()}“ dahinter. Schalte den Mod testweise ab und starte erneut.`,
|
||||||
|
mod ? { typ: 'mod-deaktivieren', dateiname: mod.filename, name: mod.title || name } : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return funde;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { analysiere, projektFuer };
|
||||||
+135
-13
@@ -4,8 +4,14 @@
|
|||||||
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
* friends.js – Anbindung an das AeroMC-Freundesystem (WordPress-Plugin)
|
||||||
* -----------------------------------------------------------------------------
|
* -----------------------------------------------------------------------------
|
||||||
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
* Schlanke Fetch-Wrapper gegen die REST-Routen des "AeroMC Freunde"-Plugins
|
||||||
* (siehe wordpress-plugin/aeromc-friends/). Keine eigene Anmeldung - jeder
|
* (siehe wordpress-plugin/aeromc-friends/). Jeder Aufruf trägt die
|
||||||
* Aufruf trägt einfach die Minecraft-UUID des aktiven Kontos.
|
* Minecraft-UUID des Kontos und – sobald angemeldet – ein Sitzungs-Token.
|
||||||
|
*
|
||||||
|
* Anmeldung wie bei einem Minecraft-Server, ohne eigenes Passwort: Aufgabe
|
||||||
|
* vom Plugin holen, mit dem Minecraft-Zugang des Kontos bei Mojang "beitreten",
|
||||||
|
* Plugin prüft bei Mojang nach und gibt ein Token aus. Läuft im Hintergrund
|
||||||
|
* vor der ersten Anfrage eines Kontos; kennt das Plugin die Anmeldung noch
|
||||||
|
* nicht (ältere Fassung), geht es ohne Token weiter wie bisher.
|
||||||
*
|
*
|
||||||
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
* Genau wie bei discordpresence.js: jeder Fehler (Plugin nicht installiert,
|
||||||
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
* kein Internet, Seite down) wird abgefangen und nach oben als { ok:false,
|
||||||
@@ -14,19 +20,84 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
const BASIS_URL = 'https://viper-network.de/wp-json/aeromc/v1';
|
||||||
|
const MOJANG_JOIN = 'https://sessionserver.mojang.com/session/minecraft/join';
|
||||||
|
|
||||||
|
// liefert für eine Konto-UUID { accessToken, name } (vom Hauptprozess gesetzt)
|
||||||
|
let zugangsHelfer = null;
|
||||||
|
const sitzungen = new Map(); // uuid -> Token
|
||||||
|
const laufendeAnmeldung = new Map(); // uuid -> Promise (nicht doppelt anmelden)
|
||||||
|
const fehlversuch = new Map(); // uuid -> Zeitpunkt, um nicht ständig neu zu versuchen
|
||||||
|
const NEUVERSUCH_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
function setzeZugangsHelfer(fn) { zugangsHelfer = fn; }
|
||||||
|
|
||||||
|
async function roheAnfrage(pfad, { method = 'GET', body, token } = {}) {
|
||||||
|
const url = method === 'GET' && body
|
||||||
|
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
||||||
|
: `${BASIS_URL}${pfad}`;
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
if (token) headers['X-AeroMC-Token'] = token;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
||||||
|
});
|
||||||
|
const daten = await res.json().catch(() => null);
|
||||||
|
return { res, daten };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mojang-Sitzungsprüfung durchlaufen und ein Token holen; null bei Fehlschlag
|
||||||
|
async function anmelden(uuid) {
|
||||||
|
if (!zugangsHelfer) return null;
|
||||||
|
const letzter = fehlversuch.get(uuid);
|
||||||
|
if (letzter && Date.now() - letzter < NEUVERSUCH_MS) return null;
|
||||||
|
if (laufendeAnmeldung.has(uuid)) return laufendeAnmeldung.get(uuid);
|
||||||
|
const ablauf = (async () => {
|
||||||
|
try {
|
||||||
|
const zugang = await zugangsHelfer(uuid);
|
||||||
|
if (!zugang || !zugang.accessToken || !zugang.name) throw new Error('kein Minecraft-Zugang');
|
||||||
|
const aufgabe = await roheAnfrage('/auth/challenge', { method: 'POST', body: { uuid, username: zugang.name } });
|
||||||
|
if (!aufgabe.res.ok || !aufgabe.daten || !aufgabe.daten.serverId) throw new Error('keine Aufgabe');
|
||||||
|
const join = await fetch(MOJANG_JOIN, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
accessToken: zugang.accessToken,
|
||||||
|
selectedProfile: String(uuid).replace(/-/g, ''),
|
||||||
|
serverId: aufgabe.daten.serverId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!join.ok) throw new Error('Mojang ' + join.status);
|
||||||
|
const bestaetigt = await roheAnfrage('/auth/verify', { method: 'POST', body: { uuid } });
|
||||||
|
if (!bestaetigt.res.ok || !bestaetigt.daten || !bestaetigt.daten.token) throw new Error('nicht bestätigt');
|
||||||
|
sitzungen.set(uuid, bestaetigt.daten.token);
|
||||||
|
fehlversuch.delete(uuid);
|
||||||
|
return bestaetigt.daten.token;
|
||||||
|
} catch {
|
||||||
|
fehlversuch.set(uuid, Date.now());
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
laufendeAnmeldung.delete(uuid);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
laufendeAnmeldung.set(uuid, ablauf);
|
||||||
|
return ablauf;
|
||||||
|
}
|
||||||
|
|
||||||
async function anfrage(pfad, { method = 'GET', body } = {}) {
|
async function anfrage(pfad, { method = 'GET', body } = {}) {
|
||||||
try {
|
try {
|
||||||
const url = method === 'GET' && body
|
const uuid = body && body.uuid;
|
||||||
? `${BASIS_URL}${pfad}?${new URLSearchParams(body).toString()}`
|
let token = uuid ? sitzungen.get(uuid) : null;
|
||||||
: `${BASIS_URL}${pfad}`;
|
if (uuid && !token) token = await anmelden(uuid);
|
||||||
const res = await fetch(url, {
|
let { res, daten } = await roheAnfrage(pfad, { method, body, token });
|
||||||
method,
|
// Sitzung abgelaufen oder Anmeldung jetzt Pflicht -> einmal neu anmelden
|
||||||
headers: { 'Content-Type': 'application/json' },
|
if (res.status === 401 && daten && daten.code === 'aeromc_auth' && uuid) {
|
||||||
body: method === 'GET' ? undefined : JSON.stringify(body || {}),
|
sitzungen.delete(uuid);
|
||||||
});
|
fehlversuch.delete(uuid);
|
||||||
const daten = await res.json().catch(() => null);
|
token = await anmelden(uuid);
|
||||||
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
if (token) ({ res, daten } = await roheAnfrage(pfad, { method, body, token }));
|
||||||
|
}
|
||||||
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}`, code: daten && daten.code };
|
||||||
return { ok: true, data: daten };
|
return { ok: true, data: daten };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
|
return { ok: false, message: 'Freundesystem nicht erreichbar: ' + err.message };
|
||||||
@@ -79,7 +150,58 @@ function dismissInvite(uuid, id) {
|
|||||||
return anfrage('/friends/invites/dismiss', { method: 'POST', body: { uuid, id } });
|
return anfrage('/friends/invites/dismiss', { method: 'POST', body: { uuid, id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instanz per Code teilen: Beschreibung (Version, Loader, Mod-Liste) hinterlegen -> { code, days }
|
||||||
|
function createShare(uuid, manifest) {
|
||||||
|
return anfrage('/share', { method: 'POST', body: { uuid, manifest } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// geteilte Instanz abrufen (ohne Anmeldung – wer den Code hat, darf sie sehen)
|
||||||
|
async function getShare(code) {
|
||||||
|
try {
|
||||||
|
const { res, daten } = await roheAnfrage('/share', { method: 'GET', body: { code } });
|
||||||
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
||||||
|
return { ok: true, data: daten };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Server nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Kurznachrichten ----
|
||||||
|
// image: optional base64-JPEG (geteilter Screenshot)
|
||||||
|
function sendMessage(uuid, targetUuid, text, image) {
|
||||||
|
const body = { uuid, targetUuid, text: text || '' };
|
||||||
|
if (image) body.image = image;
|
||||||
|
return anfrage('/friends/message', { method: 'POST', body });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unterhaltung mit einem Freund (älteste zuerst)
|
||||||
|
function getMessages(uuid, withUuid) {
|
||||||
|
return anfrage('/friends/messages', { method: 'GET', body: { uuid, with: withUuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ungelesene Nachrichten an mich
|
||||||
|
function getUnreadMessages(uuid) {
|
||||||
|
return anfrage('/friends/messages/unread', { method: 'GET', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function markMessagesRead(uuid, fromUuid) {
|
||||||
|
return anfrage('/friends/messages/read', { method: 'POST', body: { uuid, fromUuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fehlerbericht aus dem Absturz-Assistenten; mit uuid wird angemeldet gesendet, sonst anonym
|
||||||
|
async function sendReport(daten) {
|
||||||
|
if (daten && daten.uuid) return anfrage('/report', { method: 'POST', body: daten });
|
||||||
|
try {
|
||||||
|
const { res, daten: antwort } = await roheAnfrage('/report', { method: 'POST', body: daten });
|
||||||
|
if (!res.ok) return { ok: false, message: (antwort && antwort.message) || `Fehler ${res.status}` };
|
||||||
|
return { ok: true, data: antwort };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Server nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
||||||
sendInvite, getInvites, dismissInvite,
|
sendInvite, getInvites, dismissInvite, createShare, getShare, setzeZugangsHelfer,
|
||||||
|
sendMessage, getMessages, getUnreadMessages, markMessagesRead, sendReport,
|
||||||
};
|
};
|
||||||
|
|||||||
+130
-1
@@ -64,6 +64,7 @@
|
|||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<button id="p-shortcut" class="sbtn">Verknüpfung erstellen</button>
|
<button id="p-shortcut" class="sbtn">Verknüpfung erstellen</button>
|
||||||
<button id="p-export" class="sbtn">Instanz exportieren</button>
|
<button id="p-export" class="sbtn">Instanz exportieren</button>
|
||||||
|
<button id="p-share" class="sbtn" title="Code erzeugen, mit dem Freunde diese Instanz samt Mods nachbauen können">Instanz teilen</button>
|
||||||
<button id="p-delete" class="sbtn del" title="Entf">Löschen</button>
|
<button id="p-delete" class="sbtn del" title="Entf">Löschen</button>
|
||||||
<button id="p-copy" class="sbtn">Instanz kopieren</button>
|
<button id="p-copy" class="sbtn">Instanz kopieren</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -583,6 +584,14 @@
|
|||||||
<small id="s-autobackup-last" class="hint">Noch keine automatische Sicherung.</small>
|
<small id="s-autobackup-last" class="hint">Noch keine automatische Sicherung.</small>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card">
|
||||||
|
<h5>Welten</h5>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="s-auto-world" type="checkbox" /> <span>Nach jeder Spielsitzung die zuletzt gespielte Welt sichern</span>
|
||||||
|
</label>
|
||||||
|
<small class="hint">Läuft im Hintergrund, sobald Minecraft beendet ist. Pro Welt bleiben die letzten 5 Stände erhalten – unter Dokumente\AeroMC Launcher\Welten-Sicherungen\<Instanz>\Automatisch.</small>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="settings-card settings-card-wide">
|
<section class="settings-card settings-card-wide">
|
||||||
<h5>Wiederherstellen</h5>
|
<h5>Wiederherstellen</h5>
|
||||||
<small class="hint">Wähle ein Backup aus Dokumente/AeroMC Launcher/Backups aus. Achtung: Alle vorhandenen Instanzen werden beim Wiederherstellen mit dem Inhalt des Backups überschrieben. Launcher-Einstellungen werden ebenfalls wiederhergestellt. Konten und Tokens bleiben unverändert.</small>
|
<small class="hint">Wähle ein Backup aus Dokumente/AeroMC Launcher/Backups aus. Achtung: Alle vorhandenen Instanzen werden beim Wiederherstellen mit dem Inhalt des Backups überschrieben. Launcher-Einstellungen werden ebenfalls wiederhergestellt. Konten und Tokens bleiben unverändert.</small>
|
||||||
@@ -1011,6 +1020,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Instanz teilen ===================== -->
|
||||||
|
<div id="modal-share" class="modal hidden">
|
||||||
|
<div class="modal-card modal-sm">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3 id="share-title">Instanz teilen</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="share-status" class="hint">Code wird erstellt …</p>
|
||||||
|
<div id="share-result" class="hidden">
|
||||||
|
<p class="hint" style="margin-top:0">Gib diesen Code an deine Freunde weiter. Sie lösen ihn unter <b>Importieren → Per Code</b> ein.</p>
|
||||||
|
<div class="share-code-box">
|
||||||
|
<span id="share-code" class="share-code">XXXX-XXXX</span>
|
||||||
|
<button id="share-copy" class="btn btn-mini">Kopieren</button>
|
||||||
|
</div>
|
||||||
|
<p id="share-info" class="hint"></p>
|
||||||
|
<p id="share-warn" class="hint share-warn hidden"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="btn" data-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Dialog: Spielzeit-Statistik ===================== -->
|
<!-- ===================== Dialog: Spielzeit-Statistik ===================== -->
|
||||||
<div id="modal-stats" class="modal hidden">
|
<div id="modal-stats" class="modal hidden">
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
@@ -1048,6 +1083,25 @@
|
|||||||
<div id="friends-requests-box"></div>
|
<div id="friends-requests-box"></div>
|
||||||
<div id="friends-list-box"></div>
|
<div id="friends-list-box"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Unterhaltung mit einem Freund -->
|
||||||
|
<div id="friends-chat" class="friends-chat hidden">
|
||||||
|
<div class="chat-head">
|
||||||
|
<button id="chat-back" class="btn btn-mini" title="Zurück zur Freundesliste">← Zurück</button>
|
||||||
|
<img id="chat-avatar" class="friend-avatar" alt="" />
|
||||||
|
<b id="chat-name"></b>
|
||||||
|
</div>
|
||||||
|
<div id="chat-verlauf" class="chat-verlauf"></div>
|
||||||
|
<div class="chat-schnell">
|
||||||
|
<button class="btn btn-mini chat-vorlage">Bin in 10 Min. online</button>
|
||||||
|
<button class="btn btn-mini chat-vorlage">Komm online!</button>
|
||||||
|
<button class="btn btn-mini chat-vorlage">Bin gleich zurück</button>
|
||||||
|
<button class="btn btn-mini chat-vorlage">Gute Nacht 👋</button>
|
||||||
|
</div>
|
||||||
|
<div class="path-row">
|
||||||
|
<input id="chat-text" type="text" maxlength="200" placeholder="Nachricht …" autocomplete="off" />
|
||||||
|
<button id="chat-send" class="btn btn-primary">Senden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot">
|
<div class="modal-foot">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
@@ -1201,8 +1255,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p id="crash-sub" class="hint">–</p>
|
<p id="crash-sub" class="hint">–</p>
|
||||||
|
<div id="crash-hilfe" class="crash-hilfe"></div>
|
||||||
<div id="crash-kern" class="crash-kern"></div>
|
<div id="crash-kern" class="crash-kern"></div>
|
||||||
<details class="crash-details">
|
<details id="crash-details" class="crash-details">
|
||||||
<summary>Vollständigen Bericht anzeigen</summary>
|
<summary>Vollständigen Bericht anzeigen</summary>
|
||||||
<pre id="crash-voll" class="crash-voll"></pre>
|
<pre id="crash-voll" class="crash-voll"></pre>
|
||||||
</details>
|
</details>
|
||||||
@@ -1210,13 +1265,47 @@
|
|||||||
<div class="modal-foot">
|
<div class="modal-foot">
|
||||||
<button id="crash-folder" class="btn" data-ic="folder">Ordner öffnen</button>
|
<button id="crash-folder" class="btn" data-ic="folder">Ordner öffnen</button>
|
||||||
<button id="crash-console" class="btn">Konsole</button>
|
<button id="crash-console" class="btn">Konsole</button>
|
||||||
|
<button id="crash-send" class="btn" title="Bericht und Mod-Liste an den Entwickler schicken">An M_Viper senden</button>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
<button id="crash-retry" class="btn btn-primary hidden">Erneut starten</button>
|
||||||
<button class="btn" data-close>Schließen</button>
|
<button class="btn" data-close>Schließen</button>
|
||||||
<button id="crash-copy" class="btn btn-primary">Bericht kopieren</button>
|
<button id="crash-copy" class="btn btn-primary">Bericht kopieren</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Fehlerbericht senden ===================== -->
|
||||||
|
<div id="modal-report" class="modal hidden">
|
||||||
|
<div class="modal-card modal-sm">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>Fehlerbericht an M_Viper senden</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="hint" style="margin-top:0">Damit hilfst du, Fehler in AeroMC zu finden und zu beheben. Gesendet wird:</p>
|
||||||
|
<ul class="report-liste">
|
||||||
|
<li>der Absturzbericht bzw. das Ende des Spielprotokolls</li>
|
||||||
|
<li>die Liste deiner Mods dieser Instanz und ihre Versionen</li>
|
||||||
|
<li>Minecraft-Version, Loader, Launcher-Version, Windows-Version und RAM</li>
|
||||||
|
<li>was der Absturz-Assistent erkannt hat</li>
|
||||||
|
</ul>
|
||||||
|
<p class="hint">Dein Windows-Benutzername und Anmeldedaten werden vorher entfernt. Ohne Häkchen unten wird anonym gesendet – dann sind auch dein Minecraft-Name und deine UUID unkenntlich gemacht.</p>
|
||||||
|
<label class="field">
|
||||||
|
<span>Was hast du gemacht, als es passiert ist? <small class="opt">(optional)</small></span>
|
||||||
|
<textarea id="report-kommentar" rows="3" maxlength="2000" placeholder="z. B. „Beim Betreten des Nethers abgestürzt“"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="report-name" type="checkbox" /> <span>Meinen Minecraft-Namen mitsenden (für Rückfragen)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="btn" data-close>Abbrechen</button>
|
||||||
|
<button id="report-do" class="btn btn-primary">Senden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Dialog: Lizenz ===================== -->
|
<!-- ===================== Dialog: Lizenz ===================== -->
|
||||||
<div id="modal-license" class="modal hidden">
|
<div id="modal-license" class="modal hidden">
|
||||||
<div class="modal-card modal-lg">
|
<div class="modal-card modal-lg">
|
||||||
@@ -1277,6 +1366,7 @@
|
|||||||
<button id="import-tab-cf" class="mtab" data-kind="cf">CurseForge</button>
|
<button id="import-tab-cf" class="mtab" data-kind="cf">CurseForge</button>
|
||||||
<button id="import-tab-mmc" class="mtab" data-kind="mmc">MultiMC / Prism</button>
|
<button id="import-tab-mmc" class="mtab" data-kind="mmc">MultiMC / Prism</button>
|
||||||
<button id="import-tab-technic" class="mtab" data-kind="technic">Technic</button>
|
<button id="import-tab-technic" class="mtab" data-kind="technic">Technic</button>
|
||||||
|
<button id="import-tab-code" class="mtab" data-kind="code">Per Code</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="import-pane-zip" class="import-pane">
|
<div id="import-pane-zip" class="import-pane">
|
||||||
@@ -1394,6 +1484,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="import-pane-code" class="import-pane hidden">
|
||||||
|
<p class="import-copy">Ein Freund hat dir eine Instanz geteilt? Gib hier seinen Code ein – AeroMC legt die Instanz an und lädt alle Mods, Ressourcenpakete und Shader in denselben Versionen selbst von Modrinth bzw. CurseForge.</p>
|
||||||
|
<label class="field">
|
||||||
|
<span>Code</span>
|
||||||
|
<div class="path-row">
|
||||||
|
<input id="code-input" class="share-code-input" type="text" placeholder="z. B. K7QD-M2XP" maxlength="12" autocomplete="off" spellcheck="false" />
|
||||||
|
<button id="code-check" class="btn btn-primary">Prüfen</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div id="code-status" class="hint" style="margin-top:8px"></div>
|
||||||
|
<div id="code-preview" class="share-preview hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="import-pane-technic" class="import-pane hidden">
|
<div id="import-pane-technic" class="import-pane hidden">
|
||||||
<p class="import-copy">Modpack direkt von der Technic-Plattform installieren oder eine bereits vorhandene Technic-Installation importieren.</p>
|
<p class="import-copy">Modpack direkt von der Technic-Plattform installieren oder eine bereits vorhandene Technic-Installation importieren.</p>
|
||||||
|
|
||||||
@@ -1438,6 +1541,7 @@
|
|||||||
<div id="mmc-progress" class="hint hidden"></div>
|
<div id="mmc-progress" class="hint hidden"></div>
|
||||||
<div id="cf-progress" class="hint hidden"></div>
|
<div id="cf-progress" class="hint hidden"></div>
|
||||||
<div id="technic-progress" class="hint hidden"></div>
|
<div id="technic-progress" class="hint hidden"></div>
|
||||||
|
<div id="code-progress" class="hint hidden"></div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button class="btn" data-close>Abbrechen</button>
|
<button class="btn" data-close>Abbrechen</button>
|
||||||
<button id="import-zip-do" class="btn btn-primary">ZIP wählen…</button>
|
<button id="import-zip-do" class="btn btn-primary">ZIP wählen…</button>
|
||||||
@@ -1446,6 +1550,7 @@
|
|||||||
<button id="cf-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
<button id="cf-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||||
<button id="mmc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
<button id="mmc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||||
<button id="technic-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
<button id="technic-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||||
|
<button id="code-do" class="btn btn-primary hidden" disabled>Instanz anlegen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1494,6 +1599,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Kontextmenü (Rechtsklick auf Instanz) ===================== -->
|
<!-- ===================== Kontextmenü (Rechtsklick auf Instanz) ===================== -->
|
||||||
|
<!-- Auswahl-Dialog: z. B. Ziel-Instanz, Freund oder Schnellstart-Taste (siehe waehleAus) -->
|
||||||
|
<div id="modal-wahl" class="modal hidden">
|
||||||
|
<div class="modal-card modal-sm">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3 id="wahl-titel">Auswählen</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="wahl-text" class="hint" style="margin-top:0"></p>
|
||||||
|
<div id="wahl-liste" class="wahl-liste"></div>
|
||||||
|
<label id="wahl-eingabe-feld" class="field hidden" style="margin:12px 0 0">
|
||||||
|
<span id="wahl-eingabe-label">Text</span>
|
||||||
|
<input id="wahl-eingabe" type="text" maxlength="200" autocomplete="off" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="btn" data-close>Abbrechen</button>
|
||||||
|
<button id="wahl-ok" class="btn btn-primary" disabled>OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="ctx-menu" class="ctx hidden">
|
<div id="ctx-menu" class="ctx hidden">
|
||||||
<button class="ctx-item" data-act="p-edit">Symbol ändern</button>
|
<button class="ctx-item" data-act="p-edit">Symbol ändern</button>
|
||||||
<button class="ctx-item" data-act="p-edit">Umbenennen</button>
|
<button class="ctx-item" data-act="p-edit">Umbenennen</button>
|
||||||
@@ -1501,6 +1629,7 @@
|
|||||||
<div class="ctx-sep"></div>
|
<div class="ctx-sep"></div>
|
||||||
<button class="ctx-item ctx-strong" data-act="p-play">Starten</button>
|
<button class="ctx-item ctx-strong" data-act="p-play">Starten</button>
|
||||||
<button class="ctx-item" data-act="p-offline">Offline starten</button>
|
<button class="ctx-item" data-act="p-offline">Offline starten</button>
|
||||||
|
<button class="ctx-item" data-act="ctx-schnellstart">Schnellstart-Taste …</button>
|
||||||
<div class="ctx-sep"></div>
|
<div class="ctx-sep"></div>
|
||||||
<button class="ctx-item" data-act="p-edit">Instanz bearbeiten</button>
|
<button class="ctx-item" data-act="p-edit">Instanz bearbeiten</button>
|
||||||
<button class="ctx-item" data-act="p-notes">Notizen bearbeiten</button>
|
<button class="ctx-item" data-act="p-notes">Notizen bearbeiten</button>
|
||||||
|
|||||||
+581
-20
@@ -39,6 +39,7 @@ const servers = require('./servers');
|
|||||||
const mcping = require('./mcping');
|
const mcping = require('./mcping');
|
||||||
const discordpresence = require('./discordpresence');
|
const discordpresence = require('./discordpresence');
|
||||||
const friends = require('./friends');
|
const friends = require('./friends');
|
||||||
|
const crashhelp = require('./crashhelp');
|
||||||
|
|
||||||
// undici als globales fetch verwenden -> ermöglicht Proxy via setGlobalDispatcher
|
// undici als globales fetch verwenden -> ermöglicht Proxy via setGlobalDispatcher
|
||||||
const undici = require('undici');
|
const undici = require('undici');
|
||||||
@@ -240,6 +241,10 @@ const kontoPid = new Map(); // kontoId -> pid, für den Ingame-Toast: der muss a
|
|||||||
// (sonst sieht z. B. M_Viper eine Meldung über sich selbst, weil zufällig sein
|
// (sonst sieht z. B. M_Viper eine Meldung über sich selbst, weil zufällig sein
|
||||||
// Fenster zuerst gefunden wurde, obwohl Electric__Max der Freund war, der geprüft hat)
|
// Fenster zuerst gefunden wurde, obwohl Electric__Max der Freund war, der geprüft hat)
|
||||||
const kontoInstanz = new Map(); // kontoId -> instanceId des gerade laufenden Spiels (für "Einladen": eigener Server)
|
const kontoInstanz = new Map(); // kontoId -> instanceId des gerade laufenden Spiels (für "Einladen": eigener Server)
|
||||||
|
// Ende der Konsolenausgabe des letzten Starts je Instanz – für den Absturz-Assistenten,
|
||||||
|
// weil frühe Fehler (falsches Java, zu viel RAM) in keiner Datei landen
|
||||||
|
const letzteAusgabe = new Map();
|
||||||
|
const AUSGABE_MAX = 200000;
|
||||||
// Herzschläge aller laufenden Spiele – um eine geänderte Sichtbarkeit sofort zu melden
|
// Herzschläge aller laufenden Spiele – um eine geänderte Sichtbarkeit sofort zu melden
|
||||||
const laufendeHerzschlaege = new Set();
|
const laufendeHerzschlaege = new Set();
|
||||||
ipcMain.handle('recording:getPid', (_e, id) => laufendePid.get(id) || null);
|
ipcMain.handle('recording:getPid', (_e, id) => laufendePid.get(id) || null);
|
||||||
@@ -560,6 +565,62 @@ ipcMain.handle('recording:open', (_e, instanceName, dateiname) => {
|
|||||||
|
|
||||||
// Nur eine Instanz des Launchers. Ein zweiter Aufruf (Doppelklick auf eine
|
// Nur eine Instanz des Launchers. Ein zweiter Aufruf (Doppelklick auf eine
|
||||||
// Verknüpfung) reicht seine Argumente an das laufende Fenster weiter.
|
// Verknüpfung) reicht seine Argumente an das laufende Fenster weiter.
|
||||||
|
// Rechtsklick-Menü für Textfelder und markierten Text – Electron bringt von sich
|
||||||
|
// aus keins mit, ohne das ginge z. B. "Einfügen" eines Teilen-Codes nur per Strg+V.
|
||||||
|
// Gilt für Haupt- und alle Popup-Fenster.
|
||||||
|
app.on('web-contents-created', (_e, contents) => {
|
||||||
|
contents.on('context-menu', (_ev, params) => {
|
||||||
|
const auswahl = String(params.selectionText || '').trim();
|
||||||
|
if (!params.isEditable && !auswahl) return;
|
||||||
|
const f = params.editFlags || {};
|
||||||
|
const vorlage = params.isEditable
|
||||||
|
? [
|
||||||
|
{ label: 'Ausschneiden', role: 'cut', enabled: !!f.canCut },
|
||||||
|
{ label: 'Kopieren', role: 'copy', enabled: !!f.canCopy },
|
||||||
|
{ label: 'Einfügen', role: 'paste', enabled: !!f.canPaste },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Alles auswählen', role: 'selectAll', enabled: !!f.canSelectAll },
|
||||||
|
]
|
||||||
|
: [{ label: 'Kopieren', role: 'copy', enabled: !!f.canCopy }];
|
||||||
|
const fenster = BrowserWindow.fromWebContents(contents);
|
||||||
|
Menu.buildFromTemplate(vorlage).popup(fenster ? { window: fenster } : {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eigene App-Kennung für Windows (gleich wie appId in package.json und die
|
||||||
|
// Startmenü-Verknüpfung des Installers). Ohne das zeigt Windows bei
|
||||||
|
// Benachrichtigungen "electron.app.AeroMC" ohne Logo an.
|
||||||
|
const APP_KENNUNG = 'com.aeromc.launcher';
|
||||||
|
if (process.platform === 'win32') app.setAppUserModelId(APP_KENNUNG);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name und Logo dieser App-Kennung bei Windows hinterlegen
|
||||||
|
* (HKCU\Software\Classes\AppUserModelId\<Kennung>). Damit stimmt die Anzeige
|
||||||
|
* auch ohne Startmenü-Verknüpfung, z. B. bei der Portable-Fassung. Das Logo
|
||||||
|
* liegt im Paket (app.asar), das Windows nicht lesen kann – deshalb eine Kopie
|
||||||
|
* im Datenordner.
|
||||||
|
*/
|
||||||
|
let benachrichtigungsLogo = null;
|
||||||
|
function registriereAppBeiWindows() {
|
||||||
|
if (process.platform !== 'win32') return;
|
||||||
|
try {
|
||||||
|
const ziel = path.join(app.getPath('userData'), 'aeromc-logo.png');
|
||||||
|
const quelle = path.join(__dirname, 'assets', 'logo.png');
|
||||||
|
if (fs.existsSync(quelle)) {
|
||||||
|
const bild = nativeImage.createFromPath(quelle);
|
||||||
|
if (!bild.isEmpty()) {
|
||||||
|
fs.writeFileSync(ziel, bild.resize({ width: 128, height: 128, quality: 'best' }).toPNG());
|
||||||
|
benachrichtigungsLogo = ziel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const schluessel = `HKCU\\Software\\Classes\\AppUserModelId\\${APP_KENNUNG}`;
|
||||||
|
execFile('reg', ['add', schluessel, '/v', 'DisplayName', '/t', 'REG_SZ', '/d', 'AeroMC', '/f'], { windowsHide: true }, () => {});
|
||||||
|
if (benachrichtigungsLogo) {
|
||||||
|
execFile('reg', ['add', schluessel, '/v', 'IconUri', '/t', 'REG_SZ', '/d', benachrichtigungsLogo, '/f'], { windowsHide: true }, () => {});
|
||||||
|
}
|
||||||
|
} catch { /* rein kosmetisch */ }
|
||||||
|
}
|
||||||
|
|
||||||
const hatEinzelSperre = app.requestSingleInstanceLock();
|
const hatEinzelSperre = app.requestSingleInstanceLock();
|
||||||
if (!hatEinzelSperre) app.quit();
|
if (!hatEinzelSperre) app.quit();
|
||||||
|
|
||||||
@@ -712,6 +773,15 @@ function createWindow(startHidden) {
|
|||||||
ipcMain.once('renderer:ready', () => oeffneVorbereitetenImport());
|
ipcMain.once('renderer:ready', () => oeffneVorbereitetenImport());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Freundesystem-Anmeldung: Minecraft-Zugang eines Kontos für Mojangs Sitzungsprüfung
|
||||||
|
friends.setzeZugangsHelfer(async (uuid) => {
|
||||||
|
const clientId = store.getSettings().azureClientId || auth.DEFAULT_CLIENT_ID;
|
||||||
|
const konto = await auth.getValidAccountById(clientId, uuid).catch(() => null);
|
||||||
|
return konto && konto.profile && konto.mcAccessToken
|
||||||
|
? { accessToken: konto.mcAccessToken, name: konto.profile.name }
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Das Setup kann optional andere Launcher übernehmen lassen (installer/Installer.cs):
|
* Das Setup kann optional andere Launcher übernehmen lassen (installer/Installer.cs):
|
||||||
* es legt dazu pending-import.json mit den angekreuzten Launchern ab. Beim ersten
|
* es legt dazu pending-import.json mit den angekreuzten Launchern ab. Beim ersten
|
||||||
@@ -755,15 +825,47 @@ function createTray() {
|
|||||||
const bild = iconPfad ? nativeImage.createFromPath(iconPfad) : nativeImage.createEmpty();
|
const bild = iconPfad ? nativeImage.createFromPath(iconPfad) : nativeImage.createEmpty();
|
||||||
tray = new Tray(bild.isEmpty() ? nativeImage.createEmpty() : bild.resize({ width: 16, height: 16 }));
|
tray = new Tray(bild.isEmpty() ? nativeImage.createEmpty() : bild.resize({ width: 16, height: 16 }));
|
||||||
tray.setToolTip('AeroMC');
|
tray.setToolTip('AeroMC');
|
||||||
tray.setContextMenu(Menu.buildFromTemplate([
|
// Menü bei jedem Rechtsklick neu bauen – so sind Favoriten und zuletzt
|
||||||
{ label: 'Öffnen', click: () => showMainWindow() },
|
// gespielte Instanzen immer aktuell
|
||||||
{ type: 'separator' },
|
tray.on('right-click', () => tray.popUpContextMenu(trayMenue()));
|
||||||
{ label: 'Beenden', click: () => { appIsQuitting = true; app.quit(); } },
|
|
||||||
]));
|
|
||||||
tray.on('click', () => showMainWindow());
|
tray.on('click', () => showMainWindow());
|
||||||
} catch { /* z. B. wenn kein Icon vorhanden ist – Autostart läuft dann ohne Tray-Symbol weiter */ }
|
} catch { /* z. B. wenn kein Icon vorhanden ist – Autostart läuft dann ohne Tray-Symbol weiter */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instanzen direkt aus dem Infobereich starten, ohne das Fenster zu öffnen:
|
||||||
|
// erst die Schnellstart-Favoriten (Strg+1 … 5), dann die zuletzt gespielten
|
||||||
|
function trayMenue() {
|
||||||
|
const alle = store.listInstances();
|
||||||
|
const nachId = new Map(alle.map((i) => [i.id, i]));
|
||||||
|
const schnell = store.getSettings().schnellstart || {};
|
||||||
|
const favoriten = Object.keys(schnell).sort()
|
||||||
|
.map((taste) => ({ taste, inst: nachId.get(schnell[taste]) }))
|
||||||
|
.filter((f) => f.inst);
|
||||||
|
const favIds = new Set(favoriten.map((f) => f.inst.id));
|
||||||
|
const zuletzt = alle.filter((i) => i.lastPlayed && !favIds.has(i.id))
|
||||||
|
.sort((a, b) => String(b.lastPlayed).localeCompare(String(a.lastPlayed)))
|
||||||
|
.slice(0, 5);
|
||||||
|
const starte = (inst) => () => starteInstanzImFenster(inst.id);
|
||||||
|
const eintrag = (inst, zusatz) => ({
|
||||||
|
label: `${inst.name} · ${inst.minecraft.version || '?'}${zusatz ? ' ' + zusatz : ''}`,
|
||||||
|
click: starte(inst),
|
||||||
|
});
|
||||||
|
|
||||||
|
const vorlage = [{ label: 'AeroMC öffnen', click: () => showMainWindow() }, { type: 'separator' }];
|
||||||
|
if (favoriten.length) {
|
||||||
|
vorlage.push({ label: 'Schnellstart', enabled: false });
|
||||||
|
favoriten.forEach((f) => vorlage.push(eintrag(f.inst, `Strg+${f.taste}`)));
|
||||||
|
}
|
||||||
|
if (zuletzt.length) {
|
||||||
|
if (favoriten.length) vorlage.push({ type: 'separator' });
|
||||||
|
vorlage.push({ label: 'Zuletzt gespielt', enabled: false });
|
||||||
|
zuletzt.forEach((i) => vorlage.push(eintrag(i)));
|
||||||
|
}
|
||||||
|
if (!favoriten.length && !zuletzt.length) vorlage.push({ label: 'Noch keine Instanz gespielt', enabled: false });
|
||||||
|
vorlage.push({ type: 'separator' }, { label: 'Beenden', click: () => { appIsQuitting = true; app.quit(); } });
|
||||||
|
return Menu.buildFromTemplate(vorlage);
|
||||||
|
}
|
||||||
|
|
||||||
function destroyTray() {
|
function destroyTray() {
|
||||||
if (tray) { tray.destroy(); tray = null; }
|
if (tray) { tray.destroy(); tray = null; }
|
||||||
}
|
}
|
||||||
@@ -782,12 +884,13 @@ function applyAutostart(enabled) {
|
|||||||
: [];
|
: [];
|
||||||
app.setLoginItemSettings({ openAtLogin: !!enabled, path: verknuepfungsZiel(), args });
|
app.setLoginItemSettings({ openAtLogin: !!enabled, path: verknuepfungsZiel(), args });
|
||||||
} catch { /* z. B. in portabler Umgebung ohne Schreibrechte auf die Registry */ }
|
} catch { /* z. B. in portabler Umgebung ohne Schreibrechte auf die Registry */ }
|
||||||
if (enabled) createTray();
|
// Symbol im Infobereich gibt es immer (Schnellstart per Rechtsklick)
|
||||||
else destroyTray();
|
createTray();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
if (!hatEinzelSperre) return; // zweite Instanz beendet sich selbst
|
if (!hatEinzelSperre) return; // zweite Instanz beendet sich selbst
|
||||||
|
registriereAppBeiWindows();
|
||||||
store.init(app.getPath('userData'));
|
store.init(app.getPath('userData'));
|
||||||
services.setCacheDir(app.getPath('userData')); // Java-Suche zwischenspeichern
|
services.setCacheDir(app.getPath('userData')); // Java-Suche zwischenspeichern
|
||||||
// selbst geholte Laufzeiten: ablegen und bei der Java-Suche mitberücksichtigen
|
// selbst geholte Laufzeiten: ablegen und bei der Java-Suche mitberücksichtigen
|
||||||
@@ -846,6 +949,8 @@ app.whenReady().then(() => {
|
|||||||
// Einladungen ("Komm auf meinen Server") öfter prüfen – sie gelten nur ein paar Minuten
|
// Einladungen ("Komm auf meinen Server") öfter prüfen – sie gelten nur ein paar Minuten
|
||||||
setTimeout(() => { pruefeEinladungen().catch(() => {}); }, 8000);
|
setTimeout(() => { pruefeEinladungen().catch(() => {}); }, 8000);
|
||||||
setInterval(() => { pruefeEinladungen().catch(() => {}); }, 15000);
|
setInterval(() => { pruefeEinladungen().catch(() => {}); }, 15000);
|
||||||
|
setTimeout(() => { pruefeNachrichten().catch(() => {}); }, 9000);
|
||||||
|
setInterval(() => { pruefeNachrichten().catch(() => {}); }, 15000);
|
||||||
// Zahl offener Freundschaftsanfragen für den "Freunde"-Knopf
|
// Zahl offener Freundschaftsanfragen für den "Freunde"-Knopf
|
||||||
setTimeout(() => { aktualisiereAnfragenZahl().catch(() => {}); }, 3000);
|
setTimeout(() => { aktualisiereAnfragenZahl().catch(() => {}); }, 3000);
|
||||||
setInterval(() => { aktualisiereAnfragenZahl().catch(() => {}); }, 60000);
|
setInterval(() => { aktualisiereAnfragenZahl().catch(() => {}); }, 60000);
|
||||||
@@ -1027,6 +1132,7 @@ async function installiereInhalt(id, project, art) {
|
|||||||
} else {
|
} else {
|
||||||
res = await services.modrinthInstall({
|
res = await services.modrinthInstall({
|
||||||
projectId: proj.projectId, title: proj.title, gameVersion, loader, modsDir, art: art || 'mod',
|
projectId: proj.projectId, title: proj.title, gameVersion, loader, modsDir, art: art || 'mod',
|
||||||
|
versionId: isDep ? '' : (proj.versionId || ''),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (!isDep && !firstError) firstError = res.message;
|
if (!isDep && !firstError) firstError = res.message;
|
||||||
@@ -1272,6 +1378,164 @@ ipcMain.handle('crash:latest', (_e, id, seit) => {
|
|||||||
return crashreports.lies(store.gameDir(id), Number(seit) || 0);
|
return crashreports.lies(store.gameDir(id), Number(seit) || 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Absturz-Assistent ----
|
||||||
|
// Wertet Absturzbericht, latest.log und Konsolenausgabe des letzten Starts aus
|
||||||
|
// (siehe crashhelp.js) und liefert verständliche Funde mit Lösungs-Aktion.
|
||||||
|
function absturzDaten(id, seit) {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
if (!inst) return null;
|
||||||
|
const gameDir = store.gameDir(id);
|
||||||
|
const seitMs = Number(seit) || 0;
|
||||||
|
const bericht = crashreports.lies(gameDir, seitMs);
|
||||||
|
|
||||||
|
// latest.log nur, wenn es zu diesem Start gehört (sonst ist es vom letzten Mal)
|
||||||
|
let log = '';
|
||||||
|
try {
|
||||||
|
const logDatei = path.join(gameDir, 'logs', 'latest.log');
|
||||||
|
if (fs.existsSync(logDatei) && (!seitMs || fs.statSync(logDatei).mtimeMs >= seitMs)) {
|
||||||
|
const r = crashreports.liesLog(gameDir, 'latest.log', 4000);
|
||||||
|
if (r.ok) log = r.text;
|
||||||
|
}
|
||||||
|
} catch { /* egal */ }
|
||||||
|
|
||||||
|
const settings = store.getSettings();
|
||||||
|
const text = [bericht.ok ? bericht.text : '', log, letzteAusgabe.get(id) || ''].join('\n');
|
||||||
|
const funde = crashhelp.analysiere(text, {
|
||||||
|
loader: inst.minecraft.loader,
|
||||||
|
version: inst.minecraft.version,
|
||||||
|
maxMemMb: inst.java.maxMemMb || settings.defaultMaxMemMb,
|
||||||
|
gesamtMb: gesamtSpeicherMb(),
|
||||||
|
mods: store.listMods(id, 'mod'),
|
||||||
|
});
|
||||||
|
return { inst, funde, bericht: bericht.ok ? bericht : null, log, ausgabe: letzteAusgabe.get(id) || '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('crash:analyze', (_e, id, seit) => {
|
||||||
|
const daten = absturzDaten(id, seit);
|
||||||
|
if (!daten) return { ok: false };
|
||||||
|
return { ok: true, funde: daten.funde, bericht: daten.bericht };
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Fehlerbericht an M_Viper (nur auf ausdrücklichen Wunsch) ----
|
||||||
|
// Vor dem Senden werden persönliche Angaben entfernt: Windows-Benutzerordner in
|
||||||
|
// Pfaden, Zugangs-Tokens und – wenn anonym – Minecraft-Name und UUID.
|
||||||
|
function regexSicher(t) { return String(t).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
||||||
|
|
||||||
|
function bereinigeBericht(text, konto) {
|
||||||
|
let t = String(text || '');
|
||||||
|
const heim = os.homedir();
|
||||||
|
for (const variante of [heim, heim.replace(/\\/g, '/'), heim.replace(/\\/g, '\\\\')]) {
|
||||||
|
t = t.replace(new RegExp(regexSicher(variante), 'gi'), '%USERPROFILE%');
|
||||||
|
}
|
||||||
|
const benutzer = os.userInfo().username;
|
||||||
|
if (benutzer && benutzer.length > 2) t = t.replace(new RegExp('(\\\\|/)' + regexSicher(benutzer) + '(\\\\|/)', 'gi'), '$1%USERNAME%$2');
|
||||||
|
// Zugangsdaten nie mitschicken
|
||||||
|
t = t.replace(/eyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]{10,}/g, '***');
|
||||||
|
t = t.replace(/((?:accessToken|auth_access_token|auth_session|session)["'\s:=]+(?:token:)?)[^\s"',]+/gi, '$1***');
|
||||||
|
if (konto) {
|
||||||
|
if (konto.name) t = t.replace(new RegExp(regexSicher(konto.name), 'g'), '<Spieler>');
|
||||||
|
if (konto.uuid) {
|
||||||
|
const ohne = String(konto.uuid).replace(/-/g, '');
|
||||||
|
const mit = ohne.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, '$1-$2-$3-$4-$5');
|
||||||
|
t = t.replace(new RegExp(regexSicher(ohne) + '|' + regexSicher(mit), 'gi'), '<UUID>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('crash:report', async (_e, id, seit, wahl) => {
|
||||||
|
const daten = absturzDaten(id, seit);
|
||||||
|
if (!daten) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
const mitName = !!(wahl && wahl.mitName);
|
||||||
|
const aktiv = aktivesFreundeKonto();
|
||||||
|
// anonym: Name/UUID aus dem Text entfernen; mit Name: nur Pfade/Tokens bereinigen
|
||||||
|
const anonymKonto = mitName ? null : (aktiv ? { name: aktiv.username, uuid: aktiv.uuid } : null);
|
||||||
|
|
||||||
|
const ende = (text, zeilen) => String(text || '').split(/\r?\n/).slice(-zeilen).join('\n');
|
||||||
|
let bericht = '';
|
||||||
|
if (daten.bericht) bericht += '--- Absturzbericht ' + (daten.bericht.datei || '') + ' ---\n' + daten.bericht.text + '\n\n';
|
||||||
|
if (daten.log) bericht += '--- latest.log (Ende) ---\n' + ende(daten.log, daten.bericht ? 150 : 400) + '\n\n';
|
||||||
|
if (daten.ausgabe) bericht += '--- Konsolenausgabe (Ende) ---\n' + ende(daten.ausgabe, 300) + '\n';
|
||||||
|
bericht = bereinigeBericht(bericht, anonymKonto).slice(-190000);
|
||||||
|
|
||||||
|
const inst = daten.inst;
|
||||||
|
const nutzlast = {
|
||||||
|
launcherVersion: app.getVersion(),
|
||||||
|
mcVersion: inst.minecraft.version || '',
|
||||||
|
loader: inst.minecraft.loader || 'vanilla',
|
||||||
|
loaderVersion: inst.minecraft.loaderVersion || '',
|
||||||
|
system: `Windows ${os.release()} · ${Math.round(os.totalmem() / 1073741824)} GB RAM · Instanz-RAM ${inst.java.maxMemMb || store.getSettings().defaultMaxMemMb || '?'} MB`,
|
||||||
|
funde: daten.funde.map((f) => ({ titel: f.titel, text: f.text })),
|
||||||
|
mods: store.listMods(id, 'mod').map((m) => ({
|
||||||
|
datei: m.filename, name: m.title || '', version: m.versionNumber || '', aktiv: m.enabled !== false,
|
||||||
|
})),
|
||||||
|
bericht,
|
||||||
|
kommentar: bereinigeBericht(String((wahl && wahl.kommentar) || '').slice(0, 2000), anonymKonto),
|
||||||
|
};
|
||||||
|
if (mitName && aktiv) { nutzlast.uuid = aktiv.uuid; nutzlast.username = aktiv.username; }
|
||||||
|
const res = await friends.sendReport(nutzlast);
|
||||||
|
return res.ok ? { ok: true, id: res.data && res.data.id } : res;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lösungs-Aktion eines Fundes ausführen
|
||||||
|
ipcMain.handle('crash:fix', async (_e, id, aktion) => {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
const a = aktion || {};
|
||||||
|
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
try {
|
||||||
|
switch (a.typ) {
|
||||||
|
case 'mod-installieren': {
|
||||||
|
// Mod-IDs und Modrinth-Namen weichen manchmal ab -> ein paar Schreibweisen probieren
|
||||||
|
const kandidaten = [...new Set([a.projekt, crashhelp.projektFuer(a.name),
|
||||||
|
String(a.name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-')].filter((k) => k && /^[\w-]{1,64}$/.test(k)))];
|
||||||
|
let letzter = null;
|
||||||
|
for (const projekt of kandidaten) {
|
||||||
|
const res = await installiereInhalt(id, { projectId: projekt, title: a.name || projekt }, 'mod').catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (res.ok) return { ok: true, text: `„${a.name || projekt}“ installiert.` };
|
||||||
|
letzter = res.message;
|
||||||
|
}
|
||||||
|
return { ok: false, message: `„${a.name}“ wurde auf Modrinth nicht gefunden${letzter ? ` (${letzter})` : ''}. Bitte im Inhalts-Dialog von Hand suchen.` };
|
||||||
|
}
|
||||||
|
case 'mod-deaktivieren': {
|
||||||
|
const res = store.setModEnabled(id, String(a.dateiname || ''), false, 'mod');
|
||||||
|
return res.ok ? { ok: true, text: `„${a.name || a.dateiname}“ abgeschaltet.` } : { ok: false, message: 'Konnte den Mod nicht abschalten.' };
|
||||||
|
}
|
||||||
|
case 'ram': {
|
||||||
|
const mb = Math.round(Number(a.mb) || 0);
|
||||||
|
if (mb < 1024) return { ok: false, message: 'Ungültige Speichergröße.' };
|
||||||
|
const minMb = inst.java.minMemMb && inst.java.minMemMb > mb ? Math.min(mb, 1024) : inst.java.minMemMb;
|
||||||
|
store.updateInstance(id, { java: { maxMemMb: mb, minMemMb: minMb || null } });
|
||||||
|
broadcast('instances:changed');
|
||||||
|
return { ok: true, text: `Arbeitsspeicher auf ${Math.round(mb / 102.4) / 10} GB gestellt.` };
|
||||||
|
}
|
||||||
|
case 'java-auto': {
|
||||||
|
store.updateInstance(id, { java: { path: '' } });
|
||||||
|
store.saveSettings({ autoJava: true });
|
||||||
|
broadcast('instances:changed');
|
||||||
|
return { ok: true, text: 'Beim nächsten Start wählt AeroMC das passende Java und lädt es bei Bedarf herunter.' };
|
||||||
|
}
|
||||||
|
case 'mods-ordner': {
|
||||||
|
const dir = store.inhaltsDir(id, 'mod');
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
shell.openPath(dir);
|
||||||
|
return { ok: true, text: 'Mods-Ordner geöffnet.' };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { ok: false, message: 'Unbekannte Aktion.' };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// nach einer Lösung gleich erneut starten (Start läuft im Hauptfenster)
|
||||||
|
ipcMain.handle('crash:relaunch', (_e, id) => {
|
||||||
|
if (!store.getInstance(id)) return false;
|
||||||
|
showMainWindow();
|
||||||
|
starteInstanzImFenster(id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Serverliste (servers.dat) ----
|
// ---- Serverliste (servers.dat) ----
|
||||||
|
|
||||||
ipcMain.handle('servers:list', (_e, id) => {
|
ipcMain.handle('servers:list', (_e, id) => {
|
||||||
@@ -1454,7 +1718,9 @@ ipcMain.handle('auth:setActive', (_e, id) => {
|
|||||||
// schon, wurde von hier nur nie ausgelöst.
|
// schon, wurde von hier nur nie ausgelöst.
|
||||||
broadcast('accounts:changed');
|
broadcast('accounts:changed');
|
||||||
// Anfragen-Zahl am "Freunde"-Knopf gilt für das aktive Konto
|
// Anfragen-Zahl am "Freunde"-Knopf gilt für das aktive Konto
|
||||||
|
ungeleseneNachrichten = [];
|
||||||
aktualisiereAnfragenZahl().catch(() => {});
|
aktualisiereAnfragenZahl().catch(() => {});
|
||||||
|
pruefeNachrichten().catch(() => {});
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1589,10 +1855,12 @@ async function zeigeIngameOverlay(titel, text, pid, dauerMs = 6000) {
|
|||||||
|
|
||||||
const zeilen = await Promise.all((Array.isArray(text) ? text : [text]).map(async (z) => {
|
const zeilen = await Promise.all((Array.isArray(text) ? text : [text]).map(async (z) => {
|
||||||
if (typeof z === 'string') return { text: z, kopf: null };
|
if (typeof z === 'string') return { text: z, kopf: null };
|
||||||
return { text: z.text, kopf: await spielerKopf(z.uuid) };
|
return { text: z.text, kopf: await spielerKopf(z.uuid), umbruch: !!z.umbruch };
|
||||||
}));
|
}));
|
||||||
const breite = 320;
|
const breite = 320;
|
||||||
const hoehe = 44 + zeilen.reduce((summe, z) => summe + (z.kopf ? 24 : 18), 0);
|
// Zeilen mit umbruch (z. B. Nachrichten) dürfen mehrzeilig werden – Höhe grob schätzen
|
||||||
|
const zeilenHoehe = (z) => (z.umbruch ? Math.max(z.kopf ? 24 : 18, Math.ceil(String(z.text).length / 38) * 17) : (z.kopf ? 24 : 18));
|
||||||
|
const hoehe = 44 + zeilen.reduce((summe, z) => summe + zeilenHoehe(z), 0);
|
||||||
const stapel = toastsProFenster.get(pid) || 0;
|
const stapel = toastsProFenster.get(pid) || 0;
|
||||||
toastsProFenster.set(pid, stapel + 1);
|
toastsProFenster.set(pid, stapel + 1);
|
||||||
const overlayFenster = new BrowserWindow({
|
const overlayFenster = new BrowserWindow({
|
||||||
@@ -1621,9 +1889,9 @@ async function zeigeIngameOverlay(titel, text, pid, dauerMs = 6000) {
|
|||||||
<div style="font-family:Segoe UI,sans-serif;background:rgba(20,20,25,0.88);color:#fff;border-radius:8px;
|
<div style="font-family:Segoe UI,sans-serif;background:rgba(20,20,25,0.88);color:#fff;border-radius:8px;
|
||||||
padding:10px 14px;box-shadow:0 4px 16px rgba(0,0,0,0.4);box-sizing:border-box;">
|
padding:10px 14px;box-shadow:0 4px 16px rgba(0,0,0,0.4);box-sizing:border-box;">
|
||||||
<div style="font-weight:600;font-size:13px;">${esc(titel)}</div>
|
<div style="font-weight:600;font-size:13px;">${esc(titel)}</div>
|
||||||
${zeilen.map((z) => `<div style="display:flex;align-items:center;gap:7px;font-size:12px;margin-top:${z.kopf ? 4 : 2}px;">
|
${zeilen.map((z) => `<div style="display:flex;align-items:${z.umbruch ? 'flex-start' : 'center'};gap:7px;font-size:12px;margin-top:${z.kopf ? 4 : 2}px;">
|
||||||
${z.kopf ? `<img src="${z.kopf}" style="width:20px;height:20px;flex:0 0 auto;" />` : ''}
|
${z.kopf ? `<img src="${z.kopf}" style="width:20px;height:20px;flex:0 0 auto;" />` : ''}
|
||||||
<span style="opacity:0.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(z.text)}</span>
|
<span style="opacity:0.85;${z.umbruch ? 'white-space:normal;word-break:break-word;line-height:1.35;' : 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'}">${esc(z.text)}</span>
|
||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</body></html>`)}`;
|
</body></html>`)}`;
|
||||||
@@ -1815,18 +2083,228 @@ ipcMain.handle('friends:dismissInvite', async (_e, inviteId) => {
|
|||||||
return friends.dismissInvite(konto.uuid, inviteId);
|
return friends.dismissInvite(konto.uuid, inviteId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- Offene Freundschaftsanfragen (Zahl am "Freunde"-Knopf) ----------
|
// ---------- Instanz per Code teilen ----------
|
||||||
let letzteAnfragenZahl = 0;
|
// Geteilt wird nur die Beschreibung: Minecraft-Version, Loader und die Liste der
|
||||||
|
// Mods/Ressourcenpakete/Shader mit ihrer genauen Modrinth/CurseForge-Version.
|
||||||
|
// Wer den Code einlöst, lädt alles selbst von dort – keine Dateien über den
|
||||||
|
// eigenen Server, keine Urheberrechtsprobleme. Von Hand hinzugefügte Dateien
|
||||||
|
// (ohne Herkunft) lassen sich so nicht übertragen und werden genannt.
|
||||||
|
const TEILBARE_ARTEN = ['mod', 'resourcepack', 'shader'];
|
||||||
|
const TEILBARE_LOADER = new Set(['vanilla', 'fabric', 'quilt', 'forge', 'neoforge']);
|
||||||
|
|
||||||
|
function teilBeschreibung(id) {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
if (!inst) return null;
|
||||||
|
const inhalte = [];
|
||||||
|
const nichtUebertragbar = [];
|
||||||
|
for (const art of TEILBARE_ARTEN) {
|
||||||
|
for (const m of store.listMods(id, art)) {
|
||||||
|
if (m.fehlt) continue;
|
||||||
|
if (!m.projectId) { nichtUebertragbar.push(m.filename); continue; }
|
||||||
|
inhalte.push({
|
||||||
|
art,
|
||||||
|
projectId: m.projectId,
|
||||||
|
versionId: m.versionId || '',
|
||||||
|
title: m.title || m.projectId,
|
||||||
|
aktiv: m.enabled !== false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
format: 1,
|
||||||
|
name: inst.name,
|
||||||
|
icon: inst.icon || '',
|
||||||
|
minecraft: {
|
||||||
|
version: inst.minecraft.version,
|
||||||
|
loader: inst.minecraft.loader || 'vanilla',
|
||||||
|
loaderVersion: inst.minecraft.loaderVersion || '',
|
||||||
|
},
|
||||||
|
inhalte,
|
||||||
|
nichtUebertragbar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('share:create', async (_e, id) => {
|
||||||
|
const beschreibung = teilBeschreibung(id);
|
||||||
|
if (!beschreibung) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
if (!beschreibung.minecraft.version) return { ok: false, message: 'Die Instanz hat noch keine Minecraft-Version.' };
|
||||||
|
if (!TEILBARE_LOADER.has(beschreibung.minecraft.loader)) {
|
||||||
|
return { ok: false, message: 'OptiFine-Instanzen lassen sich nicht teilen – OptiFine muss jeder selbst installieren.' };
|
||||||
|
}
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Bitte zuerst mit einem Microsoft-Konto anmelden.' };
|
||||||
|
const res = await friends.createShare(konto.uuid, beschreibung);
|
||||||
|
if (!res.ok) return res;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
code: res.data.code,
|
||||||
|
tage: res.data.days || 30,
|
||||||
|
anzahl: beschreibung.inhalte.length,
|
||||||
|
nichtUebertragbar: beschreibung.nichtUebertragbar,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('share:fetch', async (_e, code) => {
|
||||||
|
const res = await friends.getShare(code);
|
||||||
|
if (!res.ok) return res;
|
||||||
|
const m = res.data && res.data.manifest;
|
||||||
|
if (!m || !m.minecraft || !Array.isArray(m.inhalte)) return { ok: false, message: 'Die geteilte Instanz ist beschädigt.' };
|
||||||
|
return { ok: true, data: res.data };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('share:install', async (_e, code) => {
|
||||||
|
const abruf = await friends.getShare(code);
|
||||||
|
if (!abruf.ok) return abruf;
|
||||||
|
const m = abruf.data && abruf.data.manifest;
|
||||||
|
const version = m && m.minecraft && String(m.minecraft.version || '');
|
||||||
|
const loader = m && m.minecraft && String(m.minecraft.loader || 'vanilla');
|
||||||
|
if (!version || !/^[\w.\- ]{1,32}$/.test(version) || !TEILBARE_LOADER.has(loader) || !Array.isArray(m.inhalte)) {
|
||||||
|
return { ok: false, message: 'Die geteilte Instanz ist beschädigt.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// eindeutiger Name, damit nichts Vorhandenes ersetzt wird
|
||||||
|
const basis = String(m.name || 'Geteilte Instanz').replace(/[\\/:*?"<>|]/g, '').trim().slice(0, 60) || 'Geteilte Instanz';
|
||||||
|
const vorhanden = new Set(store.listInstances().map((i) => String(i.name).toLowerCase()));
|
||||||
|
let name = basis;
|
||||||
|
for (let n = 2; vorhanden.has(name.toLowerCase()); n++) name = `${basis} (${n})`;
|
||||||
|
const von = abruf.data.fromUsername || 'einem Freund';
|
||||||
|
const inst = store.createInstance({
|
||||||
|
name,
|
||||||
|
icon: typeof m.icon === 'string' ? m.icon : undefined,
|
||||||
|
notes: `Geteilt von ${von} (Code ${abruf.data.code})`,
|
||||||
|
minecraft: { version, loader, loaderVersion: String(m.minecraft.loaderVersion || '') },
|
||||||
|
});
|
||||||
|
broadcast('instances:changed');
|
||||||
|
|
||||||
|
const inhalte = m.inhalte.filter((it) => it && TEILBARE_ARTEN.includes(it.art)
|
||||||
|
&& /^[\w-]{1,64}$/.test(String(it.projectId || '')));
|
||||||
|
const installiert = [];
|
||||||
|
const fehlgeschlagen = [];
|
||||||
|
for (let i = 0; i < inhalte.length; i++) {
|
||||||
|
const it = inhalte[i];
|
||||||
|
const titel = String(it.title || it.projectId).slice(0, 80);
|
||||||
|
broadcast('share:progress', { aktuell: i + 1, gesamt: inhalte.length, titel });
|
||||||
|
// schon als Abhängigkeit eines früheren Eintrags mitgekommen?
|
||||||
|
if (store.listMods(inst.id, it.art).some((x) => x.projectId === it.projectId)) { installiert.push(titel); continue; }
|
||||||
|
const res = await installiereInhalt(inst.id, {
|
||||||
|
projectId: it.projectId,
|
||||||
|
title: titel,
|
||||||
|
versionId: typeof it.versionId === 'string' ? it.versionId : '',
|
||||||
|
}, it.art).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (res.ok) {
|
||||||
|
installiert.push(titel);
|
||||||
|
// war beim Teilenden abgeschaltet -> hier auch
|
||||||
|
if (it.aktiv === false && res.entry) store.setModEnabled(inst.id, res.entry.filename, false, it.art);
|
||||||
|
} else {
|
||||||
|
fehlgeschlagen.push({ titel, grund: res.message || 'unbekannt' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcast('instances:changed');
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
instanz: { id: inst.id, name: inst.name },
|
||||||
|
installiert: installiert.length,
|
||||||
|
fehlgeschlagen,
|
||||||
|
nichtUebertragbar: Array.isArray(m.nichtUebertragbar) ? m.nichtUebertragbar.slice(0, 50) : [],
|
||||||
|
von,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- Kurznachrichten ----------
|
||||||
|
// Ungelesene Nachrichten werden zusammen mit den Einladungen abgefragt. Neue
|
||||||
|
// erscheinen im Launcher als Hinweis und – wer gerade spielt – als Meldung im Spiel.
|
||||||
|
const gezeigteNachrichten = new Set();
|
||||||
|
let ungeleseneNachrichten = []; // des aktiven Kontos (für Zähler und Liste)
|
||||||
|
|
||||||
|
async function pruefeNachrichten() {
|
||||||
|
if (store.getSettings().friendsEnabled === false) return;
|
||||||
|
const aktiv = aktivesFreundeKonto();
|
||||||
|
const konten = new Set([...kontoPid.keys()]);
|
||||||
|
if (aktiv) konten.add(aktiv.uuid);
|
||||||
|
const spitznamen = store.getSettings().friendNicknames || {};
|
||||||
|
for (const kontoUuid of konten) {
|
||||||
|
const res = await friends.getUnreadMessages(kontoUuid);
|
||||||
|
if (!res.ok) continue;
|
||||||
|
const liste = res.data || [];
|
||||||
|
const neu = liste.filter((n) => !gezeigteNachrichten.has(n.id));
|
||||||
|
neu.forEach((n) => gezeigteNachrichten.add(n.id));
|
||||||
|
if (aktiv && aktiv.uuid === kontoUuid) {
|
||||||
|
const vorher = ungeleseneNachrichten.length;
|
||||||
|
ungeleseneNachrichten = liste;
|
||||||
|
if (neu.length > 2) broadcast('friends:message', { sammel: neu.length });
|
||||||
|
else neu.forEach((n) => broadcast('friends:message', n));
|
||||||
|
if (neu.length || vorher !== liste.length) aktualisiereAnfragenZahl().catch(() => {});
|
||||||
|
}
|
||||||
|
const pid = kontoPid.get(kontoUuid);
|
||||||
|
if (pid) {
|
||||||
|
for (const n of neu.slice(-3)) {
|
||||||
|
const name = spitznamen[n.fromUuid] || n.fromUsername || 'Ein Freund';
|
||||||
|
const inhalt = n.imageUrl ? `📷 Screenshot${n.text ? ' – ' + n.text : ''}` : n.text;
|
||||||
|
await zeigeIngameOverlay('Nachricht', [{ uuid: n.fromUuid, text: `${name}: ${inhalt}`, umbruch: true }], pid, 9000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('friends:messages', async (_e, otherUuid) => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein Konto angemeldet.' };
|
||||||
|
const res = await friends.getMessages(konto.uuid, otherUuid);
|
||||||
|
if (!res.ok) return res;
|
||||||
|
// geöffnet = gelesen
|
||||||
|
if (ungeleseneNachrichten.some((n) => n.fromUuid === otherUuid)) {
|
||||||
|
await friends.markMessagesRead(konto.uuid, otherUuid).catch(() => {});
|
||||||
|
ungeleseneNachrichten = ungeleseneNachrichten.filter((n) => n.fromUuid !== otherUuid);
|
||||||
|
aktualisiereAnfragenZahl().catch(() => {});
|
||||||
|
}
|
||||||
|
return { ok: true, list: res.data || [], ich: konto.uuid };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Screenshot verkleinern (höchstens 1600 px breit), als JPEG an einen Freund schicken
|
||||||
|
ipcMain.handle('friends:sendScreenshot', async (_e, otherUuid, instanceId, dateiname, text) => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein Konto angemeldet.' };
|
||||||
|
const datei = safeSub(instanceId, 'screenshots', String(dateiname || ''));
|
||||||
|
if (!datei || !fs.existsSync(datei)) return { ok: false, message: 'Screenshot nicht gefunden.' };
|
||||||
|
let bild = nativeImage.createFromPath(datei);
|
||||||
|
if (bild.isEmpty()) return { ok: false, message: 'Bild konnte nicht gelesen werden.' };
|
||||||
|
const { width } = bild.getSize();
|
||||||
|
if (width > 1600) bild = bild.resize({ width: 1600, quality: 'best' });
|
||||||
|
let jpeg = bild.toJPEG(85);
|
||||||
|
if (jpeg.length > 2000000) jpeg = bild.toJPEG(65);
|
||||||
|
const res = await friends.sendMessage(konto.uuid, otherUuid, String(text || '').trim().slice(0, 200), jpeg.toString('base64'));
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('friends:sendMessage', async (_e, otherUuid, text) => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein Konto angemeldet.' };
|
||||||
|
const t = String(text || '').trim().slice(0, 200);
|
||||||
|
if (!t) return { ok: false, message: 'Leere Nachricht.' };
|
||||||
|
return friends.sendMessage(konto.uuid, otherUuid, t);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ungelesene Nachrichten je Freund (für die Zahl am Sprechblasen-Knopf)
|
||||||
|
ipcMain.handle('friends:unreadByFriend', () => {
|
||||||
|
const zaehler = {};
|
||||||
|
for (const n of ungeleseneNachrichten) zaehler[n.fromUuid] = (zaehler[n.fromUuid] || 0) + 1;
|
||||||
|
return zaehler;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- Offene Anfragen + ungelesene Nachrichten (Zahl am "Freunde"-Knopf) ----------
|
||||||
|
let letzteAnfragenZahl = { anfragen: 0, nachrichten: 0 };
|
||||||
async function aktualisiereAnfragenZahl() {
|
async function aktualisiereAnfragenZahl() {
|
||||||
let zahl = 0;
|
let anfragen = 0;
|
||||||
const acc = auth.currentAccount();
|
const acc = auth.currentAccount();
|
||||||
if (store.getSettings().friendsEnabled !== false && acc && acc.profile) {
|
if (store.getSettings().friendsEnabled !== false && acc && acc.profile) {
|
||||||
const res = await friends.getRequests(acc.profile.id);
|
const res = await friends.getRequests(acc.profile.id);
|
||||||
if (!res.ok) return; // nicht erreichbar -> letzten bekannten Stand behalten
|
if (!res.ok) return; // nicht erreichbar -> letzten bekannten Stand behalten
|
||||||
zahl = (res.data || []).length;
|
anfragen = (res.data || []).length;
|
||||||
}
|
}
|
||||||
letzteAnfragenZahl = zahl;
|
const aktiv = aktivesFreundeKonto();
|
||||||
broadcast('friends:requestCount', zahl);
|
const nachrichten = aktiv && acc && acc.profile && acc.profile.id === aktiv.uuid ? ungeleseneNachrichten.length : 0;
|
||||||
|
letzteAnfragenZahl = { anfragen, nachrichten };
|
||||||
|
broadcast('friends:requestCount', letzteAnfragenZahl);
|
||||||
}
|
}
|
||||||
ipcMain.handle('friends:requestCount', () => letzteAnfragenZahl);
|
ipcMain.handle('friends:requestCount', () => letzteAnfragenZahl);
|
||||||
|
|
||||||
@@ -2085,6 +2563,22 @@ ipcMain.handle('instances:export', async (_e, id) => {
|
|||||||
* Backup-System, landet in einem eigenen Unterordner der Dokumente.
|
* Backup-System, landet in einem eigenen Unterordner der Dokumente.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('world:backup', async (_e, id, worldName) => {
|
ipcMain.handle('world:backup', async (_e, id, worldName) => {
|
||||||
|
const res = await sichereWelt(id, worldName, false);
|
||||||
|
if (res.ok) {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
notify('Welt gesichert', `${worldName} (${inst ? inst.name : ''}) wurde gesichert.`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
// automatische Sicherungen: so viele Stände je Welt bleiben erhalten
|
||||||
|
const AUTO_WELT_STAENDE = 5;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Eine Welt als ZIP sichern. automatisch = true: Unterordner "Automatisch",
|
||||||
|
* es bleiben nur die letzten AUTO_WELT_STAENDE Stände dieser Welt.
|
||||||
|
*/
|
||||||
|
async function sichereWelt(id, worldName, automatisch) {
|
||||||
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.' };
|
||||||
const worldDir = path.join(store.gameDir(id), 'saves', String(worldName || ''));
|
const worldDir = path.join(store.gameDir(id), 'saves', String(worldName || ''));
|
||||||
@@ -2093,8 +2587,9 @@ ipcMain.handle('world:backup', async (_e, id, worldName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const zielOrdner = path.join(app.getPath('documents'), 'AeroMC Launcher', 'Welten-Sicherungen',
|
let zielOrdner = path.join(app.getPath('documents'), 'AeroMC Launcher', 'Welten-Sicherungen',
|
||||||
(inst.name || 'Instanz').replace(/[^\w.-]+/g, '_'));
|
(inst.name || 'Instanz').replace(/[^\w.-]+/g, '_'));
|
||||||
|
if (automatisch) zielOrdner = path.join(zielOrdner, 'Automatisch');
|
||||||
fs.mkdirSync(zielOrdner, { recursive: true });
|
fs.mkdirSync(zielOrdner, { recursive: true });
|
||||||
const zeitstempel = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 16);
|
const zeitstempel = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 16);
|
||||||
const safeWorld = String(worldName).replace(/[^\w.-]+/g, '_');
|
const safeWorld = String(worldName).replace(/[^\w.-]+/g, '_');
|
||||||
@@ -2103,11 +2598,69 @@ ipcMain.handle('world:backup', async (_e, id, worldName) => {
|
|||||||
|
|
||||||
await runPwsh("Compress-Archive -Path (Join-Path $env:VN_SRC '*') -DestinationPath $env:VN_DST -Force",
|
await runPwsh("Compress-Archive -Path (Join-Path $env:VN_SRC '*') -DestinationPath $env:VN_DST -Force",
|
||||||
{ VN_SRC: worldDir, VN_DST: zielDatei });
|
{ VN_SRC: worldDir, VN_DST: zielDatei });
|
||||||
notify('Welt gesichert', `${worldName} (${inst.name}) wurde gesichert.`);
|
|
||||||
|
if (automatisch) {
|
||||||
|
// ältere automatische Stände dieser Welt wegräumen
|
||||||
|
const praefix = safeWorld + '_';
|
||||||
|
const staende = fs.readdirSync(zielOrdner)
|
||||||
|
.filter((n) => n.startsWith(praefix) && n.endsWith('.zip') && /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}/.test(n.slice(praefix.length)))
|
||||||
|
.sort();
|
||||||
|
for (const alt of staende.slice(0, Math.max(0, staende.length - AUTO_WELT_STAENDE))) {
|
||||||
|
try { fs.unlinkSync(path.join(zielOrdner, alt)); } catch { /* egal */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
return { ok: true, path: zielDatei };
|
return { ok: true, path: zielDatei };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, message: 'Sicherung fehlgeschlagen: ' + err.message };
|
return { ok: false, message: 'Sicherung fehlgeschlagen: ' + err.message };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nach einer Spielsitzung: die Welt sichern, die während der Sitzung zuletzt
|
||||||
|
* gespeichert wurde (level.dat neuer als der Spielstart). Läuft im Hintergrund.
|
||||||
|
*/
|
||||||
|
async function sichereZuletztGespielteWelt(id, sitzungStart) {
|
||||||
|
const savesDir = path.join(store.gameDir(id), 'saves');
|
||||||
|
let beste = null;
|
||||||
|
try {
|
||||||
|
for (const e of fs.readdirSync(savesDir, { withFileTypes: true })) {
|
||||||
|
if (!e.isDirectory()) continue;
|
||||||
|
const level = path.join(savesDir, e.name, 'level.dat');
|
||||||
|
try {
|
||||||
|
const zeit = fs.statSync(level).mtimeMs;
|
||||||
|
if (zeit >= sitzungStart - 5000 && (!beste || zeit > beste.zeit)) beste = { name: e.name, zeit };
|
||||||
|
} catch { /* keine Welt */ }
|
||||||
|
}
|
||||||
|
} catch { return; }
|
||||||
|
if (!beste) return; // nur Mehrspieler gespielt
|
||||||
|
const res = await sichereWelt(id, beste.name, true);
|
||||||
|
if (res.ok) broadcast('ui:toast', { text: `Welt „${beste.name}“ automatisch gesichert (die letzten ${AUTO_WELT_STAENDE} Stände bleiben erhalten).` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Welt in eine andere Instanz kopieren
|
||||||
|
ipcMain.handle('world:copyTo', async (_e, id, worldName, zielId) => {
|
||||||
|
const quelle = store.getInstance(id);
|
||||||
|
const ziel = store.getInstance(zielId);
|
||||||
|
if (!quelle || !ziel) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
if (id === zielId) return { ok: false, message: 'Quelle und Ziel sind dieselbe Instanz.' };
|
||||||
|
const savesQuelle = path.join(store.gameDir(id), 'saves');
|
||||||
|
const worldDir = path.join(savesQuelle, String(worldName || ''));
|
||||||
|
if (path.dirname(worldDir) !== savesQuelle || !fs.existsSync(worldDir)) return { ok: false, message: 'Welt nicht gefunden.' };
|
||||||
|
if (laufendePid.has(id)) return { ok: false, message: 'Bitte erst Minecraft in dieser Instanz beenden – sonst wird die Welt mitten beim Speichern kopiert.' };
|
||||||
|
|
||||||
|
const savesZiel = path.join(store.gameDir(zielId), 'saves');
|
||||||
|
fs.mkdirSync(savesZiel, { recursive: true });
|
||||||
|
let name = String(worldName);
|
||||||
|
for (let n = 2; fs.existsSync(path.join(savesZiel, name)); n++) name = `${worldName} (${n})`;
|
||||||
|
try {
|
||||||
|
await fs.promises.cp(worldDir, path.join(savesZiel, name), {
|
||||||
|
recursive: true,
|
||||||
|
filter: (src) => path.basename(src) !== 'session.lock',
|
||||||
|
});
|
||||||
|
return { ok: true, name, ziel: ziel.name };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Kopieren fehlgeschlagen: ' + err.message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Modpack-Import (Modrinth .mrpack) ----
|
// ---- Modpack-Import (Modrinth .mrpack) ----
|
||||||
@@ -2740,7 +3293,7 @@ function notify(title, body) {
|
|||||||
if (store.getSettings().desktopNotifications === false) return;
|
if (store.getSettings().desktopNotifications === false) return;
|
||||||
if (BrowserWindow.getAllWindows().some((w) => !w.isDestroyed() && w.isFocused())) return;
|
if (BrowserWindow.getAllWindows().some((w) => !w.isDestroyed() && w.isFocused())) return;
|
||||||
if (!Notification.isSupported()) return;
|
if (!Notification.isSupported()) return;
|
||||||
const n = new Notification({ title, body, silent: false });
|
const n = new Notification({ title, body, silent: false, icon: benachrichtigungsLogo || undefined });
|
||||||
n.on('click', () => {
|
n.on('click', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
@@ -2970,6 +3523,7 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
|||||||
let freundeHerzschlag = null;
|
let freundeHerzschlag = null;
|
||||||
let herzschlagJetzt = null; // sofort melden, z. B. sobald ein Server erkannt wurde
|
let herzschlagJetzt = null; // sofort melden, z. B. sobald ein Server erkannt wurde
|
||||||
|
|
||||||
|
letzteAusgabe.set(id, '');
|
||||||
try {
|
try {
|
||||||
let launchStart = 0;
|
let launchStart = 0;
|
||||||
const res = await launcher.prepareAndLaunch(opts, (evt) => {
|
const res = await launcher.prepareAndLaunch(opts, (evt) => {
|
||||||
@@ -3016,12 +3570,19 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
|||||||
aktualisiereFreundeTastenkuerzel();
|
aktualisiereFreundeTastenkuerzel();
|
||||||
if (launchStart) {
|
if (launchStart) {
|
||||||
const secs = Math.max(0, Math.round((Date.now() - launchStart) / 1000));
|
const secs = Math.max(0, Math.round((Date.now() - launchStart) / 1000));
|
||||||
|
const sitzungStart = launchStart;
|
||||||
launchStart = 0; // Spielzeit nur einmal gutschreiben
|
launchStart = 0; // Spielzeit nur einmal gutschreiben
|
||||||
|
if (store.getSettings().autoWorldBackup !== false) {
|
||||||
|
// kurz warten, bis Minecraft die Weltdateien sicher geschlossen hat
|
||||||
|
setTimeout(() => { sichereZuletztGespielteWelt(id, sitzungStart).catch(() => {}); }, 3000);
|
||||||
|
}
|
||||||
const cur = store.getInstance(id);
|
const cur = store.getInstance(id);
|
||||||
if (cur) store.updateInstance(id, { totalPlaySeconds: (cur.totalPlaySeconds || 0) + secs });
|
if (cur) store.updateInstance(id, { totalPlaySeconds: (cur.totalPlaySeconds || 0) + secs });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (evt.phase === 'log') {
|
if (evt.phase === 'log') {
|
||||||
|
const bisher = (letzteAusgabe.get(id) || '') + evt.line;
|
||||||
|
letzteAusgabe.set(id, bisher.length > AUSGABE_MAX ? bisher.slice(-AUSGABE_MAX) : bisher);
|
||||||
const m = VERBINDUNGS_ZEILE.exec(evt.line);
|
const m = VERBINDUNGS_ZEILE.exec(evt.line);
|
||||||
if (m) {
|
if (m) {
|
||||||
const server = m[2] === '25565' ? m[1] : `${m[1]}:${m[2]}`;
|
const server = m[2] === '25565' ? m[1] : `${m[1]}:${m[2]}`;
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
||||||
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
||||||
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
|
crashLatest: (id, seit) => ipcRenderer.invoke('crash:latest', id, seit),
|
||||||
|
crashAnalyze: (id, seit) => ipcRenderer.invoke('crash:analyze', id, seit),
|
||||||
|
crashFix: (id, aktion) => ipcRenderer.invoke('crash:fix', id, aktion),
|
||||||
|
crashRelaunch: (id) => ipcRenderer.invoke('crash:relaunch', id),
|
||||||
|
shareCreate: (id) => ipcRenderer.invoke('share:create', id),
|
||||||
|
shareFetch: (code) => ipcRenderer.invoke('share:fetch', code),
|
||||||
|
shareInstall: (code) => ipcRenderer.invoke('share:install', code),
|
||||||
|
onShareProgress: (cb) => {
|
||||||
|
const l = (_e, p) => cb(p);
|
||||||
|
ipcRenderer.on('share:progress', l);
|
||||||
|
return () => ipcRenderer.removeListener('share:progress', l);
|
||||||
|
},
|
||||||
onAutoBackup: (cb) => {
|
onAutoBackup: (cb) => {
|
||||||
const l = (_e, d) => cb(d);
|
const l = (_e, d) => cb(d);
|
||||||
ipcRenderer.on('backup:auto', l);
|
ipcRenderer.on('backup:auto', l);
|
||||||
@@ -85,6 +96,8 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
||||||
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
||||||
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
||||||
|
worldCopyTo: (id, worldName, zielId) => ipcRenderer.invoke('world:copyTo', id, worldName, zielId),
|
||||||
|
friendsSendScreenshot: (otherUuid, instanceId, datei, text) => ipcRenderer.invoke('friends:sendScreenshot', otherUuid, instanceId, datei, text),
|
||||||
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
||||||
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
||||||
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
||||||
@@ -299,6 +312,15 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
friendsCancel: (otherUuid) => ipcRenderer.invoke('friends:cancel', otherUuid),
|
||||||
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
friendsSetNickname: (uuid, nickname) => ipcRenderer.invoke('friends:setNickname', uuid, nickname),
|
||||||
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
friendsRequestCount: () => ipcRenderer.invoke('friends:requestCount'),
|
||||||
|
friendsMessages: (otherUuid) => ipcRenderer.invoke('friends:messages', otherUuid),
|
||||||
|
friendsSendMessage: (otherUuid, text) => ipcRenderer.invoke('friends:sendMessage', otherUuid, text),
|
||||||
|
friendsUnreadByFriend: () => ipcRenderer.invoke('friends:unreadByFriend'),
|
||||||
|
onFriendsMessage: (cb) => {
|
||||||
|
const l = (_e, n) => cb(n);
|
||||||
|
ipcRenderer.on('friends:message', l);
|
||||||
|
return () => ipcRenderer.removeListener('friends:message', l);
|
||||||
|
},
|
||||||
|
crashReport: (id, seit, wahl) => ipcRenderer.invoke('crash:report', id, seit, wahl),
|
||||||
clipSetActive: (active, instanceId) => ipcRenderer.invoke('clip:setActive', active, instanceId),
|
clipSetActive: (active, instanceId) => ipcRenderer.invoke('clip:setActive', active, instanceId),
|
||||||
clipSave: (data) => ipcRenderer.invoke('clip:save', data),
|
clipSave: (data) => ipcRenderer.invoke('clip:save', data),
|
||||||
onClipHotkey: (cb) => {
|
onClipHotkey: (cb) => {
|
||||||
|
|||||||
+487
-20
@@ -568,8 +568,8 @@ async function ensureDefaultMcScan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setImportKind(kind) {
|
function setImportKind(kind) {
|
||||||
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].includes(kind) ? kind : 'zip';
|
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
|
||||||
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].forEach((entryKind) => {
|
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].forEach((entryKind) => {
|
||||||
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
|
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
|
||||||
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
|
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
|
||||||
});
|
});
|
||||||
@@ -579,6 +579,8 @@ function setImportKind(kind) {
|
|||||||
el('cf-do').classList.toggle('hidden', importKind !== 'cf');
|
el('cf-do').classList.toggle('hidden', importKind !== 'cf');
|
||||||
el('mmc-do').classList.toggle('hidden', importKind !== 'mmc');
|
el('mmc-do').classList.toggle('hidden', importKind !== 'mmc');
|
||||||
el('technic-do').classList.toggle('hidden', importKind !== 'technic');
|
el('technic-do').classList.toggle('hidden', importKind !== 'technic');
|
||||||
|
el('code-do').classList.toggle('hidden', importKind !== 'code');
|
||||||
|
el('code-progress').classList.toggle('hidden', importKind !== 'code' || !el('code-progress').textContent);
|
||||||
el('mc-progress').classList.toggle('hidden', importKind !== 'mc' || !el('mc-progress').textContent);
|
el('mc-progress').classList.toggle('hidden', importKind !== 'mc' || !el('mc-progress').textContent);
|
||||||
el('cf-progress').classList.toggle('hidden', importKind !== 'cf' || !el('cf-progress').textContent);
|
el('cf-progress').classList.toggle('hidden', importKind !== 'cf' || !el('cf-progress').textContent);
|
||||||
el('mmc-progress').classList.toggle('hidden', importKind !== 'mmc' || !el('mmc-progress').textContent);
|
el('mmc-progress').classList.toggle('hidden', importKind !== 'mmc' || !el('mmc-progress').textContent);
|
||||||
@@ -1225,6 +1227,94 @@ async function renderBedrockList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Auswahl-Dialog ----------
|
||||||
|
// waehleAus({ titel, text, optionen: [{ wert, label, sub, bild, aus }], textfeld, okLabel })
|
||||||
|
// -> Promise<{ wert, text } | null>
|
||||||
|
let wahlAufloesen = null;
|
||||||
|
let wahlGewaehlt = null;
|
||||||
|
|
||||||
|
function waehleAus({ titel, text, optionen, textfeld, okLabel }) {
|
||||||
|
if (wahlAufloesen) wahlAufloesen(null);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
wahlAufloesen = resolve;
|
||||||
|
wahlGewaehlt = null;
|
||||||
|
el('wahl-titel').textContent = titel;
|
||||||
|
el('wahl-text').textContent = text || '';
|
||||||
|
el('wahl-text').classList.toggle('hidden', !text);
|
||||||
|
const liste = el('wahl-liste');
|
||||||
|
liste.innerHTML = '';
|
||||||
|
for (const o of optionen) {
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.className = 'wahl-option';
|
||||||
|
b.disabled = !!o.aus;
|
||||||
|
b.innerHTML = `${o.bild ? `<img class="wahl-bild" src="${escapeHtml(o.bild)}" alt="" />` : ''}`
|
||||||
|
+ `<span class="wahl-label"><b>${escapeHtml(o.label)}</b>${o.sub ? `<small>${escapeHtml(o.sub)}</small>` : ''}</span>`;
|
||||||
|
const bild = b.querySelector('img');
|
||||||
|
if (bild) bild.addEventListener('error', () => bild.remove());
|
||||||
|
b.addEventListener('click', () => {
|
||||||
|
wahlGewaehlt = o.wert;
|
||||||
|
liste.querySelectorAll('.wahl-option').forEach((x) => x.classList.toggle('an', x === b));
|
||||||
|
el('wahl-ok').disabled = false;
|
||||||
|
});
|
||||||
|
b.addEventListener('dblclick', () => { wahlGewaehlt = o.wert; schliesseWahl(true); });
|
||||||
|
liste.appendChild(b);
|
||||||
|
}
|
||||||
|
el('wahl-eingabe-feld').classList.toggle('hidden', !textfeld);
|
||||||
|
el('wahl-eingabe').value = '';
|
||||||
|
if (textfeld) {
|
||||||
|
el('wahl-eingabe-label').textContent = textfeld.label || 'Text';
|
||||||
|
el('wahl-eingabe').placeholder = textfeld.placeholder || '';
|
||||||
|
}
|
||||||
|
el('wahl-ok').textContent = okLabel || 'OK';
|
||||||
|
el('wahl-ok').disabled = true;
|
||||||
|
show('modal-wahl');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function schliesseWahl(bestaetigt) {
|
||||||
|
const aufloesen = wahlAufloesen;
|
||||||
|
wahlAufloesen = null;
|
||||||
|
hide('modal-wahl');
|
||||||
|
if (aufloesen) aufloesen(bestaetigt && wahlGewaehlt !== null ? { wert: wahlGewaehlt, text: el('wahl-eingabe').value.trim() } : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Schnellstart-Tasten (Strg+1 … Strg+5) ----------
|
||||||
|
let schnellstartBelegung = {};
|
||||||
|
|
||||||
|
function schnellstartTasteVon(id) {
|
||||||
|
return Object.keys(schnellstartBelegung).find((t) => schnellstartBelegung[t] === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setzeSchnellstart(id) {
|
||||||
|
const inst = instances.find((i) => i.id === id);
|
||||||
|
if (!inst) return;
|
||||||
|
const s = await window.api.getSettings();
|
||||||
|
const belegung = Object.assign({}, s.schnellstart || {});
|
||||||
|
const aktuell = Object.keys(belegung).find((t) => belegung[t] === id);
|
||||||
|
const optionen = ['1', '2', '3', '4', '5'].map((t) => {
|
||||||
|
const belegt = belegung[t] && belegung[t] !== id ? instances.find((i) => i.id === belegung[t]) : null;
|
||||||
|
return {
|
||||||
|
wert: t,
|
||||||
|
label: `Strg+${t}`,
|
||||||
|
sub: belegung[t] === id ? 'aktuell diese Instanz' : (belegt ? `ersetzt „${belegt.name}“` : 'frei'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (aktuell) optionen.push({ wert: '0', label: 'Keine Taste', sub: 'Schnellstart-Taste entfernen' });
|
||||||
|
const wahl = await waehleAus({
|
||||||
|
titel: `Schnellstart für „${inst.name}“`,
|
||||||
|
text: 'Mit der Taste startest du diese Instanz direkt aus dem Launcher-Fenster. Im Infobereich (Rechtsklick auf das Symbol neben der Uhr) steht sie ganz oben.',
|
||||||
|
optionen,
|
||||||
|
okLabel: 'Übernehmen',
|
||||||
|
});
|
||||||
|
if (!wahl) return;
|
||||||
|
for (const t of Object.keys(belegung)) if (belegung[t] === id) delete belegung[t];
|
||||||
|
if (wahl.wert !== '0') belegung[wahl.wert] = id;
|
||||||
|
await window.api.saveSettings({ schnellstart: belegung });
|
||||||
|
schnellstartBelegung = belegung;
|
||||||
|
render();
|
||||||
|
toast(wahl.wert === '0' ? 'Schnellstart-Taste entfernt.' : `„${inst.name}“ startet jetzt mit Strg+${wahl.wert}.`);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Kontextmenü (Rechtsklick auf eine Instanz) ----------
|
// ---------- Kontextmenü (Rechtsklick auf eine Instanz) ----------
|
||||||
function openContextMenu(x, y) {
|
function openContextMenu(x, y) {
|
||||||
const menu = el('ctx-menu');
|
const menu = el('ctx-menu');
|
||||||
@@ -1688,12 +1778,15 @@ function buildCard(inst) {
|
|||||||
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
|
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
|
||||||
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
|
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
|
||||||
let tip = inst.name + '\n' + (inst.minecraft.version || 'ohne Version') + loaderTxt;
|
let tip = inst.name + '\n' + (inst.minecraft.version || 'ohne Version') + loaderTxt;
|
||||||
|
const taste = schnellstartTasteVon(inst.id);
|
||||||
|
if (taste) tip += `\nSchnellstart: Strg+${taste}`;
|
||||||
if (inst.notes && inst.notes.trim()) tip += '\n\n' + inst.notes.trim();
|
if (inst.notes && inst.notes.trim()) tip += '\n\n' + inst.notes.trim();
|
||||||
card.title = tip;
|
card.title = tip;
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
${iconHtml}
|
${iconHtml}
|
||||||
<div class="card-name">${escapeHtml(inst.name)}</div>`;
|
<div class="card-name">${escapeHtml(inst.name)}</div>
|
||||||
|
${taste ? `<span class="card-taste">Strg+${taste}</span>` : ''}`;
|
||||||
|
|
||||||
// einfacher Klick = auswählen (MultiMC), Doppelklick = starten
|
// einfacher Klick = auswählen (MultiMC), Doppelklick = starten
|
||||||
card.addEventListener('click', (e) => {
|
card.addEventListener('click', (e) => {
|
||||||
@@ -2597,23 +2690,98 @@ async function ladeLog(name) {
|
|||||||
|
|
||||||
let letzterStartMs = 0;
|
let letzterStartMs = 0;
|
||||||
let aktuellerCrash = null;
|
let aktuellerCrash = null;
|
||||||
|
let aktuellerCrashId = null;
|
||||||
|
let aktuellerCrashSeit = 0;
|
||||||
|
|
||||||
async function zeigeAbsturzbericht(instanzId) {
|
const CRASH_AKTION_LABEL = {
|
||||||
|
'mod-installieren': (a) => `„${a.name}“ installieren`,
|
||||||
|
'mod-deaktivieren': (a) => `„${a.name}“ abschalten`,
|
||||||
|
ram: (a) => `RAM auf ${Math.round(a.mb / 102.4) / 10} GB stellen`,
|
||||||
|
'java-auto': () => 'Passendes Java wählen',
|
||||||
|
'mods-ordner': () => 'Mods-Ordner öffnen',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Funde des Absturz-Assistenten als Karten mit Lösungs-Knopf
|
||||||
|
function zeigeAbsturzHilfe(id, funde) {
|
||||||
|
const box = el('crash-hilfe');
|
||||||
|
box.innerHTML = '';
|
||||||
|
if (!funde.length) {
|
||||||
|
box.innerHTML = '<div class="crash-unklar">AeroMC konnte die Ursache nicht eindeutig erkennen. Die Angaben unten helfen beim Suchen – oder beim Nachfragen, z. B. im Forum des betroffenen Mods.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const titel = document.createElement('div');
|
||||||
|
titel.className = 'crash-hilfe-titel';
|
||||||
|
titel.textContent = funde.length === 1 ? 'Das ist wahrscheinlich passiert' : `Das ist wahrscheinlich passiert (${funde.length} Punkte)`;
|
||||||
|
box.appendChild(titel);
|
||||||
|
for (const f of funde) {
|
||||||
|
const karte = document.createElement('div');
|
||||||
|
karte.className = 'crash-fund';
|
||||||
|
karte.innerHTML = `<div class="crash-fund-text"><b>${escapeHtml(f.titel)}</b><span>${escapeHtml(f.text)}</span></div>`;
|
||||||
|
const label = f.aktion && CRASH_AKTION_LABEL[f.aktion.typ];
|
||||||
|
if (label) {
|
||||||
|
const knopf = document.createElement('button');
|
||||||
|
knopf.className = 'btn btn-mini btn-primary';
|
||||||
|
knopf.textContent = label(f.aktion);
|
||||||
|
knopf.addEventListener('click', async () => {
|
||||||
|
knopf.disabled = true;
|
||||||
|
const vorher = knopf.textContent;
|
||||||
|
knopf.textContent = 'Einen Moment …';
|
||||||
|
const r = await window.api.crashFix(id, f.aktion).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (r.ok) {
|
||||||
|
knopf.textContent = '✓ Erledigt';
|
||||||
|
karte.classList.add('erledigt');
|
||||||
|
toast(r.text || 'Erledigt.');
|
||||||
|
if (f.aktion.typ !== 'mods-ordner') el('crash-retry').classList.remove('hidden');
|
||||||
|
else knopf.disabled = false;
|
||||||
|
} else {
|
||||||
|
knopf.textContent = vorher;
|
||||||
|
knopf.disabled = false;
|
||||||
|
toast(r.message || 'Das hat nicht geklappt.', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
karte.appendChild(knopf);
|
||||||
|
}
|
||||||
|
box.appendChild(karte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zeigeAbsturzbericht(instanzId, seit) {
|
||||||
const id = instanzId || currentDetailId;
|
const id = instanzId || currentDetailId;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (POPOUT !== 'crash') { window.api.popoutOpen('crash', { instanceId: id }); return; }
|
const startMs = seit !== undefined ? seit : letzterStartMs;
|
||||||
|
if (POPOUT !== 'crash') { window.api.popoutOpen('crash', { instanceId: id, seit: startMs }); return; }
|
||||||
// Minecraft schreibt den Bericht erst kurz nach dem Ende
|
// Minecraft schreibt den Bericht erst kurz nach dem Ende
|
||||||
await new Promise((r) => setTimeout(r, 900));
|
await new Promise((r) => setTimeout(r, 900));
|
||||||
|
|
||||||
let res;
|
// Absturz-Assistent: Bericht + Spielprotokoll + Konsolenausgabe auswerten
|
||||||
try { res = await window.api.crashLatest(id, letzterStartMs); }
|
let analyse;
|
||||||
|
try { analyse = await window.api.crashAnalyze(id, startMs); }
|
||||||
catch { return; }
|
catch { return; }
|
||||||
if (!res || !res.ok) return; // kein Bericht -> Konsole muss reichen
|
if (!analyse || !analyse.ok) return;
|
||||||
|
const res = analyse.bericht;
|
||||||
|
// weder Bericht noch erkannte Ursache -> die Konsole muss reichen
|
||||||
|
if (!res && !analyse.funde.length) return;
|
||||||
|
|
||||||
aktuellerCrash = res;
|
aktuellerCrash = res || { text: '' };
|
||||||
|
aktuellerCrashId = id;
|
||||||
|
aktuellerCrashSeit = startMs;
|
||||||
|
el('crash-send').disabled = false;
|
||||||
|
el('crash-send').textContent = 'An M_Viper senden';
|
||||||
const inst = await window.api.getInstance(id);
|
const inst = await window.api.getInstance(id);
|
||||||
el('crash-sub').textContent = `${inst ? inst.name + ' · ' : ''}${res.datei} · `
|
el('crash-sub').textContent = res
|
||||||
+ new Date(res.zeitpunkt).toLocaleString('de-DE');
|
? `${inst ? inst.name + ' · ' : ''}${res.datei} · ` + new Date(res.zeitpunkt).toLocaleString('de-DE')
|
||||||
|
: `${inst ? inst.name + ' · ' : ''}ausgewertet aus dem Spielprotokoll`;
|
||||||
|
zeigeAbsturzHilfe(id, analyse.funde);
|
||||||
|
el('crash-retry').classList.add('hidden');
|
||||||
|
el('crash-details').classList.toggle('hidden', !res);
|
||||||
|
el('crash-kern').classList.toggle('hidden', !res);
|
||||||
|
el('crash-copy').classList.toggle('hidden', !res);
|
||||||
|
if (!res) {
|
||||||
|
el('crash-kern').innerHTML = '';
|
||||||
|
el('crash-voll').textContent = '';
|
||||||
|
show('modal-crash');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const zeile = (titel, wert) => wert
|
const zeile = (titel, wert) => wert
|
||||||
? `<div class="crash-zeile"><div class="crash-titel">${titel}</div><div class="crash-wert">${escapeHtml(wert)}</div></div>`
|
? `<div class="crash-zeile"><div class="crash-titel">${titel}</div><div class="crash-wert">${escapeHtml(wert)}</div></div>`
|
||||||
@@ -3562,6 +3730,7 @@ async function openScreens() {
|
|||||||
thumb.innerHTML =
|
thumb.innerHTML =
|
||||||
`<input type="checkbox" class="screen-check" title="Auswählen" />
|
`<input type="checkbox" class="screen-check" title="Auswählen" />
|
||||||
<img alt="" /><div class="screen-name">${escapeHtml(s.name)}</div>
|
<img alt="" /><div class="screen-name">${escapeHtml(s.name)}</div>
|
||||||
|
<button class="screen-del screen-teilen" title="An einen Freund schicken">📤</button>
|
||||||
<button class="screen-del" title="Löschen">🗑</button>`;
|
<button class="screen-del" title="Löschen">🗑</button>`;
|
||||||
const img = thumb.querySelector('img');
|
const img = thumb.querySelector('img');
|
||||||
window.api.screenshotData(currentDetailId, s.name).then((d) => { if (d) img.src = d; });
|
window.api.screenshotData(currentDetailId, s.name).then((d) => { if (d) img.src = d; });
|
||||||
@@ -3583,7 +3752,11 @@ async function openScreens() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
window.api.openScreenshot(currentDetailId, s.name);
|
window.api.openScreenshot(currentDetailId, s.name);
|
||||||
});
|
});
|
||||||
thumb.querySelector('.screen-del').addEventListener('click', (e) => {
|
thumb.querySelector('.screen-teilen').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
schickeScreenshot(s.name);
|
||||||
|
});
|
||||||
|
thumb.querySelector('.screen-del:not(.screen-teilen)').addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
askConfirm('Screenshot löschen', `„${s.name}" löschen?`, '🗑 Löschen', async () => {
|
askConfirm('Screenshot löschen', `„${s.name}" löschen?`, '🗑 Löschen', async () => {
|
||||||
await window.api.deleteContent(currentDetailId, 'screenshots', s.name);
|
await window.api.deleteContent(currentDetailId, 'screenshots', s.name);
|
||||||
@@ -3595,6 +3768,35 @@ async function openScreens() {
|
|||||||
updateScreensSelectionUi();
|
updateScreensSelectionUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Screenshot an einen Freund schicken – er sieht ihn im Chat
|
||||||
|
async function schickeScreenshot(dateiname) {
|
||||||
|
const res = await window.api.friendsList().catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (!res.ok) { toast(res.message || 'Freundesliste nicht erreichbar.', true); return; }
|
||||||
|
if (!res.list.length) { toast('Du hast noch keine Freunde im Freundesystem.', true); return; }
|
||||||
|
const s = await window.api.getSettings();
|
||||||
|
const spitznamen = s.friendNicknames || {};
|
||||||
|
const optionen = [...res.list]
|
||||||
|
.sort((a, b) => (a.online === b.online ? String(a.username).localeCompare(String(b.username)) : (a.online ? -1 : 1)))
|
||||||
|
.map((f) => ({
|
||||||
|
wert: f.uuid,
|
||||||
|
label: spitznamen[f.uuid] || f.username || f.uuid,
|
||||||
|
sub: f.online ? 'online' : 'offline – sieht es beim nächsten Mal',
|
||||||
|
bild: `https://crafthead.net/cube/${f.uuid}/64`,
|
||||||
|
}));
|
||||||
|
const wahl = await waehleAus({
|
||||||
|
titel: 'Screenshot schicken',
|
||||||
|
text: `„${dateiname}“ an wen?`,
|
||||||
|
optionen,
|
||||||
|
textfeld: { label: 'Nachricht dazu (optional)', placeholder: 'z. B. „Schau mal, mein neues Haus!“' },
|
||||||
|
okLabel: 'Schicken',
|
||||||
|
});
|
||||||
|
if (!wahl) return;
|
||||||
|
toast('Screenshot wird geschickt …');
|
||||||
|
const r = await window.api.friendsSendScreenshot(wahl.wert, currentDetailId, dateiname, wahl.text).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (r.ok) toast('Screenshot verschickt.');
|
||||||
|
else toast(r.message || 'Verschicken fehlgeschlagen.', true);
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteSelectedScreenshots() {
|
async function deleteSelectedScreenshots() {
|
||||||
const names = [...screenSelected];
|
const names = [...screenSelected];
|
||||||
if (!names.length || !currentDetailId) return;
|
if (!names.length || !currentDetailId) return;
|
||||||
@@ -3725,6 +3927,80 @@ async function deleteSelectedRecords() {
|
|||||||
// ---------- Freunde ----------
|
// ---------- Freunde ----------
|
||||||
let friendsAutoRefresh = null;
|
let friendsAutoRefresh = null;
|
||||||
|
|
||||||
|
// ---------- Instanz per Code teilen ----------
|
||||||
|
async function teileInstanz() {
|
||||||
|
if (!currentDetailId) return;
|
||||||
|
const inst = await window.api.getInstance(currentDetailId);
|
||||||
|
el('share-title').textContent = inst ? `„${inst.name}“ teilen` : 'Instanz teilen';
|
||||||
|
el('share-status').textContent = 'Code wird erstellt …';
|
||||||
|
el('share-status').classList.remove('hidden');
|
||||||
|
el('share-result').classList.add('hidden');
|
||||||
|
show('modal-share');
|
||||||
|
const res = await window.api.shareCreate(currentDetailId).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (!res.ok) { el('share-status').textContent = '✖ ' + (res.message || 'Teilen fehlgeschlagen.'); return; }
|
||||||
|
el('share-status').classList.add('hidden');
|
||||||
|
el('share-result').classList.remove('hidden');
|
||||||
|
el('share-code').textContent = res.code;
|
||||||
|
el('share-info').textContent = res.anzahl
|
||||||
|
? `${res.anzahl} Mod${res.anzahl === 1 ? '' : 's'}, Ressourcenpakete und Shader werden in ihren genauen Versionen übertragen, dazu Minecraft-Version und Loader. Der Code ist ${res.tage} Tage gültig.`
|
||||||
|
: `Übertragen werden Minecraft-Version und Loader. Der Code ist ${res.tage} Tage gültig.`;
|
||||||
|
const nicht = res.nichtUebertragbar || [];
|
||||||
|
el('share-warn').classList.toggle('hidden', !nicht.length);
|
||||||
|
if (nicht.length) {
|
||||||
|
el('share-warn').textContent = `Nicht dabei, weil von Hand hinzugefügt: ${nicht.slice(0, 5).join(', ')}${nicht.length > 5 ? ` und ${nicht.length - 5} weitere` : ''}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let codeVorschau = null;
|
||||||
|
|
||||||
|
async function pruefeTeilCode() {
|
||||||
|
const code = el('code-input').value.trim();
|
||||||
|
codeVorschau = null;
|
||||||
|
el('code-do').disabled = true;
|
||||||
|
el('code-preview').classList.add('hidden');
|
||||||
|
if (!code) { el('code-status').textContent = 'Bitte einen Code eingeben.'; return; }
|
||||||
|
el('code-status').textContent = 'Suche …';
|
||||||
|
const res = await window.api.shareFetch(code).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (!res.ok) { el('code-status').textContent = '✖ ' + (res.message || 'Code nicht gefunden.'); return; }
|
||||||
|
codeVorschau = res.data;
|
||||||
|
const m = res.data.manifest;
|
||||||
|
const zaehle = (art) => m.inhalte.filter((it) => it && it.art === art).length;
|
||||||
|
const teile = [[zaehle('mod'), 'Mod', 'Mods'], [zaehle('resourcepack'), 'Ressourcenpaket', 'Ressourcenpakete'], [zaehle('shader'), 'Shader', 'Shader']]
|
||||||
|
.filter(([n]) => n).map(([n, e, mz]) => `${n} ${n === 1 ? e : mz}`);
|
||||||
|
const loader = m.minecraft.loader && m.minecraft.loader !== 'vanilla' ? (LOADER_LABEL[m.minecraft.loader] || m.minecraft.loader) : 'Vanilla';
|
||||||
|
const cf = m.inhalte.some((it) => String(it.projectId || '').startsWith('cf-'));
|
||||||
|
const nicht = Array.isArray(m.nichtUebertragbar) ? m.nichtUebertragbar.length : 0;
|
||||||
|
el('code-preview').innerHTML = `<b>${escapeHtml(m.name || 'Geteilte Instanz')}</b>
|
||||||
|
<p class="hint">Minecraft ${escapeHtml(m.minecraft.version)} · ${escapeHtml(loader)}${res.data.fromUsername ? ' · geteilt von ' + escapeHtml(res.data.fromUsername) : ''}</p>
|
||||||
|
<p class="hint">${teile.length ? escapeHtml(teile.join(', ')) : 'Keine Mods – nur Version und Loader'}</p>
|
||||||
|
${cf ? '<p class="hint">Enthält Inhalte von CurseForge – dafür braucht es einen CurseForge-Zugang (Optionen → Netzwerk).</p>' : ''}
|
||||||
|
${nicht ? `<p class="hint">${nicht} Datei${nicht === 1 ? '' : 'en'} hatte der Teilende von Hand hinzugefügt – die sind nicht dabei.</p>` : ''}`;
|
||||||
|
el('code-preview').classList.remove('hidden');
|
||||||
|
el('code-status').textContent = '';
|
||||||
|
el('code-do').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installiereTeilCode() {
|
||||||
|
if (!codeVorschau) return;
|
||||||
|
el('code-do').disabled = true;
|
||||||
|
el('code-check').disabled = true;
|
||||||
|
el('code-progress').textContent = 'Instanz wird angelegt …';
|
||||||
|
el('code-progress').classList.remove('hidden');
|
||||||
|
const res = await window.api.shareInstall(codeVorschau.code).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
el('code-check').disabled = false;
|
||||||
|
el('code-progress').textContent = '';
|
||||||
|
el('code-progress').classList.add('hidden');
|
||||||
|
if (!res.ok) { toast(res.message || 'Anlegen fehlgeschlagen.', true); el('code-do').disabled = false; return; }
|
||||||
|
await reload();
|
||||||
|
hide('modal-import');
|
||||||
|
const fehl = res.fehlgeschlagen || [];
|
||||||
|
toast(`„${res.instanz.name}“ angelegt – ${res.installiert} Inhalt${res.installiert === 1 ? '' : 'e'} installiert`
|
||||||
|
+ (fehl.length ? ` · nicht verfügbar: ${fehl.slice(0, 3).map((f) => f.titel).join(', ')}${fehl.length > 3 ? ' …' : ''}` : '') + '.', fehl.length > 0);
|
||||||
|
codeVorschau = null;
|
||||||
|
el('code-input').value = '';
|
||||||
|
el('code-preview').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Spielzeit-Statistik ----------
|
// ---------- Spielzeit-Statistik ----------
|
||||||
// Gesamtwerte als Kennzahlen, darunter eine nach Spielzeit sortierte Balkenliste
|
// Gesamtwerte als Kennzahlen, darunter eine nach Spielzeit sortierte Balkenliste
|
||||||
// (eine Reihe = eine Instanz, Balkenlänge relativ zur meistgespielten).
|
// (eine Reihe = eine Instanz, Balkenlänge relativ zur meistgespielten).
|
||||||
@@ -3780,10 +4056,14 @@ async function ladeStatistik() {
|
|||||||
|
|
||||||
async function openFriends() {
|
async function openFriends() {
|
||||||
if (POPOUT !== 'friends') { window.api.popoutOpen('friends'); return; }
|
if (POPOUT !== 'friends') { window.api.popoutOpen('friends'); return; }
|
||||||
|
// beim (Wieder-)Öffnen immer mit der Liste beginnen
|
||||||
|
chatPartner = null;
|
||||||
|
el('friends-chat').classList.add('hidden');
|
||||||
|
el('friends-content').classList.remove('hidden');
|
||||||
show('modal-friends');
|
show('modal-friends');
|
||||||
await ladeFreunde();
|
await ladeFreunde();
|
||||||
clearInterval(friendsAutoRefresh);
|
clearInterval(friendsAutoRefresh);
|
||||||
friendsAutoRefresh = setInterval(ladeFreunde, 30000);
|
friendsAutoRefresh = setInterval(() => (chatPartner ? ladeChat() : ladeFreunde()), chatPartner ? 10000 : 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ladeFreunde() {
|
async function ladeFreunde() {
|
||||||
@@ -3792,13 +4072,14 @@ async function ladeFreunde() {
|
|||||||
el('friends-content').classList.toggle('hidden', !aktiv);
|
el('friends-content').classList.toggle('hidden', !aktiv);
|
||||||
if (!aktiv) return;
|
if (!aktiv) return;
|
||||||
|
|
||||||
const [anfragenRes, gesendetRes, freundeRes, einstellungen, einladungenRes, meinServer] = await Promise.all([
|
const [anfragenRes, gesendetRes, freundeRes, einstellungen, einladungenRes, meinServer, ungelesen] = await Promise.all([
|
||||||
window.api.friendsRequests(),
|
window.api.friendsRequests(),
|
||||||
window.api.friendsSentRequests(),
|
window.api.friendsSentRequests(),
|
||||||
window.api.friendsList(),
|
window.api.friendsList(),
|
||||||
window.api.getSettings(),
|
window.api.getSettings(),
|
||||||
window.api.friendsInvites(),
|
window.api.friendsInvites(),
|
||||||
window.api.friendsMyServer(),
|
window.api.friendsMyServer(),
|
||||||
|
window.api.friendsUnreadByFriend().catch(() => ({})),
|
||||||
]);
|
]);
|
||||||
const spitznamen = einstellungen.friendNicknames || {};
|
const spitznamen = einstellungen.friendNicknames || {};
|
||||||
|
|
||||||
@@ -3899,10 +4180,12 @@ async function ladeFreunde() {
|
|||||||
<div class="friend-info"><div class="friend-name">${escapeHtml(anzeigeName)}</div><div class="friend-status">${escapeHtml(status)}</div></div>
|
<div class="friend-info"><div class="friend-name">${escapeHtml(anzeigeName)}</div><div class="friend-status">${escapeHtml(status)}</div></div>
|
||||||
${kannBeitreten ? '<button class="btn btn-mini btn-primary" data-act="join" title="Passende Instanz starten und diesem Server beitreten">Beitreten</button>' : ''}
|
${kannBeitreten ? '<button class="btn btn-mini btn-primary" data-act="join" title="Passende Instanz starten und diesem Server beitreten">Beitreten</button>' : ''}
|
||||||
${kannEinladen ? `<button class="btn btn-mini" data-act="invite" title="Auf ${escapeHtml(meinServer.server)} einladen">Einladen</button>` : ''}
|
${kannEinladen ? `<button class="btn btn-mini" data-act="invite" title="Auf ${escapeHtml(meinServer.server)} einladen">Einladen</button>` : ''}
|
||||||
|
<button class="btn btn-mini chat-btn" data-act="chat" title="Nachricht schreiben">💬${ungelesen[f.uuid] ? `<span class="chat-badge">${ungelesen[f.uuid]}</span>` : ''}</button>
|
||||||
<button class="btn btn-mini" data-act="nick" title="Spitzname">✎</button>
|
<button class="btn btn-mini" data-act="nick" title="Spitzname">✎</button>
|
||||||
<button class="btn btn-mini btn-danger" data-act="remove" title="Entfernen">🗑</button>`;
|
<button class="btn btn-mini btn-danger" data-act="remove" title="Entfernen">🗑</button>`;
|
||||||
row.querySelector('.friend-avatar').addEventListener('error', function () { this.remove(); });
|
row.querySelector('.friend-avatar').addEventListener('error', function () { this.remove(); });
|
||||||
if (kannBeitreten) row.querySelector('[data-act="join"]').addEventListener('click', () => freundBeitreten(f.server, f.version));
|
if (kannBeitreten) row.querySelector('[data-act="join"]').addEventListener('click', () => freundBeitreten(f.server, f.version));
|
||||||
|
row.querySelector('[data-act="chat"]').addEventListener('click', () => oeffneChat(f.uuid, anzeigeName));
|
||||||
if (kannEinladen) {
|
if (kannEinladen) {
|
||||||
row.querySelector('[data-act="invite"]').addEventListener('click', async (ev) => {
|
row.querySelector('[data-act="invite"]').addEventListener('click', async (ev) => {
|
||||||
const btn = ev.currentTarget;
|
const btn = ev.currentTarget;
|
||||||
@@ -3968,6 +4251,71 @@ async function freundBeitreten(server, version, einladungId) {
|
|||||||
toast(res.message || 'Beitreten fehlgeschlagen.', true);
|
toast(res.message || 'Beitreten fehlgeschlagen.', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Kurznachrichten ----------
|
||||||
|
let chatPartner = null; // { uuid, name } – solange die Unterhaltung offen ist
|
||||||
|
|
||||||
|
function oeffneChat(uuid, name) {
|
||||||
|
chatPartner = { uuid, name };
|
||||||
|
el('friends-content').classList.add('hidden');
|
||||||
|
el('friends-chat').classList.remove('hidden');
|
||||||
|
el('chat-name').textContent = name;
|
||||||
|
el('chat-avatar').src = `https://crafthead.net/cube/${encodeURIComponent(uuid)}/64`;
|
||||||
|
el('chat-verlauf').innerHTML = '<div class="chat-leer">Lade …</div>';
|
||||||
|
el('chat-text').value = '';
|
||||||
|
ladeChat();
|
||||||
|
el('chat-text').focus();
|
||||||
|
clearInterval(friendsAutoRefresh);
|
||||||
|
friendsAutoRefresh = setInterval(ladeChat, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function schliesseChat() {
|
||||||
|
chatPartner = null;
|
||||||
|
el('friends-chat').classList.add('hidden');
|
||||||
|
el('friends-content').classList.remove('hidden');
|
||||||
|
ladeFreunde();
|
||||||
|
clearInterval(friendsAutoRefresh);
|
||||||
|
friendsAutoRefresh = setInterval(ladeFreunde, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ladeChat() {
|
||||||
|
if (!chatPartner) return;
|
||||||
|
const partner = chatPartner;
|
||||||
|
const res = await window.api.friendsMessages(partner.uuid).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (chatPartner !== partner) return; // inzwischen gewechselt
|
||||||
|
const box = el('chat-verlauf');
|
||||||
|
if (!res.ok) { box.innerHTML = `<div class="chat-leer">${escapeHtml(res.message || 'Nicht erreichbar.')}</div>`; return; }
|
||||||
|
const unten = box.scrollHeight - box.scrollTop - box.clientHeight < 40;
|
||||||
|
if (!res.list.length) {
|
||||||
|
box.innerHTML = `<div class="chat-leer">Noch keine Nachrichten. Schreib ${escapeHtml(partner.name)} etwas – er sieht es im Launcher oder als Meldung im Spiel.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = res.list.map((n) => {
|
||||||
|
const zeit = new Date(String(n.createdAt).replace(' ', 'T') + 'Z');
|
||||||
|
const zeitText = isNaN(zeit) ? '' : zeit.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
|
const bild = n.imageUrl && /^https:\/\//.test(n.imageUrl)
|
||||||
|
? `<img class="chat-bild" src="${escapeHtml(n.imageUrl)}" alt="Screenshot" data-url="${escapeHtml(n.imageUrl)}" title="Groß öffnen" />` : '';
|
||||||
|
return `<div class="chat-blase${n.fromUuid === res.ich ? ' ich' : ''}">${bild}${escapeHtml(n.text)}<span class="chat-zeit">${zeitText}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
box.querySelectorAll('.chat-bild').forEach((img) => {
|
||||||
|
img.addEventListener('click', () => window.api.openExternal(img.dataset.url));
|
||||||
|
img.addEventListener('load', () => { if (unten) box.scrollTop = box.scrollHeight; });
|
||||||
|
});
|
||||||
|
if (unten || box.dataset.erstesMal !== partner.uuid) box.scrollTop = box.scrollHeight;
|
||||||
|
box.dataset.erstesMal = partner.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendeChat(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!chatPartner || !t) return;
|
||||||
|
el('chat-send').disabled = true;
|
||||||
|
const res = await window.api.friendsSendMessage(chatPartner.uuid, t).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
el('chat-send').disabled = false;
|
||||||
|
if (!res.ok) { toast(res.message || 'Nachricht konnte nicht gesendet werden.', true); return; }
|
||||||
|
el('chat-text').value = '';
|
||||||
|
await ladeChat();
|
||||||
|
el('chat-verlauf').scrollTop = el('chat-verlauf').scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendeFreundschaftsanfrage() {
|
async function sendeFreundschaftsanfrage() {
|
||||||
const input = el('friends-add-input');
|
const input = el('friends-add-input');
|
||||||
const name = input.value.trim();
|
const name = input.value.trim();
|
||||||
@@ -3982,6 +4330,36 @@ async function sendeFreundschaftsanfrage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Welt in eine andere Instanz kopieren – Ziel aus allen anderen Instanzen wählen
|
||||||
|
async function kopiereWelt(weltName) {
|
||||||
|
const quelle = await window.api.getInstance(currentDetailId);
|
||||||
|
const alle = (await window.api.listInstances()).filter((i) => i.id !== currentDetailId);
|
||||||
|
if (!alle.length) { toast('Es gibt keine andere Instanz, in die kopiert werden könnte.', true); return; }
|
||||||
|
const vergleiche = (a, b) => String(a).localeCompare(String(b), undefined, { numeric: true });
|
||||||
|
const optionen = alle
|
||||||
|
.sort((a, b) => String(b.lastPlayed || '').localeCompare(String(a.lastPlayed || '')))
|
||||||
|
.map((i) => {
|
||||||
|
const v = i.minecraft.version || '?';
|
||||||
|
const aelter = quelle && quelle.minecraft.version && i.minecraft.version && vergleiche(i.minecraft.version, quelle.minecraft.version) < 0;
|
||||||
|
return {
|
||||||
|
wert: i.id,
|
||||||
|
label: i.name,
|
||||||
|
sub: `Minecraft ${v}${aelter ? ' – älter als die Welt, sie könnte dort Probleme machen' : ''}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const wahl = await waehleAus({
|
||||||
|
titel: `„${weltName}“ kopieren`,
|
||||||
|
text: 'In welche Instanz soll die Welt kopiert werden? Das Original bleibt unverändert.',
|
||||||
|
optionen,
|
||||||
|
okLabel: 'Kopieren',
|
||||||
|
});
|
||||||
|
if (!wahl) return;
|
||||||
|
toast('Welt wird kopiert …');
|
||||||
|
const res = await window.api.worldCopyTo(currentDetailId, weltName, wahl.wert).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (res.ok) toast(`„${weltName}“ nach „${res.ziel}“ kopiert${res.name !== weltName ? ` (als „${res.name}“)` : ''}.`);
|
||||||
|
else toast(res.message || 'Kopieren fehlgeschlagen.', true);
|
||||||
|
}
|
||||||
|
|
||||||
function renderContentList(boxId, items, sub, emoji) {
|
function renderContentList(boxId, items, sub, emoji) {
|
||||||
const box = el(boxId);
|
const box = el(boxId);
|
||||||
box.innerHTML = '';
|
box.innerHTML = '';
|
||||||
@@ -4010,6 +4388,13 @@ function renderContentList(boxId, items, sub, emoji) {
|
|||||||
else toast(res.message || 'Sicherung fehlgeschlagen.', true);
|
else toast(res.message || 'Sicherung fehlgeschlagen.', true);
|
||||||
});
|
});
|
||||||
aktionen.appendChild(sichern);
|
aktionen.appendChild(sichern);
|
||||||
|
|
||||||
|
const kopieren = document.createElement('button');
|
||||||
|
kopieren.className = 'btn btn-mini';
|
||||||
|
kopieren.textContent = 'Kopieren nach …';
|
||||||
|
kopieren.title = 'Diese Welt in eine andere Instanz übernehmen';
|
||||||
|
kopieren.addEventListener('click', () => kopiereWelt(it.name));
|
||||||
|
aktionen.appendChild(kopieren);
|
||||||
}
|
}
|
||||||
const rm = document.createElement('button');
|
const rm = document.createElement('button');
|
||||||
rm.className = 'btn btn-danger btn-mini';
|
rm.className = 'btn btn-danger btn-mini';
|
||||||
@@ -4437,6 +4822,7 @@ async function openSettings(pane) {
|
|||||||
el('s-autobackup-every').value = s.autoBackupEvery || 'daily';
|
el('s-autobackup-every').value = s.autoBackupEvery || 'daily';
|
||||||
el('s-autobackup-keep').value = s.autoBackupKeep || 5;
|
el('s-autobackup-keep').value = s.autoBackupKeep || 5;
|
||||||
el('s-autobackup-launch').checked = !!s.autoBackupBeforeLaunch;
|
el('s-autobackup-launch').checked = !!s.autoBackupBeforeLaunch;
|
||||||
|
el('s-auto-world').checked = s.autoWorldBackup !== false;
|
||||||
el('s-autobackup-last').textContent = s.autoBackupLast
|
el('s-autobackup-last').textContent = s.autoBackupLast
|
||||||
? 'Zuletzt automatisch gesichert: ' + new Date(s.autoBackupLast).toLocaleString('de-DE')
|
? 'Zuletzt automatisch gesichert: ' + new Date(s.autoBackupLast).toLocaleString('de-DE')
|
||||||
: 'Noch keine automatische Sicherung.';
|
: 'Noch keine automatische Sicherung.';
|
||||||
@@ -4568,6 +4954,7 @@ async function saveSettings() {
|
|||||||
autoBackupEvery: el('s-autobackup-every').value,
|
autoBackupEvery: el('s-autobackup-every').value,
|
||||||
autoBackupKeep: Math.min(50, Math.max(1, numOrNull(el('s-autobackup-keep').value) || 5)),
|
autoBackupKeep: Math.min(50, Math.max(1, numOrNull(el('s-autobackup-keep').value) || 5)),
|
||||||
autoBackupBeforeLaunch: el('s-autobackup-launch').checked,
|
autoBackupBeforeLaunch: el('s-autobackup-launch').checked,
|
||||||
|
autoWorldBackup: el('s-auto-world').checked,
|
||||||
showConsoleOnLaunch: el('s-console-launch').checked,
|
showConsoleOnLaunch: el('s-console-launch').checked,
|
||||||
autoCloseConsole: el('s-console-autoclose').checked,
|
autoCloseConsole: el('s-console-autoclose').checked,
|
||||||
showConsoleOnCrash: el('s-console-crash').checked,
|
showConsoleOnCrash: el('s-console-crash').checked,
|
||||||
@@ -5065,11 +5452,16 @@ function wire() {
|
|||||||
// Kontextmenü: Einträge lösen die Aktionen der rechten Leiste aus
|
// Kontextmenü: Einträge lösen die Aktionen der rechten Leiste aus
|
||||||
document.querySelectorAll('#ctx-menu .ctx-item').forEach((item) => {
|
document.querySelectorAll('#ctx-menu .ctx-item').forEach((item) => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
const target = el(item.dataset.act);
|
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
|
if (item.dataset.act === 'ctx-schnellstart') { if (currentDetailId) setzeSchnellstart(currentDetailId); return; }
|
||||||
|
const target = el(item.dataset.act);
|
||||||
if (target) target.click();
|
if (target) target.click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Auswahl-Dialog
|
||||||
|
el('wahl-ok').addEventListener('click', () => schliesseWahl(true));
|
||||||
|
el('wahl-eingabe').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !el('wahl-ok').disabled) schliesseWahl(true); });
|
||||||
|
el('modal-wahl').querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => schliesseWahl(false)));
|
||||||
// schließen bei Klick daneben, Rechtsklick ins Leere, Scrollen oder ESC
|
// schließen bei Klick daneben, Rechtsklick ins Leere, Scrollen oder ESC
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!e.target.closest('#ctx-menu')) closeContextMenu();
|
if (!e.target.closest('#ctx-menu')) closeContextMenu();
|
||||||
@@ -5220,7 +5612,26 @@ function wire() {
|
|||||||
// Freunde
|
// Freunde
|
||||||
el('btn-friends').addEventListener('click', openFriends);
|
el('btn-friends').addEventListener('click', openFriends);
|
||||||
el('btn-stats').addEventListener('click', openStats);
|
el('btn-stats').addEventListener('click', openStats);
|
||||||
|
el('p-share').addEventListener('click', teileInstanz);
|
||||||
|
el('share-copy').addEventListener('click', async () => {
|
||||||
|
try { await navigator.clipboard.writeText(el('share-code').textContent); toast('Code kopiert.'); }
|
||||||
|
catch { toast('Kopieren fehlgeschlagen.', true); }
|
||||||
|
});
|
||||||
|
el('code-check').addEventListener('click', pruefeTeilCode);
|
||||||
|
el('code-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') pruefeTeilCode(); });
|
||||||
|
el('code-do').addEventListener('click', installiereTeilCode);
|
||||||
|
window.api.onShareProgress((p) => {
|
||||||
|
if (!el('code-progress')) return;
|
||||||
|
el('code-progress').textContent = `Installiere ${p.aktuell}/${p.gesamt}: ${p.titel}`;
|
||||||
|
el('code-progress').classList.toggle('hidden', importKind !== 'code');
|
||||||
|
});
|
||||||
el('friends-add-btn').addEventListener('click', sendeFreundschaftsanfrage);
|
el('friends-add-btn').addEventListener('click', sendeFreundschaftsanfrage);
|
||||||
|
el('chat-back').addEventListener('click', schliesseChat);
|
||||||
|
el('chat-send').addEventListener('click', () => sendeChat(el('chat-text').value));
|
||||||
|
el('chat-text').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendeChat(el('chat-text').value); });
|
||||||
|
document.querySelectorAll('.chat-vorlage').forEach((b) => b.addEventListener('click', () => sendeChat(b.textContent)));
|
||||||
|
el('chat-avatar').addEventListener('error', function () { this.style.visibility = 'hidden'; });
|
||||||
|
el('chat-avatar').addEventListener('load', function () { this.style.visibility = ''; });
|
||||||
el('friends-add-input').addEventListener('keydown', (e) => {
|
el('friends-add-input').addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') sendeFreundschaftsanfrage();
|
if (e.key === 'Enter') sendeFreundschaftsanfrage();
|
||||||
});
|
});
|
||||||
@@ -5518,6 +5929,38 @@ function wire() {
|
|||||||
if (!ok) toast('Ordner mit den Berichten nicht gefunden.', true);
|
if (!ok) toast('Ordner mit den Berichten nicht gefunden.', true);
|
||||||
});
|
});
|
||||||
el('crash-console').addEventListener('click', () => { hide('modal-crash'); openConsole(); });
|
el('crash-console').addEventListener('click', () => { hide('modal-crash'); openConsole(); });
|
||||||
|
el('crash-send').addEventListener('click', () => {
|
||||||
|
el('report-kommentar').value = '';
|
||||||
|
el('report-name').checked = false;
|
||||||
|
el('report-do').disabled = false;
|
||||||
|
el('report-do').textContent = 'Senden';
|
||||||
|
show('modal-report');
|
||||||
|
el('report-kommentar').focus();
|
||||||
|
});
|
||||||
|
el('report-do').addEventListener('click', async () => {
|
||||||
|
if (!aktuellerCrashId) return;
|
||||||
|
el('report-do').disabled = true;
|
||||||
|
el('report-do').textContent = 'Wird gesendet …';
|
||||||
|
const res = await window.api.crashReport(aktuellerCrashId, aktuellerCrashSeit, {
|
||||||
|
kommentar: el('report-kommentar').value,
|
||||||
|
mitName: el('report-name').checked,
|
||||||
|
}).catch((err) => ({ ok: false, message: err.message }));
|
||||||
|
if (res.ok) {
|
||||||
|
hide('modal-report');
|
||||||
|
toast(`Danke! Dein Bericht${res.id ? ` (#${res.id})` : ''} ist bei M_Viper angekommen.`);
|
||||||
|
el('crash-send').disabled = true;
|
||||||
|
el('crash-send').textContent = '✓ Gesendet';
|
||||||
|
} else {
|
||||||
|
el('report-do').disabled = false;
|
||||||
|
el('report-do').textContent = 'Senden';
|
||||||
|
toast(res.message || 'Senden fehlgeschlagen.', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el('crash-retry').addEventListener('click', async () => {
|
||||||
|
if (!aktuellerCrashId) return;
|
||||||
|
await window.api.crashRelaunch(aktuellerCrashId);
|
||||||
|
hide('modal-crash');
|
||||||
|
});
|
||||||
|
|
||||||
el('m-add-file').addEventListener('click', async () => {
|
el('m-add-file').addEventListener('click', async () => {
|
||||||
const res = await window.api.modAddFile(currentDetailId, modArt);
|
const res = await window.api.modAddFile(currentDetailId, modArt);
|
||||||
@@ -5702,6 +6145,15 @@ function wire() {
|
|||||||
el('search').select();
|
el('search').select();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Strg+1 … Strg+5: Schnellstart-Instanz starten (nur im Hauptfenster)
|
||||||
|
if (!POPOUT && strg && !e.shiftKey && !e.altKey && /^[1-5]$/.test(e.key) && !modalOffen && !inEingabe) {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = schnellstartBelegung[e.key];
|
||||||
|
const inst = id && instances.find((i) => i.id === id);
|
||||||
|
if (inst) { selectInstance(inst.id); launch(inst.id); }
|
||||||
|
else toast(`Strg+${e.key} ist noch keiner Instanz zugewiesen – Rechtsklick auf eine Instanz → „Schnellstart-Taste …“.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Strg+N: neue Instanz anlegen
|
// Strg+N: neue Instanz anlegen
|
||||||
if (strg && e.key.toLowerCase() === 'n' && !modalOffen) {
|
if (strg && e.key.toLowerCase() === 'n' && !modalOffen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -5782,7 +6234,7 @@ const POPOUT_OPENERS = {
|
|||||||
settings: (p) => openSettings(p.pane || new URLSearchParams(location.search).get('pane') || undefined),
|
settings: (p) => openSettings(p.pane || new URLSearchParams(location.search).get('pane') || undefined),
|
||||||
console: () => openConsole(),
|
console: () => openConsole(),
|
||||||
mods: () => openMods(),
|
mods: () => openMods(),
|
||||||
crash: () => zeigeAbsturzbericht(currentDetailId),
|
crash: (p) => zeigeAbsturzbericht(currentDetailId, Number(p.seit) || 0),
|
||||||
logs: () => openLogs(),
|
logs: () => openLogs(),
|
||||||
content: () => openContent(),
|
content: () => openContent(),
|
||||||
screens: () => openScreens(),
|
screens: () => openScreens(),
|
||||||
@@ -5855,22 +6307,34 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
iso3dEnabled = s.iso3dIcons !== false;
|
iso3dEnabled = s.iso3dIcons !== false;
|
||||||
consoleMaxChars = Math.max(50000, (s.consoleMaxLines || 100000) * 3);
|
consoleMaxChars = Math.max(50000, (s.consoleMaxLines || 100000) * 3);
|
||||||
el('btn-friends').classList.toggle('hidden', s.friendsEnabled === false);
|
el('btn-friends').classList.toggle('hidden', s.friendsEnabled === false);
|
||||||
|
schnellstartBelegung = s.schnellstart || {};
|
||||||
} catch { applyTheme('dark'); }
|
} catch { applyTheme('dark'); }
|
||||||
// Zahl offener Freundschaftsanfragen am "Freunde"-Knopf (nur Hauptfenster
|
// Zahl offener Freundschaftsanfragen am "Freunde"-Knopf (nur Hauptfenster
|
||||||
// zeigt die Werkzeugleiste)
|
// zeigt die Werkzeugleiste)
|
||||||
if (!POPOUT) {
|
if (!POPOUT) {
|
||||||
const zeigeAnfragenZahl = (zahl) => {
|
const zeigeAnfragenZahl = (info) => {
|
||||||
|
const anfragen = typeof info === 'number' ? info : ((info && info.anfragen) || 0);
|
||||||
|
const nachrichten = typeof info === 'number' ? 0 : ((info && info.nachrichten) || 0);
|
||||||
|
const zahl = anfragen + nachrichten;
|
||||||
const badge = el('friends-badge');
|
const badge = el('friends-badge');
|
||||||
badge.textContent = zahl > 99 ? '99+' : String(zahl);
|
badge.textContent = zahl > 99 ? '99+' : String(zahl);
|
||||||
badge.classList.toggle('hidden', !zahl);
|
badge.classList.toggle('hidden', !zahl);
|
||||||
el('btn-friends').title = zahl
|
const teile = [];
|
||||||
? `Freunde – ${zahl} offene Anfrage${zahl === 1 ? '' : 'n'} (ingame: Strg+Alt+F zeigt, wer online ist)`
|
if (anfragen) teile.push(`${anfragen} offene Anfrage${anfragen === 1 ? '' : 'n'}`);
|
||||||
: 'Freunde (ingame: Strg+Alt+F zeigt, wer online ist)';
|
if (nachrichten) teile.push(`${nachrichten} ungelesene Nachricht${nachrichten === 1 ? '' : 'en'}`);
|
||||||
|
el('btn-friends').title = (teile.length ? `Freunde – ${teile.join(', ')}` : 'Freunde') + ' (ingame: Strg+Alt+F zeigt, wer online ist)';
|
||||||
};
|
};
|
||||||
window.api.onFriendsRequestCount(zeigeAnfragenZahl);
|
window.api.onFriendsRequestCount(zeigeAnfragenZahl);
|
||||||
window.api.friendsRequestCount().then(zeigeAnfragenZahl).catch(() => {});
|
window.api.friendsRequestCount().then(zeigeAnfragenZahl).catch(() => {});
|
||||||
// Meldungen aus dem Hauptprozess, deren Fenster schon zu ist (z. B. Performance-Paket)
|
// Meldungen aus dem Hauptprozess, deren Fenster schon zu ist (z. B. Performance-Paket)
|
||||||
window.api.onUiToast((t) => toast(t.text, !!t.error));
|
window.api.onUiToast((t) => toast(t.text, !!t.error));
|
||||||
|
// neue Kurznachricht (im Spiel kommt zusätzlich eine Meldung)
|
||||||
|
window.api.onFriendsMessage((n) => {
|
||||||
|
if (n.sammel) { toast(`💬 ${n.sammel} neue Nachrichten – im Freunde-Fenster lesen.`); return; }
|
||||||
|
toast(n.imageUrl
|
||||||
|
? `📷 ${n.fromUsername || 'Ein Freund'} hat dir einen Screenshot geschickt${n.text ? ': ' + n.text : ''}`
|
||||||
|
: `💬 ${n.fromUsername || 'Ein Freund'}: ${n.text}`);
|
||||||
|
});
|
||||||
// Einladung "Komm auf meinen Server" (im Spiel kommt zusätzlich ein Toast)
|
// Einladung "Komm auf meinen Server" (im Spiel kommt zusätzlich ein Toast)
|
||||||
window.api.onFriendsInvite((inv) => {
|
window.api.onFriendsInvite((inv) => {
|
||||||
toast(`${inv.fromUsername || 'Ein Freund'} lädt dich auf ${inv.server} ein – im Freunde-Fenster beitreten.`);
|
toast(`${inv.fromUsername || 'Ein Freund'} lädt dich auf ${inv.server} ein – im Freunde-Fenster beitreten.`);
|
||||||
@@ -5878,6 +6342,8 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
}
|
}
|
||||||
// offenes Freunde-Fenster bei neuer Einladung gleich aktualisieren
|
// offenes Freunde-Fenster bei neuer Einladung gleich aktualisieren
|
||||||
if (POPOUT === 'friends') window.api.onFriendsInvite(() => ladeFreunde());
|
if (POPOUT === 'friends') window.api.onFriendsInvite(() => ladeFreunde());
|
||||||
|
// neue Nachricht: offene Unterhaltung oder Liste (Zähler) aktualisieren
|
||||||
|
if (POPOUT === 'friends') window.api.onFriendsMessage(() => (chatPartner ? ladeChat() : ladeFreunde()));
|
||||||
await refreshSkinBackground();
|
await refreshSkinBackground();
|
||||||
window.api.onLaunchProgress(onLaunchEvt);
|
window.api.onLaunchProgress(onLaunchEvt);
|
||||||
// Servername live in den Titel nachtragen, falls das Aufzeichnungs-Fenster
|
// Servername live in den Titel nachtragen, falls das Aufzeichnungs-Fenster
|
||||||
@@ -5921,6 +6387,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
window.api.onSettingsChanged((s) => {
|
window.api.onSettingsChanged((s) => {
|
||||||
applyTheme(s.theme);
|
applyTheme(s.theme);
|
||||||
iso3dEnabled = s.iso3dIcons !== false;
|
iso3dEnabled = s.iso3dIcons !== false;
|
||||||
|
schnellstartBelegung = s.schnellstart || {};
|
||||||
if (!POPOUT) {
|
if (!POPOUT) {
|
||||||
applyBedrockVisibility();
|
applyBedrockVisibility();
|
||||||
refreshSkinBackground();
|
refreshSkinBackground();
|
||||||
|
|||||||
+25
-3
@@ -283,8 +283,30 @@ async function modrinthSearch({ query = '', gameVersion = '', loader = '', categ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findet die passende Datei-Version für Spielversion + Loader
|
// findet die passende Datei-Version für Spielversion + Loader
|
||||||
async function modrinthResolveFile({ projectId, gameVersion, loader, art = 'mod' }) {
|
async function modrinthResolveFile({ projectId, gameVersion, loader, art = 'mod', versionId = '' }) {
|
||||||
const typ = inhaltsArt(art);
|
const typ = inhaltsArt(art);
|
||||||
|
// bestimmte Version gewünscht (z. B. geteilte Instanz) – sonst neueste passende
|
||||||
|
if (versionId) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(MODRINTH + '/version/' + encodeURIComponent(versionId), { headers: { 'User-Agent': UA } });
|
||||||
|
if (res.ok) {
|
||||||
|
const v = await res.json();
|
||||||
|
const passt = !gameVersion || (v.game_versions || []).includes(gameVersion);
|
||||||
|
const file = (v.files || []).find((f) => f.primary) || (v.files || [])[0];
|
||||||
|
if (passt && file && v.project_id === projectId) {
|
||||||
|
return {
|
||||||
|
versionId: v.id,
|
||||||
|
versionNumber: v.version_number,
|
||||||
|
filename: file.filename,
|
||||||
|
url: file.url,
|
||||||
|
sha1: file.hashes && file.hashes.sha1,
|
||||||
|
size: file.size,
|
||||||
|
dependencies: v.dependencies || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* dann eben die neueste passende */ }
|
||||||
|
}
|
||||||
const url = new URL(MODRINTH + '/project/' + projectId + '/version');
|
const url = new URL(MODRINTH + '/project/' + projectId + '/version');
|
||||||
if (gameVersion) url.searchParams.set('game_versions', JSON.stringify([gameVersion]));
|
if (gameVersion) url.searchParams.set('game_versions', JSON.stringify([gameVersion]));
|
||||||
// Nur Mods werden nach Loader eingeordnet; bei Ressourcenpaketen und
|
// Nur Mods werden nach Loader eingeordnet; bei Ressourcenpaketen und
|
||||||
@@ -351,9 +373,9 @@ async function downloadFile(url, destPath, extraHeaders) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// lädt eine Mod in <modsDir>; gibt Metadaten für die Instanz zurück
|
// lädt eine Mod in <modsDir>; gibt Metadaten für die Instanz zurück
|
||||||
async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir, art = 'mod', versionId = '' }) {
|
||||||
const typ = inhaltsArt(art);
|
const typ = inhaltsArt(art);
|
||||||
const file = await modrinthResolveFile({ projectId, gameVersion, loader, art });
|
const file = await modrinthResolveFile({ projectId, gameVersion, loader, art, versionId });
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return { ok: false, reason: 'no-match',
|
return { ok: false, reason: 'no-match',
|
||||||
message: typ.loaderNoetig
|
message: typ.loaderNoetig
|
||||||
|
|||||||
+3
-1
@@ -78,7 +78,9 @@ const DEFAULT_SETTINGS = {
|
|||||||
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
||||||
friendsEnabled: true,
|
friendsEnabled: true,
|
||||||
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
||||||
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren
|
||||||
|
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
||||||
|
autoWorldBackup: true, // nach jeder Spielsitzung die zuletzt gespielte Welt sichern (letzte 5 Stände) // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
||||||
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
||||||
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
autostart: false, // mit Windows starten, im Infobereich statt als Fenster
|
||||||
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID
|
||||||
|
|||||||
@@ -427,6 +427,61 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
.clip-bar.aktiv .clip-status{color:var(--text)}
|
.clip-bar.aktiv .clip-status{color:var(--text)}
|
||||||
.clip-bar.aktiv .clip-status::before{content:"";display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:var(--danger);vertical-align:1px;animation:rec-pulse 1.6s infinite}
|
.clip-bar.aktiv .clip-status::before{content:"";display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:var(--danger);vertical-align:1px;animation:rec-pulse 1.6s infinite}
|
||||||
|
|
||||||
|
/* ---- Absturz-Assistent ---- */
|
||||||
|
.crash-hilfe{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
||||||
|
.crash-hilfe-titel{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em}
|
||||||
|
.crash-fund{display:flex;gap:12px;align-items:flex-start;padding:10px 12px;border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:4px;background:var(--area)}
|
||||||
|
.crash-fund-text{flex:1;min-width:0}
|
||||||
|
.crash-fund-text b{display:block;font-size:13px;margin-bottom:3px}
|
||||||
|
.crash-fund-text span{font-size:12px;color:var(--dim);line-height:1.45}
|
||||||
|
.crash-fund .btn{flex:0 0 auto;align-self:center}
|
||||||
|
.crash-fund.erledigt{border-left-color:var(--play)}
|
||||||
|
.crash-unklar{padding:10px 12px;border:1px dashed var(--border);border-radius:4px;font-size:12px;color:var(--dim)}
|
||||||
|
|
||||||
|
/* ---- Instanz teilen ---- */
|
||||||
|
.share-code-box{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border:1px solid var(--accent);border-radius:4px;background:var(--area);margin:10px 0}
|
||||||
|
.share-code{font-family:var(--mono);font-size:24px;font-weight:600;letter-spacing:.12em;user-select:all}
|
||||||
|
.share-warn{color:var(--danger)}
|
||||||
|
.share-code-input{text-transform:uppercase;letter-spacing:.1em;font-family:var(--mono)}
|
||||||
|
.share-preview{margin-top:12px;padding:12px 14px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||||
|
.share-preview b{font-size:14px}
|
||||||
|
.share-preview .hint{margin:4px 0 0}
|
||||||
|
|
||||||
|
/* ---- Kurznachrichten ---- */
|
||||||
|
.friends-chat{display:flex;flex-direction:column;gap:8px;height:100%;min-height:340px}
|
||||||
|
.chat-head{display:flex;align-items:center;gap:10px}
|
||||||
|
.chat-head b{font-size:13px}
|
||||||
|
.chat-verlauf{flex:1;min-height:200px;max-height:300px;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--border);border-radius:4px;background:var(--area)}
|
||||||
|
.popout-host .chat-verlauf{max-height:none}
|
||||||
|
.chat-leer{margin:auto;font-size:12px;color:var(--dim);text-align:center}
|
||||||
|
.chat-blase{max-width:78%;padding:6px 10px;border-radius:10px;font-size:12.5px;line-height:1.4;word-break:break-word;background:var(--win);border:1px solid var(--border);align-self:flex-start}
|
||||||
|
.chat-blase.ich{align-self:flex-end;background:var(--accent);border-color:var(--accent);color:#fff}
|
||||||
|
.chat-zeit{display:block;font-size:10px;opacity:.65;margin-top:2px}
|
||||||
|
.chat-schnell{display:flex;flex-wrap:wrap;gap:6px}
|
||||||
|
.chat-btn{position:relative}
|
||||||
|
.chat-badge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:#e5484d;color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
|
||||||
|
.report-liste{margin:6px 0 10px;padding-left:20px;font-size:12px;line-height:1.6}
|
||||||
|
|
||||||
|
/* ---- Auswahl-Dialog ---- */
|
||||||
|
.wahl-liste{display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto}
|
||||||
|
.popout-host .wahl-liste{max-height:none}
|
||||||
|
.wahl-option{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:4px;background:var(--area);color:var(--text);font-family:inherit;text-align:left;cursor:pointer}
|
||||||
|
.wahl-option:hover{border-color:var(--accent)}
|
||||||
|
.wahl-option.an{border-color:var(--accent);background:var(--sel)}
|
||||||
|
.wahl-option:disabled{opacity:.5;cursor:default}
|
||||||
|
.wahl-bild{width:26px;height:26px;flex:0 0 auto}
|
||||||
|
.wahl-label{display:flex;flex-direction:column;min-width:0}
|
||||||
|
.wahl-label b{font-size:13px;font-weight:600}
|
||||||
|
.wahl-label small{font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
|
||||||
|
/* ---- Schnellstart-Taste auf der Kachel ---- */
|
||||||
|
.card{position:relative}
|
||||||
|
.card-taste{position:absolute;top:4px;right:4px;padding:1px 5px;border-radius:3px;background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:14px}
|
||||||
|
|
||||||
|
/* ---- Bilder im Chat ---- */
|
||||||
|
.chat-bild{display:block;max-width:100%;max-height:180px;border-radius:6px;margin-bottom:3px;cursor:zoom-in}
|
||||||
|
.screen-thumb .screen-teilen{right:32px;background:var(--accent)}
|
||||||
|
|
||||||
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
|
.friends-section-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.03em;margin:10px 0 6px}
|
||||||
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
|
.friend-row{display:flex;align-items:center;gap:10px;padding:6px 4px;border-radius:3px}
|
||||||
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
|
.friend-row:hover{background:var(--sel,rgba(0,0,0,.04))}
|
||||||
@@ -531,6 +586,7 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
und soll hier nicht greifen (sonst z. B. Version/Icon im Instanz-Fenster
|
und soll hier nicht greifen (sonst z. B. Version/Icon im Instanz-Fenster
|
||||||
untereinander statt nebeneinander) */
|
untereinander statt nebeneinander) */
|
||||||
.is-popout .field-row{flex-direction:row}
|
.is-popout .field-row{flex-direction:row}
|
||||||
|
.is-popout .path-row{flex-direction:row}
|
||||||
|
|
||||||
/* Konto-Button mit 3D-Skin-Kopf */
|
/* Konto-Button mit 3D-Skin-Kopf */
|
||||||
.acct-btn{gap:8px}
|
.acct-btn{gap:8px}
|
||||||
|
|||||||
Reference in New Issue
Block a user