Upload via GUI (146 Dateien)
This commit is contained in:
+686
-11
@@ -493,6 +493,7 @@ let pendingImagePath = null; // im Dialog gewähltes eigenes Bild (noc
|
||||
let pendingClearImage = false; // eigenes Bild soll entfernt werden
|
||||
let loginVerificationUri = null; // aktueller Device-Code-Link
|
||||
const collapsedGroups = new Set(); // eingeklappte Gruppen
|
||||
let laufendeIds = new Set(); // Instanzen, deren Spiel gerade läuft
|
||||
let consoleBuffer = ''; // gesammelte Log-Ausgabe (roh, für Kopieren)
|
||||
let consolePending = ''; // noch nicht abgeschlossene Zeile
|
||||
|
||||
@@ -1585,6 +1586,93 @@ let pendingUpdate = null;
|
||||
|
||||
const CHANNEL_LABEL = { portable: 'Portable', setup: 'Setup' };
|
||||
|
||||
// ---------- Markdown für die Update-Hinweise ----------
|
||||
// Kleiner Renderer für die Release-Notizen aus Gitea: Überschriften, Listen
|
||||
// (auch verschachtelt), Absätze, **fett**, *kursiv*, ~~durchgestrichen~~,
|
||||
// `Code`, Codeblöcke, Zitate, Trennlinien und Links. Der Text wird ZUERST
|
||||
// komplett escaped – HTML aus den Notizen landet nie ungefiltert in der Seite,
|
||||
// erzeugt werden nur die Tags von hier.
|
||||
function mdInline(t) {
|
||||
const codes = [];
|
||||
t = t.replace(/`([^`]+)`/g, (_, c) => { codes.push(c); return `\u0000${codes.length - 1}\u0000`; });
|
||||
t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_, txt, url) => `<a href="${url}" data-extern>${txt}</a>`);
|
||||
// nackte Adressen verlinken (nicht die eben erzeugten href="…" bzw. Linktexte)
|
||||
t = t.replace(/(?<![="'>\w])(https?:\/\/[^\s<]*[^\s<.,;:!?)])/g, '<a href="$1" data-extern>$1</a>');
|
||||
t = t.replace(/\*\*(.+?)\*\*|__(.+?)__/g, (_, a, b) => `<strong>${a || b}</strong>`);
|
||||
t = t.replace(/(^|[^*\w])\*(?!\s)(.+?)(?<!\s)\*(?!\*)/g, '$1<em>$2</em>');
|
||||
t = t.replace(/(^|[^_\w])_(?!\s)(.+?)(?<!\s)_(?!\w)/g, '$1<em>$2</em>');
|
||||
t = t.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||||
return t.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code>${codes[i]}</code>`);
|
||||
}
|
||||
|
||||
function mdZuHtml(md) {
|
||||
const zeilen = escapeHtml(String(md || '').replace(/\r\n?/g, '\n')).split('\n');
|
||||
let html = '';
|
||||
let absatz = [];
|
||||
let zitat = [];
|
||||
let code = null; // Zeilen eines ```-Blocks
|
||||
const listen = []; // offene Listen: { typ, einzug }
|
||||
let warLeer = false;
|
||||
const absatzEnde = () => { if (absatz.length) { html += `<p>${mdInline(absatz.join(' '))}</p>`; absatz = []; } };
|
||||
const zitatEnde = () => { if (zitat.length) { html += `<blockquote>${mdInline(zitat.join(' '))}</blockquote>`; zitat = []; } };
|
||||
const listenZu = (bis = -1) => {
|
||||
while (listen.length && listen[listen.length - 1].einzug > bis) html += `</li></${listen.pop().typ}>`;
|
||||
};
|
||||
const alleZu = () => { absatzEnde(); zitatEnde(); listenZu(); };
|
||||
|
||||
for (const zeile of zeilen) {
|
||||
if (/^\s*```/.test(zeile)) {
|
||||
if (code) { html += `<pre><code>${code.join('\n')}</code></pre>`; code = null; } else { alleZu(); code = []; }
|
||||
continue;
|
||||
}
|
||||
if (code) { code.push(zeile); continue; }
|
||||
if (!zeile.trim()) { absatzEnde(); zitatEnde(); warLeer = true; continue; }
|
||||
|
||||
const ueberschrift = zeile.match(/^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/);
|
||||
if (ueberschrift) {
|
||||
alleZu();
|
||||
const tag = ['h4', 'h4', 'h5', 'h6', 'h6', 'h6'][ueberschrift[1].length - 1];
|
||||
html += `<${tag}>${mdInline(ueberschrift[2])}</${tag}>`;
|
||||
warLeer = false;
|
||||
continue;
|
||||
}
|
||||
if (/^\s{0,3}([-*_])(\s*\1){2,}\s*$/.test(zeile)) { alleZu(); html += '<hr>'; warLeer = false; continue; }
|
||||
const zitatZeile = zeile.match(/^\s{0,3}>\s?(.*)$/);
|
||||
if (zitatZeile) { absatzEnde(); listenZu(); zitat.push(zitatZeile[1]); warLeer = false; continue; }
|
||||
|
||||
const punkt = zeile.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
||||
if (punkt) {
|
||||
absatzEnde(); zitatEnde();
|
||||
const einzug = punkt[1].replace(/\t/g, ' ').length;
|
||||
const typ = /\d/.test(punkt[2]) ? 'ol' : 'ul';
|
||||
listenZu(einzug);
|
||||
const oben = listen[listen.length - 1];
|
||||
if (oben && oben.einzug === einzug && oben.typ === typ) {
|
||||
html += '</li><li>';
|
||||
} else {
|
||||
if (oben && oben.einzug === einzug) listenZu(einzug - 1); // anderer Listentyp auf gleicher Ebene
|
||||
html += `<${typ}><li>`;
|
||||
listen.push({ typ, einzug });
|
||||
}
|
||||
html += mdInline(punkt[3]);
|
||||
warLeer = false;
|
||||
continue;
|
||||
}
|
||||
// Fortsetzung eines Listenpunkts (eingerückt oder direkt darunter)
|
||||
if (listen.length && (/^\s+/.test(zeile) || !warLeer)) {
|
||||
html += ' ' + mdInline(zeile.trim());
|
||||
continue;
|
||||
}
|
||||
listenZu();
|
||||
zitatEnde();
|
||||
absatz.push(zeile.trim());
|
||||
warLeer = false;
|
||||
}
|
||||
if (code) html += `<pre><code>${code.join('\n')}</code></pre>`;
|
||||
alleZu();
|
||||
return html;
|
||||
}
|
||||
|
||||
function showUpdateDialog(info) {
|
||||
if (POPOUT !== 'update') { window.api.popoutOpen('update'); return; }
|
||||
if (!info) return; // Popup geöffnet, aber (aktuell) kein Update-Ergebnis vorhanden
|
||||
@@ -1595,7 +1683,13 @@ function showUpdateDialog(info) {
|
||||
el('upd-channel').textContent = channel
|
||||
? `Update-Kanal: ${channel}`
|
||||
: 'Update-Kanal: unbekannt – automatisches Update nicht möglich';
|
||||
el('upd-notes').textContent = (info.notes || '').trim() || 'Keine Änderungshinweise hinterlegt.';
|
||||
// erste Überschrift weglassen, wenn sie nur die neue Version nennt ("## [1.2.1] - …") –
|
||||
// die steht oben schon als "1.2.0 → 1.2.1"
|
||||
let notizen = (info.notes || '').trim();
|
||||
const kopf = notizen.match(/^#{1,6}[ \t]+([^\r\n]*)(\r?\n|$)/);
|
||||
if (kopf && info.latestVersion && kopf[1].includes(String(info.latestVersion).replace(/^v/i, ''))) notizen = notizen.slice(kopf[0].length).trim();
|
||||
el('upd-notes').innerHTML = notizen ? mdZuHtml(notizen) : '<p class="hint">Keine Änderungshinweise hinterlegt.</p>';
|
||||
el('upd-notes').scrollTop = 0;
|
||||
el('upd-progress').classList.add('hidden');
|
||||
el('upd-bar-fill').style.width = '0%';
|
||||
|
||||
@@ -1615,6 +1709,14 @@ function showUpdateDialog(info) {
|
||||
show('modal-update');
|
||||
}
|
||||
|
||||
// Links in den Update-Hinweisen im Browser öffnen, nie im Launcher-Fenster
|
||||
function updateLinkKlick(e) {
|
||||
const a = e.target.closest('a');
|
||||
if (!a) return;
|
||||
e.preventDefault();
|
||||
if (a.hasAttribute('data-extern') && /^https?:\/\//.test(a.href)) window.api.openExternal(a.href);
|
||||
}
|
||||
|
||||
function openReleasePage() {
|
||||
if (pendingUpdate && pendingUpdate.url) window.api.openExternal(pendingUpdate.url);
|
||||
}
|
||||
@@ -1762,6 +1864,43 @@ function beendeSkinLive() {
|
||||
if (flaeche) flaeche.classList.remove('skin-lebt');
|
||||
}
|
||||
|
||||
// ---------- Auftritt nach einem Skin-/Umhang-Wechsel ----------
|
||||
// Der nächste Aufbau einer Figur (innerhalb kurzer Zeit) dreht sich einmal um
|
||||
// sich selbst bzw. lässt den Umhang flattern.
|
||||
let skinAuftritt = null;
|
||||
function merkeSkinAuftritt(art) { skinAuftritt = { art, bis: Date.now() + 15000 }; }
|
||||
function holeSkinAuftritt() {
|
||||
const a = skinAuftritt;
|
||||
skinAuftritt = null;
|
||||
return a && a.bis > Date.now() && bewegungErlaubt() ? a.art : null;
|
||||
}
|
||||
// liefert pro Bild die Zusatz-Drehung und den Umhang-Ausschlag, danach null.
|
||||
// Die Zeit läuft erst ab dem ersten gezeichneten Bild – ist das Fenster gerade
|
||||
// im Hintergrund, sieht man den Auftritt, sobald man wieder hinschaut.
|
||||
function skinAuftrittWirkung(art) {
|
||||
let start = null;
|
||||
return () => {
|
||||
const jetzt = performance.now();
|
||||
if (start === null) start = jetzt;
|
||||
const s = (jetzt - start) / 1000;
|
||||
if (art === 'drehen') {
|
||||
const d = 1.35;
|
||||
if (s >= d) return null;
|
||||
const x = s / d;
|
||||
const e = x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
||||
return { drehung: e * Math.PI * 2, umhang: Math.sin(x * Math.PI) * 0.35 };
|
||||
}
|
||||
const d = 2.4;
|
||||
if (s >= d) return null;
|
||||
const x = s / d;
|
||||
const abklingen = 1 - x;
|
||||
return {
|
||||
drehung: Math.sin(x * Math.PI) * 1.1, // seitlich drehen, damit man den Umhang sieht
|
||||
umhang: (0.55 + Math.sin(s * 15) * 0.22) * abklingen, // hochwehen und flattern
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
async function zeigeLebendigeFigur(quelle, poseKey, opts = {}) {
|
||||
beendeSkinLive();
|
||||
const canvas = el('skin-live');
|
||||
@@ -1799,6 +1938,8 @@ async function zeigeLebendigeFigur(quelle, poseKey, opts = {}) {
|
||||
|
||||
const ziel = { yaw: 0, pitch: 0 };
|
||||
const kopf = { yaw: 0, pitch: 0 };
|
||||
const auftritt = holeSkinAuftritt();
|
||||
let wirkung = auftritt ? skinAuftrittWirkung(auftritt) : null;
|
||||
const maus = (e) => {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const kx = r.left + r.width * 0.5;
|
||||
@@ -1819,6 +1960,11 @@ async function zeigeLebendigeFigur(quelle, poseKey, opts = {}) {
|
||||
kopf.pitch += (ziel.pitch - kopf.pitch) * 0.07;
|
||||
// ohne Maus schaut die Figur halb zum Betrachter, sonst dem Zeiger hinterher
|
||||
pl.skin.head.rotation.set(kopfBasis.x + kopf.pitch, kopfBasis.y - koerperBasis * 0.45 + kopf.yaw, kopfBasis.z);
|
||||
if (wirkung) {
|
||||
const w = wirkung();
|
||||
if (!w) wirkung = null;
|
||||
else { pl.rotation.y += w.drehung; if (pl.cape) pl.cape.rotation.x += w.umhang; }
|
||||
}
|
||||
});
|
||||
|
||||
const sicht = () => { viewer.renderPaused = document.hidden || !document.hasFocus(); };
|
||||
@@ -2103,6 +2249,7 @@ async function refreshSelectionPanel() {
|
||||
|
||||
const id = selectedIds[0];
|
||||
currentDetailId = id;
|
||||
zeigeStartKnopf(); // Fortschritt gehört zur jeweils gewählten Instanz
|
||||
setSingleSelectionActionsDisabled(false);
|
||||
el('p-delete').disabled = false;
|
||||
el('p-delete').textContent = 'Löschen';
|
||||
@@ -2170,8 +2317,11 @@ function buildCard(inst) {
|
||||
|
||||
card.innerHTML = `
|
||||
${iconHtml}
|
||||
<i class="card-riss" aria-hidden="true"></i>
|
||||
<div class="card-name">${escapeHtml(inst.name)}</div>
|
||||
${taste ? `<span class="card-taste">Strg+${taste}</span>` : ''}`;
|
||||
if (laufendeIds.has(inst.id)) card.classList.add('laeuft');
|
||||
if (startStand.has(inst.id)) zeigeAbbau(card);
|
||||
|
||||
// einfacher Klick = auswählen (MultiMC), Doppelklick = starten
|
||||
card.addEventListener('click', (e) => {
|
||||
@@ -2207,9 +2357,72 @@ function sucheSchliessen() {
|
||||
|
||||
let instanzenEingeblendet = false;
|
||||
|
||||
// ---------- Weiche Übergänge in der Instanzliste ----------
|
||||
// Vor dem Neuaufbau merken, wo jede Karte/Überschrift stand; danach gleiten
|
||||
// verschobene Karten von dort an ihren neuen Platz, neue blenden ein und
|
||||
// entfernte schrumpfen als "Geist" an ihrer alten Stelle weg (FLIP).
|
||||
function flipSchluessel(k) {
|
||||
if (k.classList.contains('card')) return 'c:' + k.dataset.id;
|
||||
if (k.classList.contains('group-header')) return 'h:' + k.dataset.pfad;
|
||||
return null;
|
||||
}
|
||||
function flipErfassen(grid) {
|
||||
const stand = new Map();
|
||||
if (!instanzenEingeblendet || POPOUT || !bewegungErlaubt()) return stand;
|
||||
grid.querySelectorAll('.card, .group-header').forEach((k) => {
|
||||
const key = flipSchluessel(k);
|
||||
const rect = k.getBoundingClientRect();
|
||||
// zugeklappte Gruppen: Karten stecken unsichtbar im DOM -> zählen als nicht da
|
||||
if (key && rect.width > 0) stand.set(key, { rect, el: k });
|
||||
});
|
||||
return stand;
|
||||
}
|
||||
function flipAbspielen(grid, vorher) {
|
||||
if (!vorher.size) return;
|
||||
const sicht = grid.getBoundingClientRect();
|
||||
const sichtbar = (r) => r.bottom > sicht.top && r.top < sicht.bottom && r.width > 0;
|
||||
const jetzt = new Set();
|
||||
let neuIndex = 0;
|
||||
grid.querySelectorAll('.card, .group-header').forEach((k) => {
|
||||
const key = flipSchluessel(k);
|
||||
if (!key) return;
|
||||
const r = k.getBoundingClientRect();
|
||||
if (r.width === 0) return; // in zugeklappter Gruppe
|
||||
jetzt.add(key);
|
||||
if (!sichtbar(r)) return;
|
||||
const alt = vorher.get(key);
|
||||
if (alt) {
|
||||
const dx = alt.rect.left - r.left;
|
||||
const dy = alt.rect.top - r.top;
|
||||
if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
|
||||
k.animate([{ transform: `translate(${dx}px,${dy}px)` }, { transform: 'none' }],
|
||||
{ duration: 340, easing: 'cubic-bezier(.2,.8,.2,1)' });
|
||||
}
|
||||
} else {
|
||||
k.animate([{ opacity: 0, transform: 'scale(.82)' }, { opacity: 1, transform: 'none' }],
|
||||
{ duration: 260, delay: 70 + Math.min(neuIndex++, 12) * 18, easing: 'cubic-bezier(.2,.8,.2,1)', fill: 'backwards' });
|
||||
}
|
||||
});
|
||||
// weggefallene Karten (gelöscht, verschoben, Gruppe zugeklappt) schrumpfen weg
|
||||
let geister = 0;
|
||||
for (const [key, alt] of vorher) {
|
||||
if (jetzt.has(key) || !key.startsWith('c:') || !sichtbar(alt.rect) || geister++ > 40) continue;
|
||||
const geist = alt.el;
|
||||
geist.classList.add('flip-geist');
|
||||
Object.assign(geist.style, {
|
||||
left: alt.rect.left + 'px', top: alt.rect.top + 'px', width: alt.rect.width + 'px', height: alt.rect.height + 'px',
|
||||
});
|
||||
document.body.appendChild(geist);
|
||||
const weg = () => geist.remove();
|
||||
geist.animate([{ opacity: 1, transform: 'none' }, { opacity: 0, transform: 'scale(.55)' }],
|
||||
{ duration: 240, easing: 'cubic-bezier(.5,0,.75,0)', fill: 'forwards' }).finished.then(weg, weg);
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const q = el('search').value.trim().toLowerCase();
|
||||
const grid = el('grid');
|
||||
const vorher = flipErfassen(grid);
|
||||
grid.innerHTML = '';
|
||||
|
||||
const filtered = instances.filter((i) =>
|
||||
@@ -2235,6 +2448,8 @@ function render() {
|
||||
grid.classList.add('einblenden');
|
||||
setTimeout(() => grid.classList.remove('einblenden'), 1400);
|
||||
}
|
||||
} else {
|
||||
flipAbspielen(grid, vorher);
|
||||
}
|
||||
status(`${instances.length} Instanz${instances.length === 1 ? '' : 'en'}`);
|
||||
}
|
||||
@@ -2279,6 +2494,7 @@ function baueGruppenSektion(knoten, tiefe, showHeaders) {
|
||||
if (showHeaders) {
|
||||
const header = document.createElement('div');
|
||||
header.className = 'group-header' + (zu ? ' collapsed' : '');
|
||||
header.dataset.pfad = g;
|
||||
header.title = g ? gruppenAnzeige(g) : 'Instanzen ohne Gruppe';
|
||||
header.innerHTML = `<span class="chev">${zu ? '▸' : '▾'}</span> ${escapeHtml(knoten.name)} <span class="group-count">(${anzahlImZweig(knoten)})</span>`;
|
||||
header.addEventListener('click', () => {
|
||||
@@ -2648,8 +2864,11 @@ async function launch(id, accountId, launchOpts) {
|
||||
status('Vorbereiten …');
|
||||
el('status-right').textContent = '';
|
||||
appendConsole('\n=== Start: ' + new Date().toLocaleTimeString('de-DE') + ' ===\n');
|
||||
startStand.set(id, { prozent: 2, text: 'Vorbereiten …' });
|
||||
zeigeStartKnopf();
|
||||
try {
|
||||
const res = await window.api.launchInstance(id, accountId, launchOpts || null);
|
||||
beendeStartFortschritt(id, !!res.ok);
|
||||
if (res.ok) {
|
||||
status('Läuft (PID ' + res.pid + ')');
|
||||
toast(res.modUpdates ? `Minecraft gestartet – ${res.modUpdates} Mod${res.modUpdates === 1 ? '' : 's'} vorher aktualisiert.` : 'Minecraft gestartet.');
|
||||
@@ -2663,6 +2882,7 @@ async function launch(id, accountId, launchOpts) {
|
||||
toast(res.message || 'Start fehlgeschlagen.', true);
|
||||
}
|
||||
} catch (e) {
|
||||
beendeStartFortschritt(id, false);
|
||||
status('Bereit'); toast('Fehler: ' + e.message, true);
|
||||
} finally {
|
||||
launching = false;
|
||||
@@ -3000,6 +3220,21 @@ async function zeigeSkinFigur(bildId, leerId, textur, leerText, umhang) {
|
||||
viewer.controls.enableRotate = true;
|
||||
viewer.controls.enableZoom = true;
|
||||
viewer.controls.enablePan = false;
|
||||
// gerade übernommen -> einmal drehen bzw. Umhang flattern lassen
|
||||
const auftritt = bildId === 'skin-aktuell' ? holeSkinAuftritt() : null;
|
||||
if (auftritt) {
|
||||
const pl = viewer.playerObject;
|
||||
const basisY = pl.rotation.y;
|
||||
const umhangBasis = pl.cape ? pl.cape.rotation.x : 0;
|
||||
let wirkung = skinAuftrittWirkung(auftritt);
|
||||
viewer.animation = new skinview3d.FunctionAnimation((p) => {
|
||||
if (!wirkung) return;
|
||||
const w = wirkung();
|
||||
if (!w) wirkung = null;
|
||||
p.rotation.y = basisY + (w ? w.drehung : 0);
|
||||
if (p.cape) p.cape.rotation.x = umhangBasis + (w ? w.umhang : 0);
|
||||
});
|
||||
}
|
||||
skinViewers[bildId] = viewer;
|
||||
canvas.classList.remove('hidden');
|
||||
leer.classList.add('hidden');
|
||||
@@ -3032,11 +3267,106 @@ async function openSkin() {
|
||||
// Armform zuerst setzen – davon hängt ab, wie die Figur gerendert wird
|
||||
if (p.variante) el('skin-variante').value = p.variante === 'slim' ? 'slim' : 'classic';
|
||||
skinTexturAktuell = p.skinUrl || '';
|
||||
pruefeArmform(); // läuft nebenher, zeigt bei Bedarf einen Hinweis
|
||||
zeigeGarderobe();
|
||||
await ladeUmhangInfo();
|
||||
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell,
|
||||
'Standard-Skin (keine eigene Datei hinterlegt)', skinUmhang);
|
||||
}
|
||||
|
||||
// ---------- Passt die Armform des Kontos zur Skin-Datei? ----------
|
||||
const ARMFORM_NAME = { classic: 'Klassisch (breite Arme, Steve)', slim: 'Schmal (Alex)' };
|
||||
async function pruefeArmform() {
|
||||
const box = el('skin-armform-hinweis');
|
||||
box.classList.add('hidden');
|
||||
const r = await window.api.skinArmformPruefen().catch(() => null);
|
||||
if (!r || !r.ok || r.passt) return;
|
||||
el('skin-armform-text').textContent = r.datei === 'classic'
|
||||
? `⚠ Deine Skin-Datei ist für breite Arme gemalt, dein Konto steht aber auf „${ARMFORM_NAME.slim}“. Im Spiel wirken die Arme dadurch gequetscht – andere Spieler sehen dich genauso.`
|
||||
: `⚠ Deine Skin-Datei ist für schmale Arme gemalt, dein Konto steht aber auf „${ARMFORM_NAME.classic}“. Im Spiel erscheinen dadurch dunkle Streifen an den Armen.`;
|
||||
el('skin-armform-fix').textContent = r.datei === 'classic' ? 'Auf Klassisch umstellen' : 'Auf Schmal umstellen';
|
||||
el('skin-armform-fix').disabled = false;
|
||||
box.classList.remove('hidden');
|
||||
}
|
||||
async function korrigiereArmform() {
|
||||
el('skin-armform-fix').disabled = true;
|
||||
const r = await window.api.skinArmformKorrigieren();
|
||||
if (!r.ok) { toast(r.message || 'Umstellen fehlgeschlagen.', true); el('skin-armform-fix').disabled = false; return; }
|
||||
toast(`Armform auf „${ARMFORM_NAME[r.variante]}“ umgestellt.`);
|
||||
await openSkin();
|
||||
await refreshSkinBackground();
|
||||
}
|
||||
|
||||
// ---------- Garderobe: gespeicherte Skins und Umhänge ----------
|
||||
const garderobeVorschau = new Map(); // hash -> Bild (3D-Figur), damit nicht jedes Mal neu gezeichnet wird
|
||||
async function zeigeGarderobe() {
|
||||
const box = el('garderobe-liste');
|
||||
const eintraege = await window.api.garderobeListe().catch(() => []);
|
||||
box.innerHTML = '';
|
||||
if (!eintraege.length) {
|
||||
box.innerHTML = '<div class="garderobe-leer">Noch nichts gespeichert. „Aktuellen Skin speichern“ legt deinen jetzigen Skin hier ab.</div>';
|
||||
return;
|
||||
}
|
||||
for (const e of eintraege) {
|
||||
const karte = document.createElement('div');
|
||||
karte.className = 'gard-karte';
|
||||
const istSkin = e.art === 'skin';
|
||||
karte.innerHTML = `
|
||||
<button class="gard-mini links" title="Umbenennen">✎</button>
|
||||
<button class="gard-mini rechts" title="Aus der Garderobe löschen">✕</button>
|
||||
<img class="gard-bild ${istSkin ? '' : 'umhang'}" alt="" />
|
||||
<div class="gard-name" title="${escapeHtml(e.name)}">${escapeHtml(e.name)}</div>
|
||||
<div class="gard-merk">${istSkin ? (e.variante === 'slim' ? 'Skin · schmal' : 'Skin · breit') : 'AeroMC-Umhang'}${e.automatisch ? ' · automatisch' : ''}</div>
|
||||
<div class="gard-aktionen"><button class="btn btn-mini btn-primary">${istSkin ? 'Anziehen' : 'Anlegen'}</button></div>`;
|
||||
const bild = karte.querySelector('.gard-bild');
|
||||
if (istSkin) {
|
||||
if (garderobeVorschau.has(e.hash)) bild.src = garderobeVorschau.get(e.hash);
|
||||
else {
|
||||
bild.src = e.dataUri; // erst die Datei, dann die 3D-Figur nachreichen
|
||||
renderSkin3D(e.dataUri, 'standing', { modell: e.variante, breite: 128, hoehe: 192 }).then((b) => {
|
||||
if (b) { garderobeVorschau.set(e.hash, b); bild.src = b; }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
bild.src = e.dataUri;
|
||||
}
|
||||
karte.querySelector('.btn-primary').addEventListener('click', async (ev) => {
|
||||
ev.target.disabled = true;
|
||||
ev.target.textContent = '…';
|
||||
const r = await window.api.garderobeAnziehen(e.id);
|
||||
if (!r.ok) { toast(r.message || 'Fehlgeschlagen.', true); await zeigeGarderobe(); return; }
|
||||
toast(istSkin ? `„${e.name}“ angezogen.` : `Umhang „${e.name}“ angelegt.`);
|
||||
await openSkin();
|
||||
await refreshSkinBackground();
|
||||
});
|
||||
karte.querySelector('.gard-mini.links').addEventListener('click', async () => {
|
||||
const name = await askPrompt({ title: 'Umbenennen', label: 'Name', value: e.name, okLabel: 'Speichern' });
|
||||
if (name == null) return;
|
||||
await window.api.garderobeUmbenennen(e.id, name);
|
||||
await zeigeGarderobe();
|
||||
});
|
||||
karte.querySelector('.gard-mini.rechts').addEventListener('click', () => {
|
||||
askConfirm(`„${e.name}“ löschen?`, 'Der Eintrag wird aus der Garderobe entfernt. Deinen aktuellen Skin betrifft das nicht.', 'Löschen', async () => {
|
||||
await window.api.garderobeLoeschen(e.id);
|
||||
await zeigeGarderobe();
|
||||
});
|
||||
});
|
||||
box.appendChild(karte);
|
||||
}
|
||||
}
|
||||
|
||||
async function garderobeSpeichern(art) {
|
||||
const name = await askPrompt({
|
||||
title: art === 'skin' ? 'Skin in der Garderobe speichern' : 'Umhang in der Garderobe speichern',
|
||||
label: 'Name', value: art === 'skin' ? 'Mein Skin' : 'Mein Umhang', okLabel: 'Speichern',
|
||||
});
|
||||
if (name == null) return;
|
||||
const r = art === 'skin' ? await window.api.garderobeSkinSpeichern(name) : await window.api.garderobeUmhangSpeichern(name);
|
||||
if (!r.ok) { toast(r.message || 'Speichern fehlgeschlagen.', true); return; }
|
||||
toast(r.neu ? `„${r.name}“ gespeichert.` : `Schon in der Garderobe (als „${r.name}“).`);
|
||||
await zeigeGarderobe();
|
||||
}
|
||||
|
||||
// ---------- Umhänge im Skin-Fenster ----------
|
||||
let skinUmhang = null; // data:-Adresse des angezeigten Umhangs (AeroMC vor offiziell)
|
||||
|
||||
@@ -3128,6 +3458,7 @@ async function setzeSkin() {
|
||||
if (res.ok) {
|
||||
el('skin-status').textContent = '✔ Skin gesetzt. Im Spiel kann es einen Moment dauern.';
|
||||
toast('Skin geändert.');
|
||||
merkeSkinAuftritt('drehen');
|
||||
await openSkin(); // Vorschau mit dem neuen Stand auffrischen
|
||||
await refreshSkinBackground(); // auch der Hintergrund zeigt jetzt den neuen Skin
|
||||
} else {
|
||||
@@ -3316,7 +3647,145 @@ async function zeigeAbsturzbericht(instanzId, seit) {
|
||||
}
|
||||
|
||||
// Fortschrittsmeldungen der Download-/Start-Pipeline
|
||||
// ---------- Laufende Instanzen: pulsierender Rand ----------
|
||||
function setzeLaufende(liste) {
|
||||
laufendeIds = new Set(liste || []);
|
||||
document.querySelectorAll('#grid .card').forEach((c) => c.classList.toggle('laeuft', laufendeIds.has(c.dataset.id)));
|
||||
}
|
||||
|
||||
// ---------- Abbau-Risse (Minecraft-Design): Download-Fortschritt auf dem Block ----------
|
||||
// Eigene 16×16-Pixel-Risse, die Stufe für Stufe von der Mitte nach außen wachsen.
|
||||
let rissBilder = null;
|
||||
function rissStufen() {
|
||||
if (rissBilder) return rissBilder;
|
||||
let saat = 20261005;
|
||||
const zufall = () => (saat = (saat * 16807) % 2147483647) / 2147483647;
|
||||
const pixel = [];
|
||||
const gesehen = new Set();
|
||||
const linien = 8;
|
||||
for (let l = 0; l < linien; l++) {
|
||||
let x = 7.5 + (zufall() - 0.5) * 2;
|
||||
let y = 7.5 + (zufall() - 0.5) * 2;
|
||||
let w = (l / linien) * Math.PI * 2 + zufall() * 0.7;
|
||||
for (let s = 0; s < 11; s++) {
|
||||
w += (zufall() - 0.5) * 0.9; // Risse knicken unregelmäßig
|
||||
x += Math.cos(w) * 0.95;
|
||||
y += Math.sin(w) * 0.95;
|
||||
const px = Math.round(x);
|
||||
const py = Math.round(y);
|
||||
if (px < 0 || py < 0 || px > 15 || py > 15) break;
|
||||
const k = px + ',' + py;
|
||||
if (!gesehen.has(k)) { gesehen.add(k); pixel.push([px, py, s + zufall() * 0.5]); }
|
||||
}
|
||||
}
|
||||
pixel.sort((a, b) => a[2] - b[2]);
|
||||
rissBilder = [];
|
||||
for (let stufe = 1; stufe <= 9; stufe++) {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = c.height = 16;
|
||||
const g = c.getContext('2d');
|
||||
const n = Math.round((pixel.length * stufe) / 9);
|
||||
for (const [x, y] of pixel.slice(0, n)) {
|
||||
g.fillStyle = 'rgba(28,18,10,.62)';
|
||||
g.fillRect(x, y, 1, 1);
|
||||
if (x < 15 && !gesehen.has((x + 1) + ',' + y)) { g.fillStyle = 'rgba(255,255,255,.12)'; g.fillRect(x + 1, y, 1, 1); }
|
||||
}
|
||||
rissBilder.push(c.toDataURL());
|
||||
}
|
||||
return rissBilder;
|
||||
}
|
||||
function zeigeAbbau(card) {
|
||||
const stand = startStand.get(card.dataset.id);
|
||||
const riss = card.querySelector('.card-riss');
|
||||
if (!riss) return;
|
||||
if (stand && stand.prozent < 100) {
|
||||
const stufe = Math.max(0, Math.min(8, Math.floor((stand.prozent / 100) * 9)));
|
||||
riss.style.backgroundImage = `url(${rissStufen()[stufe]})`;
|
||||
card.classList.add('abbau');
|
||||
} else if (card.classList.contains('abbau')) {
|
||||
// fertig: der Block "bricht" kurz auf
|
||||
card.classList.remove('abbau');
|
||||
card.classList.add('abgebaut');
|
||||
setTimeout(() => card.classList.remove('abgebaut'), 450);
|
||||
}
|
||||
}
|
||||
function aktualisiereAbbau() {
|
||||
document.querySelectorAll('#grid .card').forEach((c) => {
|
||||
if (startStand.has(c.dataset.id) || c.classList.contains('abbau')) zeigeAbbau(c);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Block-Symbol kippt leicht zur Maus ----------
|
||||
function kippeZurMaus(e) {
|
||||
if (!bewegungErlaubt()) return;
|
||||
const card = e.target.closest('.card');
|
||||
if (!card) return;
|
||||
const r = card.getBoundingClientRect();
|
||||
card.style.setProperty('--kx', ((e.clientX - r.left) / r.width - 0.5).toFixed(3));
|
||||
card.style.setProperty('--ky', ((e.clientY - r.top) / r.height - 0.5).toFixed(3));
|
||||
}
|
||||
|
||||
// ---------- Lichtpunkte im Hintergrund (AeroMC/Nachtlila) ----------
|
||||
function baueLichtpunkte() {
|
||||
const box = el('lichtpunkte');
|
||||
if (!box || box.childElementCount) return;
|
||||
const z = (a, b) => a + Math.random() * (b - a);
|
||||
for (let i = 0; i < 22; i++) {
|
||||
const p = document.createElement('i');
|
||||
p.style.cssText = `--x:${z(2, 98).toFixed(1)}%;--y:${z(8, 100).toFixed(1)}%;--g:${z(2, 4.2).toFixed(1)}px;`
|
||||
+ `--d:${z(16, 30).toFixed(1)}s;--v:-${z(0, 30).toFixed(1)}s;--o:${z(0.25, 0.6).toFixed(2)};`
|
||||
+ `--dx:${z(-50, 50).toFixed(0)}px;--dy:${z(-170, -60).toFixed(0)}px`;
|
||||
box.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Starten-Knopf mit Fortschritt ----------
|
||||
// Jede Phase des Starts bekommt einen Abschnitt des Balkens; Phasen mit
|
||||
// current/total (Libraries, Assets …) füllen ihren Abschnitt anteilig.
|
||||
const START_PHASEN = {
|
||||
backup: [0, 4, 'Sicherung'], 'mod-updates': [4, 8, 'Mod-Updates'], modpack: [0, 8, 'Modpack'],
|
||||
version: [8, 12, 'Version'], client: [12, 22, 'Client'], libraries: [22, 45, 'Libraries'],
|
||||
assets: [45, 80, 'Assets'], loader: [80, 90, 'Loader'], java: [90, 96, 'Java'], launch: [96, 100, 'Startet'],
|
||||
};
|
||||
const startStand = new Map(); // Instanz-ID -> { prozent, text }
|
||||
function zeigeStartKnopf() {
|
||||
const knopf = el('p-play');
|
||||
if (!knopf) { aktualisiereAbbau(); return; }
|
||||
const stand = currentDetailId ? startStand.get(currentDetailId) : null;
|
||||
knopf.classList.toggle('laedt', !!stand);
|
||||
knopf.style.setProperty('--fortschritt', stand ? stand.prozent + '%' : '0%');
|
||||
knopf.querySelector('.play-text').textContent = stand ? stand.text : 'Starten';
|
||||
aktualisiereAbbau();
|
||||
}
|
||||
function merkeStartFortschritt(evt) {
|
||||
if (!evt.id) return;
|
||||
if (evt.phase === 'exit') { startStand.delete(evt.id); zeigeStartKnopf(); return; }
|
||||
const p = START_PHASEN[evt.phase];
|
||||
if (!p) return;
|
||||
const [von, bis, name] = p;
|
||||
const anteil = evt.total ? Math.min(1, evt.current / evt.total) : 0;
|
||||
const prozent = Math.round(von + (bis - von) * anteil);
|
||||
const vorher = startStand.get(evt.id);
|
||||
startStand.set(evt.id, {
|
||||
prozent: Math.max(prozent, vorher ? vorher.prozent : 0), // nie rückwärts
|
||||
text: evt.phase === 'launch' ? 'Startet …' : `${name} ${Math.max(prozent, vorher ? vorher.prozent : 0)} %`,
|
||||
});
|
||||
zeigeStartKnopf();
|
||||
}
|
||||
function beendeStartFortschritt(id, erfolgreich) {
|
||||
if (!startStand.has(id)) return;
|
||||
if (erfolgreich) {
|
||||
startStand.set(id, { prozent: 100, text: 'Läuft ✓' });
|
||||
zeigeStartKnopf();
|
||||
setTimeout(() => { startStand.delete(id); zeigeStartKnopf(); }, 2500);
|
||||
} else {
|
||||
startStand.delete(id);
|
||||
zeigeStartKnopf();
|
||||
}
|
||||
}
|
||||
|
||||
function onLaunchEvt(evt) {
|
||||
if (!POPOUT && evt.phase !== 'log') merkeStartFortschritt(evt);
|
||||
const labels = { version: 'Version', client: 'Client', libraries: 'Libraries', assets: 'Assets', loader: 'Loader', java: 'Java', launch: 'Start', modpack: 'Modpack', backup: 'Sicherung', 'mod-updates': 'Mod-Updates' };
|
||||
if (evt.phase === 'log') {
|
||||
appendConsole(evt.line);
|
||||
@@ -4521,13 +4990,91 @@ async function installiereTeilCode() {
|
||||
// ---------- Spielzeit-Statistik ----------
|
||||
// Gesamtwerte als Kennzahlen, darunter eine nach Spielzeit sortierte Balkenliste
|
||||
// (eine Reihe = eine Instanz, Balkenlänge relativ zur meistgespielten).
|
||||
async function openStats() {
|
||||
if (POPOUT !== 'stats') { window.api.popoutOpen('stats'); return; }
|
||||
async function openStats(reiter) {
|
||||
if (POPOUT !== 'stats') { window.api.popoutOpen('stats', reiter ? { reiter } : undefined); return; }
|
||||
if (reiter) waehleStatistikReiter(reiter);
|
||||
show('modal-stats');
|
||||
await ladeStatistik();
|
||||
}
|
||||
|
||||
// ---------- Erfolge ----------
|
||||
function zeigeErfolgMeldung(daten) {
|
||||
if (POPOUT) return;
|
||||
const box = el('erfolg-meldungen');
|
||||
const liste = daten.liste || [];
|
||||
// viele auf einmal (z. B. nach dem Update): eine Sammelmeldung statt einer Flut
|
||||
const eintraege = liste.length > 2
|
||||
? [{ symbol: 'star', kopf: daten.ersteAuswertung ? `${liste.length} Erfolge freigeschaltet!` : `${liste.length} Erfolge erzielt!`, name: 'Alle in der Statistik ansehen' }]
|
||||
: liste.map((e) => ({ symbol: e.symbol, kopf: 'Erfolg erzielt!', name: e.titel }));
|
||||
eintraege.forEach((e, i) => {
|
||||
const m = document.createElement('div');
|
||||
m.className = 'erfolg';
|
||||
m.innerHTML = `<div class="erfolg-symbol">${iconOf(e.symbol, 28)}</div><div><div class="erfolg-kopf">${escapeHtml(e.kopf)}</div><div class="erfolg-name">${escapeHtml(e.name)}</div></div>`;
|
||||
m.title = 'Alle Erfolge ansehen';
|
||||
m.addEventListener('click', () => window.api.popoutOpen('stats', { reiter: 'erfolge' }));
|
||||
setTimeout(() => {
|
||||
box.appendChild(m);
|
||||
if (bewegungErlaubt()) erfolgFunken(m);
|
||||
setTimeout(() => { m.classList.add('weg'); setTimeout(() => m.remove(), 400); }, 5500);
|
||||
}, i * 700);
|
||||
});
|
||||
}
|
||||
|
||||
// Funken um die Meldung – im Minecraft-Design grüne XP-Kugeln, die hochhüpfen
|
||||
function erfolgFunken(m) {
|
||||
const mc = document.documentElement.dataset.design === 'minecraft';
|
||||
const z = (a, b) => a + Math.random() * (b - a);
|
||||
const n = mc ? 10 : 14;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const f = document.createElement('i');
|
||||
f.className = mc ? 'xp-kugel' : 'funke';
|
||||
m.appendChild(f);
|
||||
const weg = () => f.remove();
|
||||
if (mc) {
|
||||
// aus dem Symbol heraus nach oben, dann seitlich herunterfallen
|
||||
f.style.left = z(14, 38) + 'px';
|
||||
f.style.top = '50%';
|
||||
const dx = z(-30, 150);
|
||||
f.animate([
|
||||
{ transform: 'translate(0,0) scale(.4)', opacity: 0 },
|
||||
{ transform: `translate(${dx * 0.45}px,${-z(28, 52)}px) scale(1)`, opacity: 1, offset: 0.4 },
|
||||
{ transform: `translate(${dx}px,${z(4, 22)}px) scale(.7)`, opacity: 0 },
|
||||
], { duration: z(900, 1300), delay: 380 + i * 45, easing: 'cubic-bezier(.3,.6,.5,1)', fill: 'both' }).finished.then(weg, weg);
|
||||
} else {
|
||||
// vom Symbol aus nach allen Seiten aufleuchten
|
||||
f.style.left = '26px';
|
||||
f.style.top = '50%';
|
||||
const w = (i / n) * Math.PI * 2 + z(-0.2, 0.2);
|
||||
const r = z(45, 105);
|
||||
const bahn = (anteil) => `translate(calc(-50% + ${(Math.cos(w) * r * anteil).toFixed(1)}px),calc(-50% + ${(Math.sin(w) * r * 0.65 * anteil).toFixed(1)}px))`;
|
||||
f.animate([
|
||||
{ transform: `${bahn(0)} scale(.3)`, opacity: 0 },
|
||||
{ transform: `${bahn(0.4)} scale(1.25)`, opacity: 1, offset: 0.2 },
|
||||
{ transform: `${bahn(0.8)} scale(.9)`, opacity: 0.9, offset: 0.6 },
|
||||
{ transform: `${bahn(1)} scale(.2)`, opacity: 0 },
|
||||
], { duration: z(1000, 1400), delay: 330 + i * 22, easing: 'cubic-bezier(.2,.7,.3,1)', fill: 'both' }).finished.then(weg, weg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function zeigeErfolgeInStatistik() {
|
||||
const liste = await window.api.erfolgeListe().catch(() => []);
|
||||
const frei = liste.filter((e) => e.am).length;
|
||||
el('erfolge-zahl').textContent = `(${frei}/${liste.length})`;
|
||||
el('erfolge-liste').innerHTML = liste.map((e) => `<div class="erfolg-karte${e.am ? '' : ' offen'}" title="${escapeHtml(e.text)}">
|
||||
<div class="erfolg-symbol">${iconOf(e.symbol, 24)}</div>
|
||||
<div><b>${escapeHtml(e.titel)}</b><small>${escapeHtml(e.text)}${e.am ? ' · ' + escapeHtml(fmtDate(e.am)) : ' · 🔒'}</small></div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function waehleStatistikReiter(name) {
|
||||
document.querySelectorAll('[data-stats-reiter]').forEach((b) => b.classList.toggle('on', b.dataset.statsReiter === name));
|
||||
el('stats-reiter-spielzeit').classList.toggle('hidden', name !== 'spielzeit');
|
||||
el('stats-reiter-erfolge').classList.toggle('hidden', name !== 'erfolge');
|
||||
}
|
||||
|
||||
async function ladeStatistik() {
|
||||
zeigeErfolgeInStatistik();
|
||||
const alle = await window.api.listInstances();
|
||||
const gespielt = alle.filter((i) => (i.totalPlaySeconds || 0) > 0)
|
||||
.sort((a, b) => (b.totalPlaySeconds || 0) - (a.totalPlaySeconds || 0));
|
||||
@@ -4583,6 +5130,58 @@ async function openFriends() {
|
||||
friendsAutoRefresh = setInterval(() => (chatPartner ? ladeChat() : ladeFreunde()), chatPartner ? 10000 : 30000);
|
||||
}
|
||||
|
||||
// ---------- Freunde: 3D-Köpfe und Figur des ausgewählten Freundes ----------
|
||||
function setzeFreundKoepfe() {
|
||||
document.querySelectorAll('#friends-content img[data-kopf]').forEach((img) => {
|
||||
const uuid = img.dataset.kopf;
|
||||
kontoKopf(uuid).then((src) => { img.src = src; });
|
||||
});
|
||||
}
|
||||
|
||||
let freundBuehne = null; // { viewer, uuid }
|
||||
async function zeigeFreundFigur(uuid, name, status, online) {
|
||||
const box = el('freund-buehne');
|
||||
el('freund-buehne-name').textContent = name;
|
||||
el('freund-buehne-status').textContent = status;
|
||||
document.querySelectorAll('.friend-row.gezeigt').forEach((r) => r.classList.remove('gezeigt'));
|
||||
const zeile = document.querySelector(`.friend-row[data-uuid="${CSS.escape(uuid)}"]`);
|
||||
if (zeile) zeile.classList.add('gezeigt');
|
||||
if (freundBuehne && freundBuehne.uuid === uuid) return;
|
||||
if (typeof skinview3d === 'undefined') return;
|
||||
box.classList.remove('hidden');
|
||||
const canvas = el('freund-figur');
|
||||
if (!freundBuehne) {
|
||||
const viewer = new skinview3d.SkinViewer({ canvas, width: box.clientWidth || 400, height: box.clientHeight || 190 });
|
||||
viewer.fov = 40;
|
||||
viewer.zoom = 0.72;
|
||||
viewer.controls.enableZoom = false;
|
||||
viewer.controls.enablePan = false;
|
||||
new ResizeObserver(() => viewer.setSize(box.clientWidth, box.clientHeight)).observe(box);
|
||||
document.addEventListener('visibilitychange', () => { viewer.renderPaused = document.hidden; });
|
||||
freundBuehne = { viewer, uuid: null };
|
||||
}
|
||||
const { viewer } = freundBuehne;
|
||||
freundBuehne.uuid = uuid;
|
||||
let modell = 'classic';
|
||||
try { modell = await window.api.skinModelFor(uuid); } catch { /* breite Arme */ }
|
||||
if (freundBuehne.uuid !== uuid) return; // inzwischen anderer Freund gewählt
|
||||
try { await viewer.loadSkin(`https://crafthead.net/skin/${uuid}`, { model: modell === 'slim' ? 'slim' : 'default' }); } catch { /* Standard bleibt */ }
|
||||
try {
|
||||
const r = await window.api.capeForUuid(uuid);
|
||||
if (freundBuehne.uuid !== uuid) return;
|
||||
if (r && r.dataUri) await viewer.loadCape(r.dataUri); else viewer.resetCape();
|
||||
} catch { viewer.resetCape(); }
|
||||
// online: läuft, offline: steht und atmet – ohne Bewegung (Optionen) ganz ruhig
|
||||
const bewegt = bewegungErlaubt();
|
||||
viewer.autoRotate = bewegt;
|
||||
viewer.autoRotateSpeed = 0.7;
|
||||
viewer.animation = !bewegt ? null : (online ? new skinview3d.WalkingAnimation() : new skinview3d.IdleAnimation());
|
||||
if (viewer.animation) viewer.animation.speed = online ? 0.6 : 1;
|
||||
}
|
||||
|
||||
// zuletzt gesehener Online-Stand je Freund – für das kurze Hüpfen beim Online-Kommen
|
||||
const freundOnlineVorher = new Map();
|
||||
|
||||
async function ladeFreunde() {
|
||||
const aktiv = await window.api.friendsEnabled();
|
||||
el('friends-disabled-hint').classList.toggle('hidden', aktiv);
|
||||
@@ -4611,7 +5210,7 @@ async function ladeFreunde() {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'friend-row';
|
||||
const name = spitznamen[inv.fromUuid] || inv.fromUsername || inv.fromUuid;
|
||||
row.innerHTML = `<img class="friend-avatar" src="https://crafthead.net/cube/${escapeHtml(inv.fromUuid)}/64" alt="" />
|
||||
row.innerHTML = `<img class="friend-avatar" data-kopf="${escapeHtml(inv.fromUuid)}" alt="" />
|
||||
<div class="friend-info"><div class="friend-name">${escapeHtml(name)}</div><div class="friend-status">lädt dich ein · ${escapeHtml(inv.server)}${inv.version ? ' · ' + escapeHtml(inv.version) : ''}</div></div>
|
||||
<button class="btn btn-mini btn-primary" data-act="join">Beitreten</button>
|
||||
<button class="btn btn-mini" data-act="dismiss" title="Ablehnen">✕</button>`;
|
||||
@@ -4632,7 +5231,7 @@ async function ladeFreunde() {
|
||||
for (const r of anfragenRes.list) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'friend-row';
|
||||
row.innerHTML = `<img class="friend-avatar" src="https://crafthead.net/cube/${escapeHtml(r.uuid)}/64" alt="" />
|
||||
row.innerHTML = `<img class="friend-avatar" data-kopf="${escapeHtml(r.uuid)}" alt="" />
|
||||
<div class="friend-info"><div class="friend-name">${escapeHtml(r.username || r.uuid)}</div></div>
|
||||
<button class="btn btn-mini" data-act="accept">Annehmen</button>
|
||||
<button class="btn btn-mini btn-danger" data-act="decline">Ablehnen</button>`;
|
||||
@@ -4656,7 +5255,7 @@ async function ladeFreunde() {
|
||||
for (const r of gesendetRes.list) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'friend-row';
|
||||
row.innerHTML = `<img class="friend-avatar" src="https://crafthead.net/cube/${escapeHtml(r.uuid)}/64" alt="" />
|
||||
row.innerHTML = `<img class="friend-avatar" data-kopf="${escapeHtml(r.uuid)}" alt="" />
|
||||
<div class="friend-info"><div class="friend-name">${escapeHtml(r.username || r.uuid)}</div><div class="friend-status">wartet auf Bestätigung</div></div>
|
||||
<button class="btn btn-mini btn-danger" data-act="cancel">Zurückziehen</button>`;
|
||||
row.querySelector('.friend-avatar').addEventListener('error', function () { this.remove(); });
|
||||
@@ -4668,8 +5267,10 @@ async function ladeFreunde() {
|
||||
}
|
||||
}
|
||||
|
||||
setzeFreundKoepfe(); // Köpfe bei Einladungen/Anfragen
|
||||
const listBox = el('friends-list-box');
|
||||
listBox.innerHTML = '<div class="friends-section-title">Freunde</div>';
|
||||
if (!freundeRes.ok || !freundeRes.list.length) el('freund-buehne').classList.add('hidden');
|
||||
if (!freundeRes.ok) {
|
||||
listBox.innerHTML += `<div class="friends-empty">${escapeHtml(freundeRes.message || 'Nicht erreichbar.')}</div>`;
|
||||
return;
|
||||
@@ -4678,6 +5279,7 @@ async function ladeFreunde() {
|
||||
listBox.innerHTML += '<div class="friends-empty">Noch keine Freunde. Namen oben eingeben, um eine Anfrage zu senden.</div>';
|
||||
return;
|
||||
}
|
||||
let ersterFreund = null;
|
||||
const sortiert = [...freundeRes.list].sort((a, b) => {
|
||||
if (a.online !== b.online) return a.online ? -1 : 1;
|
||||
return (a.username || '').localeCompare(b.username || '');
|
||||
@@ -4692,8 +5294,10 @@ async function ladeFreunde() {
|
||||
: 'offline';
|
||||
const kannBeitreten = f.online && f.server && (!meinServer || meinServer.server !== f.server);
|
||||
const kannEinladen = f.online && meinServer && meinServer.server !== f.server;
|
||||
if (f.online && freundOnlineVorher.get(f.uuid) === false) row.classList.add('kommt-online');
|
||||
freundOnlineVorher.set(f.uuid, !!f.online);
|
||||
row.innerHTML = `<span class="friend-dot${f.online ? ' online' : ''}"></span>
|
||||
<img class="friend-avatar" src="https://crafthead.net/cube/${escapeHtml(f.uuid)}/64" alt="" />
|
||||
<img class="friend-avatar" data-kopf="${escapeHtml(f.uuid)}" alt="" />
|
||||
<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>` : ''}
|
||||
@@ -4701,6 +5305,11 @@ async function ladeFreunde() {
|
||||
<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(); });
|
||||
row.dataset.uuid = f.uuid;
|
||||
const zeigen = () => zeigeFreundFigur(f.uuid, anzeigeName, status, f.online);
|
||||
row.addEventListener('mouseenter', zeigen);
|
||||
row.addEventListener('click', (ev) => { if (!ev.target.closest('button')) zeigen(); });
|
||||
if (!ersterFreund || (f.online && !ersterFreund.online)) ersterFreund = { uuid: f.uuid, name: anzeigeName, status, online: f.online };
|
||||
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) {
|
||||
@@ -4731,11 +5340,21 @@ async function ladeFreunde() {
|
||||
row.querySelector('[data-act="remove"]').addEventListener('click', () => {
|
||||
askConfirm('Freund entfernen', `„${anzeigeName}" aus der Freundesliste entfernen?`, '🗑 Entfernen', async () => {
|
||||
await window.api.friendsRemove(f.uuid);
|
||||
if (freundBuehne && freundBuehne.uuid === f.uuid) freundBuehne.uuid = null;
|
||||
ladeFreunde();
|
||||
});
|
||||
});
|
||||
listBox.appendChild(row);
|
||||
}
|
||||
setzeFreundKoepfe();
|
||||
// Bühne: bisher gezeigter Freund bleibt, sonst der erste, der online ist
|
||||
const gezeigt = freundBuehne && freundBuehne.uuid && sortiert.find((f) => f.uuid === freundBuehne.uuid);
|
||||
if (gezeigt) {
|
||||
const z = document.querySelector(`.friend-row[data-uuid="${CSS.escape(gezeigt.uuid)}"]`);
|
||||
if (z) z.classList.add('gezeigt');
|
||||
} else if (ersterFreund) {
|
||||
zeigeFreundFigur(ersterFreund.uuid, ersterFreund.name, ersterFreund.status, ersterFreund.online);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -5044,6 +5663,31 @@ function sortInstalled(list) {
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// ---------- Leistungs-Paket ----------
|
||||
function installiereLeistungsPaket() {
|
||||
askConfirm('Leistungs-Paket installieren?',
|
||||
'AeroMC installiert bewährte Performance-Mods passend zu Minecraft-Version und Loader dieser Instanz: '
|
||||
+ 'Sodium (oder Embeddium), Lithium, FerriteCore, Entity Culling, ImmediatelyFast, ModernFix und Dynamic FPS – samt nötiger Abhängigkeiten. '
|
||||
+ 'Was es für diese Version nicht gibt oder schon installiert ist, wird übersprungen.',
|
||||
'Installieren', async () => {
|
||||
const b = el('m-leistung');
|
||||
b.disabled = true;
|
||||
b.textContent = '⚡ Wird vorbereitet …';
|
||||
const r = await window.api.modsLeistungsPaket(currentDetailId).catch((err) => ({ ok: false, message: err.message }));
|
||||
b.disabled = false;
|
||||
b.textContent = '⚡ Leistungs-Paket';
|
||||
if (!r.ok) { toast(r.message || 'Fehlgeschlagen.', true); return; }
|
||||
const teile = [];
|
||||
if (r.neu.length) teile.push(`Installiert: ${r.neu.join(', ')}`);
|
||||
if (r.schonDa.length) teile.push(`schon vorhanden: ${r.schonDa.join(', ')}`);
|
||||
if (r.nichtVerfuegbar.length) teile.push(`für diese Version nicht verfügbar: ${r.nichtVerfuegbar.join(', ')}`);
|
||||
if (r.uebersprungen.length) teile.push(`übersprungen: ${r.uebersprungen.join(', ')}`);
|
||||
toast(teile.join(' · ') || 'Nichts zu tun.', !r.neu.length && !r.schonDa.length);
|
||||
await refreshInstalled();
|
||||
await loadCatalog(false); // Katalog zeigt neue Mods als „✓ Installiert“
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshInstalled() {
|
||||
const list = sortInstalled(await window.api.modList(currentDetailId, modArt));
|
||||
currentModsInstalled = new Set(list.map((m) => m.projectId));
|
||||
@@ -5782,14 +6426,24 @@ async function kontoKopf(uuid) {
|
||||
return auftrag;
|
||||
}
|
||||
|
||||
let kontoKnopfUuid = null;
|
||||
async function refreshAccountButton() {
|
||||
const acc = await window.api.authStatus();
|
||||
const av = el('account-av');
|
||||
const uuid = acc && acc.profile ? acc.profile.id : null;
|
||||
const muenze = !!(kontoKnopfUuid && uuid && kontoKnopfUuid !== uuid && bewegungErlaubt() && !POPOUT);
|
||||
kontoKnopfUuid = uuid;
|
||||
const kopfBild = uuid ? await kontoKopf(uuid) : null; // vor dem Drehen fertig haben
|
||||
if (muenze) {
|
||||
// erste Hälfte: alter Kopf dreht sich weg (Kante zum Betrachter) …
|
||||
await av.animate([{ transform: 'perspective(140px) rotateY(0deg)' }, { transform: 'perspective(140px) translateY(-3px) rotateY(90deg)' }],
|
||||
{ duration: 170, easing: 'cubic-bezier(.5,0,.9,.6)' }).finished.catch(() => {});
|
||||
}
|
||||
if (acc && acc.profile) {
|
||||
av.innerHTML = '';
|
||||
const img = document.createElement('img');
|
||||
img.className = 'acct-head';
|
||||
img.src = await kontoKopf(acc.profile.id);
|
||||
img.src = kopfBild;
|
||||
img.addEventListener('error', () => { av.innerHTML = svgIcon('user'); });
|
||||
av.appendChild(img);
|
||||
el('account-name').textContent = acc.profile.name;
|
||||
@@ -5797,6 +6451,11 @@ async function refreshAccountButton() {
|
||||
av.innerHTML = svgIcon('user');
|
||||
el('account-name').textContent = 'Konten';
|
||||
}
|
||||
// … zweite Hälfte: der neue Kopf dreht sich herein und federt nach
|
||||
if (muenze) {
|
||||
av.animate([{ transform: 'perspective(140px) translateY(-3px) rotateY(-90deg)' }, { transform: 'perspective(140px) rotateY(12deg)', offset: 0.75 }, { transform: 'perspective(140px) rotateY(0deg)' }],
|
||||
{ duration: 300, easing: 'cubic-bezier(.2,.7,.3,1)' });
|
||||
}
|
||||
}
|
||||
|
||||
// aktualisiert Kontoliste im Login-Dialog und in den Einstellungen
|
||||
@@ -6383,6 +7042,9 @@ function wire() {
|
||||
el('srv-ip').addEventListener('keydown', (e) => { if (e.key === 'Enter') addServer(); });
|
||||
el('srv-copy-do').addEventListener('click', doServerCopy);
|
||||
el('login-skin').addEventListener('click', openSkin);
|
||||
el('skin-armform-fix').addEventListener('click', korrigiereArmform);
|
||||
el('gard-skin-speichern').addEventListener('click', () => garderobeSpeichern('skin'));
|
||||
el('gard-umhang-speichern').addEventListener('click', () => garderobeSpeichern('umhang'));
|
||||
el('skin-pick').addEventListener('click', waehleSkinDatei);
|
||||
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
|
||||
el('skin-cape-offiziell').addEventListener('change', waehleOffiziellenUmhang);
|
||||
@@ -6501,6 +7163,11 @@ function wire() {
|
||||
el('m-installed-sort').addEventListener('change', (e) => { installedSort = e.target.value; refreshInstalled(); });
|
||||
el('m-art').addEventListener('change', modArtGewechselt);
|
||||
el('m-open-folder').addEventListener('click', () => window.api.openGameDir(currentDetailId));
|
||||
el('m-leistung').addEventListener('click', installiereLeistungsPaket);
|
||||
window.api.onPaketFortschritt((p) => {
|
||||
const b = el('m-leistung');
|
||||
if (b.disabled) b.textContent = `⚡ ${p.nr}/${p.von}: ${p.titel} …`;
|
||||
});
|
||||
// Automatische Sicherung: unaufdringlich melden, Fehler deutlich
|
||||
window.api.onAutoBackup((d) => {
|
||||
if (d.ok) toast(d.anlass === 'vor-dem-start' ? 'Sicherung vor dem Start erstellt.' : 'Automatische Sicherung erstellt.');
|
||||
@@ -6855,7 +7522,7 @@ const POPOUT_OPENERS = {
|
||||
},
|
||||
record: () => openRecording(),
|
||||
friends: () => openFriends(),
|
||||
stats: () => openStats(),
|
||||
stats: (p) => openStats(p.reiter),
|
||||
};
|
||||
|
||||
function popoutBoot(params) {
|
||||
@@ -6944,6 +7611,13 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
if (POPOUT === 'friends') window.api.onFriendsMessage(() => (chatPartner ? ladeChat() : ladeFreunde()));
|
||||
await refreshSkinBackground();
|
||||
window.api.onLaunchProgress(onLaunchEvt);
|
||||
window.api.onErfolgeNeu(zeigeErfolgMeldung);
|
||||
el('upd-notes').addEventListener('click', updateLinkKlick);
|
||||
window.api.onLaufendeInstanzen(setzeLaufende);
|
||||
window.api.laufendeInstanzen().then(setzeLaufende).catch(() => {});
|
||||
el('grid').addEventListener('mousemove', kippeZurMaus);
|
||||
baueLichtpunkte();
|
||||
document.querySelectorAll('[data-stats-reiter]').forEach((b) => b.addEventListener('click', () => waehleStatistikReiter(b.dataset.statsReiter)));
|
||||
// Servername live in den Titel nachtragen, falls das Aufzeichnungs-Fenster
|
||||
// schon offen war, BEVOR die Verbindung zum Server erkannt wurde
|
||||
window.api.onRecordingServerDetected(({ id }) => {
|
||||
@@ -6999,9 +7673,10 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||
// Hauptfenster (Skin-Hintergrund, Seitenleiste) – Popups zeigen beides nicht an
|
||||
window.api.onSkinChanged((info) => {
|
||||
kontoKoepfe.clear();
|
||||
if (!POPOUT) { refreshSkinBackground(); refreshAccountButton(); }
|
||||
const auftritt = info.quelle === 'umhang' ? 'flattern' : 'drehen';
|
||||
if (!POPOUT) { merkeSkinAuftritt(auftritt); refreshSkinBackground(); refreshAccountButton(); }
|
||||
// im Editor gesetzt -> offenes Skin-Fenster zeigt sonst noch den alten Stand
|
||||
if (POPOUT === 'skin' && (info.quelle === 'editor' || info.quelle === 'umhang') && !el('modal-skin').classList.contains('hidden')) openSkin();
|
||||
if (POPOUT === 'skin' && (info.quelle === 'editor' || info.quelle === 'umhang') && !el('modal-skin').classList.contains('hidden')) { merkeSkinAuftritt(auftritt); openSkin(); }
|
||||
});
|
||||
window.api.onInstancesChanged(() => { if (!POPOUT) reload(); });
|
||||
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
|
||||
|
||||
Reference in New Issue
Block a user