Upload via GUI (93 Dateien)
This commit is contained in:
+487
-20
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user