Upload via GUI (93 Dateien)

This commit is contained in:
2026-10-02 19:39:47 +00:00
parent 0595b7ab1a
commit 41c63ef0ed
9 changed files with 2486 additions and 887 deletions
+487 -20
View File
@@ -568,8 +568,8 @@ async function ensureDefaultMcScan() {
}
function setImportKind(kind) {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].forEach((entryKind) => {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].forEach((entryKind) => {
el(`import-tab-${entryKind}`).classList.toggle('on', 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('mmc-do').classList.toggle('hidden', importKind !== 'mmc');
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('cf-progress').classList.toggle('hidden', importKind !== 'cf' || !el('cf-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) ----------
function openContextMenu(x, y) {
const menu = el('ctx-menu');
@@ -1688,12 +1778,15 @@ function buildCard(inst) {
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
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();
card.title = tip;
card.innerHTML = `
${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
card.addEventListener('click', (e) => {
@@ -2597,23 +2690,98 @@ async function ladeLog(name) {
let letzterStartMs = 0;
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;
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
await new Promise((r) => setTimeout(r, 900));
let res;
try { res = await window.api.crashLatest(id, letzterStartMs); }
// Absturz-Assistent: Bericht + Spielprotokoll + Konsolenausgabe auswerten
let analyse;
try { analyse = await window.api.crashAnalyze(id, startMs); }
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);
el('crash-sub').textContent = `${inst ? inst.name + ' · ' : ''}${res.datei} · `
+ new Date(res.zeitpunkt).toLocaleString('de-DE');
el('crash-sub').textContent = res
? `${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
? `<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 =
`<input type="checkbox" class="screen-check" title="Auswählen" />
<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>`;
const img = thumb.querySelector('img');
window.api.screenshotData(currentDetailId, s.name).then((d) => { if (d) img.src = d; });
@@ -3583,7 +3752,11 @@ async function openScreens() {
e.stopPropagation();
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();
askConfirm('Screenshot löschen', `„${s.name}" löschen?`, '🗑 Löschen', async () => {
await window.api.deleteContent(currentDetailId, 'screenshots', s.name);
@@ -3595,6 +3768,35 @@ async function openScreens() {
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() {
const names = [...screenSelected];
if (!names.length || !currentDetailId) return;
@@ -3725,6 +3927,80 @@ async function deleteSelectedRecords() {
// ---------- Freunde ----------
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 ----------
// Gesamtwerte als Kennzahlen, darunter eine nach Spielzeit sortierte Balkenliste
// (eine Reihe = eine Instanz, Balkenlänge relativ zur meistgespielten).
@@ -3780,10 +4056,14 @@ async function ladeStatistik() {
async function openFriends() {
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');
await ladeFreunde();
clearInterval(friendsAutoRefresh);
friendsAutoRefresh = setInterval(ladeFreunde, 30000);
friendsAutoRefresh = setInterval(() => (chatPartner ? ladeChat() : ladeFreunde()), chatPartner ? 10000 : 30000);
}
async function ladeFreunde() {
@@ -3792,13 +4072,14 @@ async function ladeFreunde() {
el('friends-content').classList.toggle('hidden', !aktiv);
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.friendsSentRequests(),
window.api.friendsList(),
window.api.getSettings(),
window.api.friendsInvites(),
window.api.friendsMyServer(),
window.api.friendsUnreadByFriend().catch(() => ({})),
]);
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>
${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>` : ''}
<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 btn-danger" data-act="remove" title="Entfernen">🗑</button>`;
row.querySelector('.friend-avatar').addEventListener('error', function () { this.remove(); });
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) {
row.querySelector('[data-act="invite"]').addEventListener('click', async (ev) => {
const btn = ev.currentTarget;
@@ -3968,6 +4251,71 @@ async function freundBeitreten(server, version, einladungId) {
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() {
const input = el('friends-add-input');
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) {
const box = el(boxId);
box.innerHTML = '';
@@ -4010,6 +4388,13 @@ function renderContentList(boxId, items, sub, emoji) {
else toast(res.message || 'Sicherung fehlgeschlagen.', true);
});
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');
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-keep').value = s.autoBackupKeep || 5;
el('s-autobackup-launch').checked = !!s.autoBackupBeforeLaunch;
el('s-auto-world').checked = s.autoWorldBackup !== false;
el('s-autobackup-last').textContent = s.autoBackupLast
? 'Zuletzt automatisch gesichert: ' + new Date(s.autoBackupLast).toLocaleString('de-DE')
: 'Noch keine automatische Sicherung.';
@@ -4568,6 +4954,7 @@ async function saveSettings() {
autoBackupEvery: el('s-autobackup-every').value,
autoBackupKeep: Math.min(50, Math.max(1, numOrNull(el('s-autobackup-keep').value) || 5)),
autoBackupBeforeLaunch: el('s-autobackup-launch').checked,
autoWorldBackup: el('s-auto-world').checked,
showConsoleOnLaunch: el('s-console-launch').checked,
autoCloseConsole: el('s-console-autoclose').checked,
showConsoleOnCrash: el('s-console-crash').checked,
@@ -5065,11 +5452,16 @@ function wire() {
// Kontextmenü: Einträge lösen die Aktionen der rechten Leiste aus
document.querySelectorAll('#ctx-menu .ctx-item').forEach((item) => {
item.addEventListener('click', () => {
const target = el(item.dataset.act);
closeContextMenu();
if (item.dataset.act === 'ctx-schnellstart') { if (currentDetailId) setzeSchnellstart(currentDetailId); return; }
const target = el(item.dataset.act);
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
document.addEventListener('click', (e) => {
if (!e.target.closest('#ctx-menu')) closeContextMenu();
@@ -5220,7 +5612,26 @@ function wire() {
// Freunde
el('btn-friends').addEventListener('click', openFriends);
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('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) => {
if (e.key === 'Enter') sendeFreundschaftsanfrage();
});
@@ -5518,6 +5929,38 @@ function wire() {
if (!ok) toast('Ordner mit den Berichten nicht gefunden.', true);
});
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 () => {
const res = await window.api.modAddFile(currentDetailId, modArt);
@@ -5702,6 +6145,15 @@ function wire() {
el('search').select();
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
if (strg && e.key.toLowerCase() === 'n' && !modalOffen) {
e.preventDefault();
@@ -5782,7 +6234,7 @@ const POPOUT_OPENERS = {
settings: (p) => openSettings(p.pane || new URLSearchParams(location.search).get('pane') || undefined),
console: () => openConsole(),
mods: () => openMods(),
crash: () => zeigeAbsturzbericht(currentDetailId),
crash: (p) => zeigeAbsturzbericht(currentDetailId, Number(p.seit) || 0),
logs: () => openLogs(),
content: () => openContent(),
screens: () => openScreens(),
@@ -5855,22 +6307,34 @@ window.addEventListener('DOMContentLoaded', async () => {
iso3dEnabled = s.iso3dIcons !== false;
consoleMaxChars = Math.max(50000, (s.consoleMaxLines || 100000) * 3);
el('btn-friends').classList.toggle('hidden', s.friendsEnabled === false);
schnellstartBelegung = s.schnellstart || {};
} catch { applyTheme('dark'); }
// Zahl offener Freundschaftsanfragen am "Freunde"-Knopf (nur Hauptfenster
// zeigt die Werkzeugleiste)
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');
badge.textContent = zahl > 99 ? '99+' : String(zahl);
badge.classList.toggle('hidden', !zahl);
el('btn-friends').title = zahl
? `Freunde – ${zahl} offene Anfrage${zahl === 1 ? '' : 'n'} (ingame: Strg+Alt+F zeigt, wer online ist)`
: 'Freunde (ingame: Strg+Alt+F zeigt, wer online ist)';
const teile = [];
if (anfragen) teile.push(`${anfragen} offene Anfrage${anfragen === 1 ? '' : 'n'}`);
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.friendsRequestCount().then(zeigeAnfragenZahl).catch(() => {});
// Meldungen aus dem Hauptprozess, deren Fenster schon zu ist (z. B. Performance-Paket)
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)
window.api.onFriendsInvite((inv) => {
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
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();
window.api.onLaunchProgress(onLaunchEvt);
// Servername live in den Titel nachtragen, falls das Aufzeichnungs-Fenster
@@ -5921,6 +6387,7 @@ window.addEventListener('DOMContentLoaded', async () => {
window.api.onSettingsChanged((s) => {
applyTheme(s.theme);
iso3dEnabled = s.iso3dIcons !== false;
schnellstartBelegung = s.schnellstart || {};
if (!POPOUT) {
applyBedrockVisibility();
refreshSkinBackground();