Upload via GUI (39 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-02 17:01:18 +02:00
parent a803f297ea
commit eff6403dd5
26 changed files with 3536 additions and 134 deletions
+578 -25
View File
@@ -608,9 +608,12 @@ async function runModpackImport() {
await reload();
hide('modal-import');
toast(`Modpack importiert: ${res.instance.name} (${res.fileCount} Dateien)`);
// CurseForge gibt nicht jede Datei heraus ehrlich benennen statt verschweigen
if (res.fehlend) toast(`${res.fehlend} Datei(en) konnten nicht geladen werden und fehlen in der Instanz.`, true);
return;
}
toast(res.message || 'Modpack-Import fehlgeschlagen.', true);
if (res.needsKey) { hide('modal-import'); openSettings('pane-network'); }
} finally {
status('Bereit'); el('status-right').textContent = '';
el('import-modpack-do').disabled = false;
@@ -960,19 +963,40 @@ function formatVersionLabel(version) {
// ---------- Automatische Updates ----------
let pendingUpdate = null;
const CHANNEL_LABEL = { portable: 'Portable', setup: 'Setup' };
function showUpdateDialog(info) {
pendingUpdate = info;
const channel = CHANNEL_LABEL[info.installKind] || null;
el('upd-current').textContent = formatVersionLabel(info.currentVersion);
el('upd-new').textContent = formatVersionLabel(info.latestVersion);
el('upd-channel').textContent = `Update-Kanal: ${info.installKind === 'portable' ? 'Portable' : 'Setup'}`;
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.';
el('upd-progress').classList.add('hidden');
el('upd-bar-fill').style.width = '0%';
el('upd-install').disabled = !info.asset;
el('upd-install').textContent = info.asset ? 'Installieren' : `Kein passendes ${info.installKind === 'portable' ? 'Portable' : 'Setup'}-Update im Release`;
// Ohne passendes Paket wird bewusst nichts installiert ein Portable-Launcher
// darf nie einen Installer bekommen und umgekehrt. Statt eines toten Knopfes
// führen wir dann zum Release, damit der Nutzer nicht in der Sackgasse steht.
const hatPaket = !!info.asset;
el('upd-install').classList.toggle('hidden', !hatPaket);
el('upd-install').disabled = !hatPaket;
el('upd-install').textContent = 'Installieren';
el('upd-open').classList.toggle('hidden', hatPaket || !info.url);
el('upd-hint').textContent = hatPaket
? 'Der Launcher lädt das Update herunter und startet die Installation automatisch.'
: (channel
? `Für den Kanal ${channel} liegt in diesem Release keine passende Datei bereit. Bitte im Release nachsehen und von Hand aktualisieren.`
: 'Die Paketart dieses Launchers lässt sich nicht sicher bestimmen. Bitte das Update von Hand aus dem Release laden.');
show('modal-update');
}
function openReleasePage() {
if (pendingUpdate && pendingUpdate.url) window.api.openExternal(pendingUpdate.url);
}
async function startUpdate() {
if (!pendingUpdate || !pendingUpdate.asset) return;
const btn = el('upd-install');
@@ -983,7 +1007,9 @@ async function startUpdate() {
const res = await window.api.updateInstall(pendingUpdate.asset);
if (res.ok) {
el('upd-status').textContent = 'Installation wird gestartet der Launcher schließt sich.';
el('upd-status').textContent = res.kind === 'portable'
? 'Launcher schließt sich, wird ersetzt und startet automatisch neu.'
: 'Installation wird gestartet der Launcher schließt sich.';
} else {
el('upd-status').textContent = '✖ ' + (res.message || 'Update fehlgeschlagen.');
btn.disabled = false;
@@ -1033,7 +1059,7 @@ async function applySkinBackground() {
// Jede Pose hat auch eine Entsprechung beim 3D-Dienst (Kamerawinkel/Ansicht),
// damit die Auswahl immer sichtbar wirkt auch wenn der Pose-Dienst offline ist.
// 1) Skin lokal in 3D rendern unabhängig von Render-Diensten
const local = await renderSkin3D(uuid, s.bgSkinPose || 'walking');
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking');
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
// 2) Rückfall: externe Render-Dienste
@@ -1108,13 +1134,22 @@ const SKIN_POSES = {
};
// Rendert den Skin lokal mit WebGL in der gewählten Pose
async function renderSkin3D(uuid, poseKey) {
/*
* Rendert eine Skin-Textur als Spielfigur und gibt ein Bild zurück.
* quelle: Adresse ODER data:-URI so lässt sich auch eine Datei anzeigen,
* die noch gar nicht hochgeladen ist.
* modell: 'default' (breite Arme) oder 'slim' (Alex)
*/
async function renderSkin3D(quelle, poseKey, opts = {}) {
if (typeof skinview3d === 'undefined') return null;
const canvas = el('skin-canvas');
const breite = opts.breite || 420;
const hoehe = opts.hoehe || 700;
try {
const viewer = new skinview3d.SkinViewer({
canvas, width: 420, height: 700,
skin: `https://crafthead.net/skin/${uuid}`,
canvas, width: breite, height: hoehe,
skin: quelle,
model: opts.modell === 'slim' ? 'slim' : 'default',
preserveDrawingBuffer: true,
});
// Auf das Laden der Skin-Textur warten
@@ -1179,7 +1214,7 @@ function applyIcons(root = document) {
// ---------- Instanz-Kacheln ----------
const SINGLE_SELECTION_BUTTON_IDS = [
'p-play', 'p-offline', 'p-edit', 'p-notes', 'p-mods', 'p-worlds', 'p-screens',
'p-play', 'p-offline', 'p-edit', 'p-notes', 'p-mods', 'p-worlds', 'p-screens', 'p-logs', 'p-servers',
'p-mcfolder', 'p-configfolder', 'p-instfolder', 'p-shortcut', 'p-export', 'p-copy',
];
@@ -1575,6 +1610,7 @@ async function openEdit(inst) {
}
el('f-min-mem').value = inst.java.minMemMb || '';
el('f-max-mem').value = inst.java.maxMemMb || '';
zeigeSpeicherHinweis();
el('f-extra-args').value = inst.java.extraArgs || '';
el('f-prelaunch').value = inst.preLaunchCommand || '';
el('f-postexit').value = inst.postExitCommand || '';
@@ -1604,12 +1640,21 @@ async function saveInstance() {
loaderVersion: el('f-loader-version').value.trim(),
},
};
// Speicherangaben prüfen, bevor gespeichert wird
const minMem = numOrNull(el('f-min-mem').value);
const maxMem = numOrNull(el('f-max-mem').value);
if (minMem || maxMem) {
const pruefung = await window.api.checkMemory(minMem, maxMem);
if (!pruefung.ok) { toast(pruefung.message, true); return; }
if (pruefung.warnung) toast(pruefung.warnung, false);
}
try {
let targetId = editingId;
if (editingId) {
payload.java = {
minMemMb: numOrNull(el('f-min-mem').value),
maxMemMb: numOrNull(el('f-max-mem').value),
minMemMb: minMem,
maxMemMb: maxMem,
extraArgs: el('f-extra-args').value.trim(),
};
payload.preLaunchCommand = el('f-prelaunch').value.trim();
@@ -1666,22 +1711,348 @@ async function launch(id, accountId) {
}
}
// ---------- Arbeitsspeicher ----------
let systemSpeicher = null;
async function zeigeSpeicherHinweis() {
const feld = el('f-mem-hint');
if (!feld) return;
try {
if (!systemSpeicher) systemSpeicher = await window.api.systemMemory();
} catch { return; }
const gesamt = systemSpeicher.totalMb;
const empfohlen = Math.floor(gesamt * 0.5 / 256) * 256;
feld.textContent = `Im Rechner stecken ${gesamt} MB. Leer lassen übernimmt die globale Vorgabe. `
+ `Für die meisten Modpacks reichen 40968192 MB; mehr als ${Math.floor(gesamt * 0.8 / 256) * 256} MB sollten es nicht sein `
+ `(Vorschlag: ${empfohlen} MB).`;
}
// ---------- Serverliste ----------
let zuUebertragenderServer = null;
async function openServers() {
if (!currentDetailId) return;
const inst = await window.api.getInstance(currentDetailId);
el('servers-title').textContent = 'Server ' + (inst ? inst.name : '');
el('srv-name').value = '';
el('srv-ip').value = '';
show('modal-servers');
await renderServers();
}
async function renderServers() {
const box = el('srv-list');
box.innerHTML = '<div class="mods-empty">Lade …</div>';
const res = await window.api.serversList(currentDetailId);
box.innerHTML = '';
if (!res.ok) { box.innerHTML = `<div class="mods-empty">${escapeHtml(res.message || 'Liste nicht lesbar.')}</div>`; return; }
if (!res.server.length) {
box.innerHTML = '<div class="mods-empty">Noch keine Server eingetragen.</div>';
return;
}
for (const s of res.server) {
const item = document.createElement('div');
item.className = 'mod-item';
item.innerHTML =
`<div class="mod-icon">🖥</div>
<div class="mod-info">
<div class="mod-name">${escapeHtml(s.name || s.ip)}</div>
<div class="mod-meta">${escapeHtml(s.ip)}</div>
</div>
<div class="mod-action"></div>`;
const aktionen = item.querySelector('.mod-action');
const kopieren = document.createElement('button');
kopieren.className = 'btn btn-mini';
kopieren.textContent = 'Übertragen…';
kopieren.title = 'Diesen Server in andere Instanzen eintragen';
kopieren.addEventListener('click', () => openServerCopy(s));
aktionen.appendChild(kopieren);
const weg = document.createElement('button');
weg.className = 'btn btn-danger btn-mini';
weg.textContent = 'Entfernen';
weg.addEventListener('click', async () => {
const r = await window.api.serversRemove(currentDetailId, s.ip);
if (r.ok) toast('Entfernt: ' + (s.name || s.ip));
else toast(r.message || 'Entfernen fehlgeschlagen.', true);
await renderServers();
});
aktionen.appendChild(weg);
box.appendChild(item);
}
}
async function addServer() {
const eintrag = { name: el('srv-name').value.trim(), ip: el('srv-ip').value.trim() };
const res = await window.api.serversAdd(currentDetailId, eintrag);
if (!res.ok) { toast(res.message || 'Hinzufügen fehlgeschlagen.', true); return; }
el('srv-name').value = '';
el('srv-ip').value = '';
toast('Server hinzugefügt.');
await renderServers();
}
async function openServerCopy(server) {
zuUebertragenderServer = server;
el('srv-copy-sub').textContent = `${server.name || server.ip}" (${server.ip}) in diese Instanzen eintragen:`;
const box = el('srv-copy-list');
box.innerHTML = '';
const alle = await window.api.listInstances();
const andere = alle.filter((i) => i.id !== currentDetailId);
if (!andere.length) {
box.innerHTML = '<div class="mods-empty">Es gibt keine weitere Instanz.</div>';
}
for (const i of andere) {
const zeile = document.createElement('label');
zeile.className = 'checkbox-field';
zeile.style.padding = '6px 2px';
zeile.innerHTML = `<input type="checkbox" value="${escapeHtml(i.id)}" /> <span>${escapeHtml(i.name)}</span>`;
box.appendChild(zeile);
}
show('modal-srv-copy');
}
async function doServerCopy() {
if (!zuUebertragenderServer) return;
const ids = [...el('srv-copy-list').querySelectorAll('input:checked')].map((c) => c.value);
if (!ids.length) { toast('Keine Instanz ausgewählt.', true); return; }
const res = await window.api.serversCopyTo(ids, zuUebertragenderServer);
hide('modal-srv-copy');
if (res.erledigt.length) toast(`Eingetragen in: ${res.erledigt.join(', ')}`);
for (const f of res.fehler) toast(`${f.name || f.id}: ${f.message}`, true);
}
// ---------- Skin ändern ----------
let gewaehlterSkin = null;
/*
* Bild nur zeigen, wenn es wirklich geladen wurde ein <img> ohne Quelle
* stellt sonst das Platzhalter-Symbol des Browsers dar.
*/
function zeigeSkinBild(bildId, leerId, quelle, leerText) {
const bild = el(bildId);
const leer = el(leerId);
leer.textContent = leerText;
if (!quelle) {
bild.classList.add('hidden');
bild.removeAttribute('src');
leer.classList.remove('hidden');
return;
}
bild.onload = () => { bild.classList.remove('hidden'); leer.classList.add('hidden'); };
bild.onerror = () => {
bild.classList.add('hidden');
bild.removeAttribute('src');
leer.textContent = 'Vorschau nicht ladbar';
leer.classList.remove('hidden');
};
bild.src = quelle;
}
// Textur einer Seite merken, damit bei einem Wechsel der Armform beide
// Figuren neu gerendert werden können
let skinTexturAktuell = '';
let skinTexturNeu = '';
// Skin-Textur als Figur rendern; klappt das nicht, wird die Textur gezeigt
async function zeigeSkinFigur(bildId, leerId, textur, leerText) {
if (!textur) { zeigeSkinBild(bildId, leerId, '', leerText); return; }
el(leerId).textContent = 'wird gerendert …';
const modell = el('skin-variante').value === 'slim' ? 'slim' : 'default';
const figur = await renderSkin3D(textur, 'standing', { modell, breite: 260, hoehe: 420 });
zeigeSkinBild(bildId, leerId, figur || textur, leerText);
}
async function openSkin() {
gewaehlterSkin = null;
skinTexturAktuell = '';
skinTexturNeu = '';
el('skin-pfad').value = '';
el('skin-apply').disabled = true;
el('skin-status').textContent = '';
zeigeSkinBild('skin-neu', 'skin-neu-leer', '', 'noch keine Datei gewählt');
zeigeSkinBild('skin-aktuell', 'skin-aktuell-leer', '', 'wird geladen …');
el('skin-konto').textContent = 'Profil wird geladen …';
show('modal-skin');
const p = await window.api.skinProfile();
if (!p.ok) {
el('skin-konto').textContent = p.message || 'Profil konnte nicht geladen werden.';
zeigeSkinBild('skin-aktuell', 'skin-aktuell-leer', '', 'kein Profil geladen');
return;
}
el('skin-konto').textContent = `Angemeldet als ${p.name}. Die Änderung gilt für dieses Konto.`;
// 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 || '';
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell,
'Standard-Skin (keine eigene Datei hinterlegt)');
}
// Armform gewechselt -> beide Figuren neu zeichnen
async function skinVarianteGewechselt() {
if (skinTexturAktuell) await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, '');
if (skinTexturNeu) await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, '');
}
async function waehleSkinDatei() {
const res = await window.api.skinChoose();
if (res.canceled) return;
if (!res.ok) { el('skin-status').textContent = '✖ ' + res.message; return; }
gewaehlterSkin = res.pfad;
el('skin-pfad').value = res.pfad;
skinTexturNeu = res.dataUri || '';
await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, 'Vorschau nicht verfügbar');
el('skin-status').textContent = `${res.breite}×${res.hoehe} Pixel sieht gut aus.`;
el('skin-apply').disabled = false;
}
async function setzeSkin() {
if (!gewaehlterSkin) return;
const btn = el('skin-apply');
btn.disabled = true;
el('skin-status').textContent = 'Wird hochgeladen …';
const res = await window.api.skinApply(gewaehlterSkin, el('skin-variante').value);
if (res.ok) {
el('skin-status').textContent = '✔ Skin gesetzt. Im Spiel kann es einen Moment dauern.';
toast('Skin geändert.');
await openSkin(); // Vorschau mit dem neuen Stand auffrischen
await applySkinBackground(); // auch der Hintergrund zeigt jetzt den neuen Skin
} else {
el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
btn.disabled = false;
}
}
async function setzeSkinZurueck() {
const btn = el('skin-reset');
btn.disabled = true;
el('skin-status').textContent = 'Wird zurückgesetzt …';
const res = await window.api.skinReset();
if (res.ok) {
toast('Skin auf Standard zurückgesetzt.');
await openSkin();
await applySkinBackground();
} else {
el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
}
btn.disabled = false;
}
// ---------- Logdateien ----------
let aktuellesLog = '';
async function openLogs() {
if (!currentDetailId) return;
const inst = await window.api.getInstance(currentDetailId);
el('logs-title').textContent = 'Logdateien ' + (inst ? inst.name : '');
const auswahl = el('logs-select');
auswahl.innerHTML = '';
el('logs-text').textContent = 'Lade …';
el('logs-info').textContent = '';
aktuellesLog = '';
show('modal-logs');
const dateien = await window.api.logsList(currentDetailId);
if (!dateien.length) {
el('logs-text').textContent = 'Für diese Instanz gibt es noch keine Logdateien. '
+ 'Sie entstehen beim ersten Spielstart.';
return;
}
for (const d of dateien) {
const o = document.createElement('option');
o.value = d.name;
const kb = Math.max(1, Math.round(d.groesse / 1024));
o.textContent = `${d.name} · ${kb} KB · ${new Date(d.geaendert).toLocaleString('de-DE')}`
+ (d.gepackt ? ' · gepackt' : '');
auswahl.appendChild(o);
}
await ladeLog(dateien[0].name);
}
async function ladeLog(name) {
el('logs-text').textContent = 'Lade …';
const res = await window.api.logsRead(currentDetailId, name);
if (!res.ok) {
el('logs-text').textContent = 'Datei konnte nicht gelesen werden.';
el('logs-info').textContent = '';
aktuellesLog = '';
return;
}
aktuellesLog = res.text;
el('logs-text').textContent = res.text || '(leer)';
el('logs-info').textContent = res.gekuerzt
? `${res.zeilenGesamt} Zeilen die letzten 5000 werden gezeigt`
: `${res.zeilenGesamt} Zeilen`;
// ans Ende springen: dort steht, was zuletzt passierte
const feld = el('logs-text');
feld.scrollTop = feld.scrollHeight;
}
// ---------- Absturzberichte ----------
let letzterStartMs = 0;
let aktuellerCrash = null;
async function zeigeAbsturzbericht(instanzId) {
const id = instanzId || currentDetailId;
if (!id) 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); }
catch { return; }
if (!res || !res.ok) return; // kein Bericht -> Konsole muss reichen
aktuellerCrash = res;
const inst = await window.api.getInstance(id);
el('crash-sub').textContent = `${inst ? inst.name + ' · ' : ''}${res.datei} · `
+ new Date(res.zeitpunkt).toLocaleString('de-DE');
const zeile = (titel, wert) => wert
? `<div class="crash-zeile"><div class="crash-titel">${titel}</div><div class="crash-wert">${escapeHtml(wert)}</div></div>`
: '';
let html = zeile('Beschreibung', res.beschreibung) + zeile('Fehler', res.fehler)
+ zeile('Verdächtige Mods', res.verdaechtigeMods);
if (res.verlauf && res.verlauf.length) {
html += `<div class="crash-zeile"><div class="crash-titel">Aufrufverlauf</div>`
+ `<div class="crash-wert"><div class="crash-verlauf">${escapeHtml(res.verlauf.join('\n'))}</div></div></div>`;
}
el('crash-kern').innerHTML = html || '<div class="hint">Der Bericht enthält keine der üblichen Kennzeichen.</div>';
el('crash-voll').textContent = res.text || '';
show('modal-crash');
}
// Fortschrittsmeldungen der Download-/Start-Pipeline
function onLaunchEvt(evt) {
const labels = { version: 'Version', client: 'Client', libraries: 'Libraries', assets: 'Assets', loader: 'Loader', java: 'Java', launch: 'Start', modpack: 'Modpack' };
const labels = { version: 'Version', client: 'Client', libraries: 'Libraries', assets: 'Assets', loader: 'Loader', java: 'Java', launch: 'Start', modpack: 'Modpack', backup: 'Sicherung' };
if (evt.phase === 'log') { appendConsole(evt.line); return; }
if (evt.phase === 'exit') {
status('Minecraft beendet (Code ' + evt.code + ')');
el('status-right').textContent = '';
appendConsole('\n[Prozess beendet, Code ' + evt.code + ']\n');
// Konsole je nach Einstellung schließen bzw. bei Absturz zeigen
const crashed = evt.code !== 0 && evt.code !== null;
// Absturzbericht suchen, bevor über die Konsole entschieden wird
// ein Bericht sagt mehr als der Java-Auswurf
if (crashed) zeigeAbsturzbericht(evt.id);
window.api.getSettings().then((s) => {
const crashed = evt.code !== 0 && evt.code !== null;
if (crashed && s.showConsoleOnCrash !== false) { openConsole(); return; }
if (s.autoCloseConsole) hide('modal-console');
}).catch(() => {});
return;
}
// Startzeitpunkt merken: nur Berichte ab hier gehören zu diesem Lauf
if (evt.phase === 'launch') letzterStartMs = Date.now();
const label = labels[evt.phase] || evt.phase;
if (evt.total) {
status(`${label}${evt.current}/${evt.total}`);
@@ -1815,6 +2186,11 @@ async function openMods() {
['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; });
el('m-query').value = '';
el('m-more').classList.add('hidden');
// Ergebnisse der vorigen Instanz nicht mitschleppen
modUpdates = new Map();
setUpdateStatus('');
el('m-apply-updates').classList.add('hidden');
el('m-check-updates').disabled = blocked;
switchModTab('catalog');
await refreshInstalled();
@@ -1846,6 +2222,9 @@ function switchModTab(which) {
el('m-pane-installed').classList.toggle('hidden', cat);
}
// gefundene Aktualisierungen: projectId -> { title, alt, neu }
let modUpdates = new Map();
async function refreshInstalled() {
const list = await window.api.modList(currentDetailId);
currentModsInstalled = new Set(list.map((m) => m.projectId));
@@ -1854,15 +2233,46 @@ async function refreshInstalled() {
box.innerHTML = '';
if (!list.length) { box.innerHTML = '<div class="mods-empty">Noch keine Mods.</div>'; return; }
for (const m of list) {
const up = m.projectId ? modUpdates.get(m.projectId) : null;
const aus = m.enabled === false;
const item = document.createElement('div');
item.className = 'mod-item';
item.className = 'mod-item' + (aus ? ' mod-aus' : '');
const kennzeichen = [];
if (up) kennzeichen.push('<span class="mod-new">Update</span>');
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
item.innerHTML =
`<div class="mod-icon">🧩</div>
`<div class="mod-icon">${aus ? '🚫' : '🧩'}</div>
<div class="mod-info">
<div class="mod-name">${escapeHtml(m.title)}</div>
<div class="mod-meta">${escapeHtml(m.versionNumber || '')} · ${escapeHtml(m.filename)}</div>
<div class="mod-name">${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}</div>
<div class="mod-meta">${up
? escapeHtml(up.alt.versionNumber) + ' → ' + escapeHtml(up.neu.versionNumber)
: escapeHtml(m.versionNumber || '') + ' · ' + escapeHtml(m.filename)}</div>
</div>
<div class="mod-action"></div>`;
const aktionen = item.querySelector('.mod-action');
// Ab-/Anschalten: die Datei bleibt liegen, Minecraft übergeht sie nur
if (!m.fehlt) {
const schalter = document.createElement('button');
schalter.className = 'btn btn-mini';
schalter.textContent = aus ? 'Einschalten' : 'Ausschalten';
schalter.title = aus
? 'Datei wieder auf .jar umbenennen'
: 'Datei auf .jar.disabled umbenennen Minecraft lädt sie dann nicht';
schalter.addEventListener('click', async () => {
schalter.disabled = true;
const res = await window.api.modSetEnabled(currentDetailId, m.filename, aus);
if (res.ok) toast((aus ? 'Eingeschaltet: ' : 'Ausgeschaltet: ') + m.title);
else toast(res.message || 'Umschalten fehlgeschlagen.', true);
await refreshInstalled();
});
aktionen.appendChild(schalter);
}
const btn = document.createElement('button');
btn.className = 'btn btn-danger btn-mini';
btn.textContent = 'Entfernen';
@@ -1871,11 +2281,61 @@ async function refreshInstalled() {
toast('Entfernt: ' + m.title);
await refreshInstalled();
});
item.querySelector('.mod-action').appendChild(btn);
aktionen.appendChild(btn);
box.appendChild(item);
}
}
// ---------- Mod-Aktualisierung ----------
function setUpdateStatus(text) { el('m-update-status').textContent = text || ''; }
async function checkModUpdates() {
const btn = el('m-check-updates');
btn.disabled = true;
el('m-apply-updates').classList.add('hidden');
setUpdateStatus('Suche …');
try {
const res = await window.api.modCheckUpdates(currentDetailId);
if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Suche fehlgeschlagen.')); return; }
modUpdates = new Map((res.treffer || []).map((t) => [t.projectId, t]));
await refreshInstalled();
const teile = [];
if (!res.treffer.length) teile.push('Alles aktuell');
else teile.push(res.treffer.length + (res.treffer.length === 1 ? ' Update verfügbar' : ' Updates verfügbar'));
teile.push(res.geprueft + ' geprüft');
// von Hand abgelegte Dateien lassen sich nicht zuordnen
if (res.uebersprungen) teile.push(res.uebersprungen + ' ohne Quelle übersprungen');
setUpdateStatus(teile.join(' · '));
el('m-apply-updates').classList.toggle('hidden', !res.treffer.length);
} finally {
btn.disabled = false;
}
}
async function applyModUpdates() {
if (!modUpdates.size) return;
const btn = el('m-apply-updates');
btn.disabled = true;
setUpdateStatus('Wird aktualisiert …');
try {
const res = await window.api.modApplyUpdates(currentDetailId, [...modUpdates.keys()]);
if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.')); return; }
modUpdates = new Map();
await refreshInstalled();
const n = res.erneuert.length;
setUpdateStatus(n + (n === 1 ? ' Mod aktualisiert' : ' Mods aktualisiert')
+ (res.fehler.length ? ' · ' + res.fehler.length + ' fehlgeschlagen' : ''));
if (n) toast(n + (n === 1 ? ' Mod aktualisiert.' : ' Mods aktualisiert.'));
for (const f of res.fehler) toast(f.title + ': ' + f.message, true);
btn.classList.add('hidden');
} finally {
btn.disabled = false;
}
}
let modOffset = 0;
async function loadCatalog(append) {
const inst = await window.api.getInstance(currentDetailId);
@@ -1973,8 +2433,9 @@ function setSettingsPane(paneId) {
document.querySelectorAll('.spane').forEach((pane) => pane.classList.toggle('hidden', pane.id !== paneId));
}
async function openSettings() {
setSettingsPane('pane-general');
// pane: optionaler Bereich, der direkt geöffnet werden soll (z. B. 'pane-network')
async function openSettings(pane) {
setSettingsPane(pane || 'pane-general');
const s = await window.api.getSettings();
el('s-theme').value = s.theme || 'dark';
@@ -1996,6 +2457,14 @@ async function openSettings() {
el('s-sortby').value = s.sortBy || 'lastPlayed';
el('s-iso3d').checked = s.iso3dIcons !== false;
el('s-bedrock').checked = s.bedrockEnabled !== false;
el('s-auto-java').checked = s.autoJava !== false;
el('s-autobackup').checked = !!s.autoBackupEnabled;
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-autobackup-last').textContent = s.autoBackupLast
? 'Zuletzt automatisch gesichert: ' + new Date(s.autoBackupLast).toLocaleString('de-DE')
: 'Noch keine automatische Sicherung.';
el('s-console-launch').checked = !!s.showConsoleOnLaunch;
el('s-console-autoclose').checked = !!s.autoCloseConsole;
el('s-console-crash').checked = s.showConsoleOnCrash !== false;
@@ -2010,6 +2479,9 @@ async function openSettings() {
el('s-proxy-port').value = s.proxyPort || '';
el('s-proxy-user').value = s.proxyUser || '';
el('s-proxy-pass').value = s.proxyPass || '';
el('s-cf-key').value = s.curseforgeApiKey || '';
el('s-cf-proxy').value = s.curseforgeProxyUrl || '';
el('s-cf-proxy-token').value = s.curseforgeProxyToken || '';
el('s-backup-status').className = 'hint';
el('s-backup-status').textContent = 'Noch kein Backup erstellt.';
el('s-backup-global').disabled = false;
@@ -2086,6 +2558,13 @@ async function scanJava() {
}
async function saveSettings() {
// globale Speicher-Vorgaben genauso prüfen wie die je Instanz
const gMin = numOrNull(el('s-min-mem').value) || 2048;
const gMax = numOrNull(el('s-max-mem').value) || 4096;
const pruefung = await window.api.checkMemory(gMin, gMax);
if (!pruefung.ok) { toast(pruefung.message, true); setSettingsPane('pane-minecraft'); return; }
if (pruefung.warnung) toast(pruefung.warnung, false);
const patch = {
theme: el('s-theme').value,
globalPreLaunch: el('s-global-pre').value.trim(),
@@ -2095,6 +2574,11 @@ async function saveSettings() {
sortBy: el('s-sortby').value,
iso3dIcons: el('s-iso3d').checked,
bedrockEnabled: el('s-bedrock').checked,
autoJava: el('s-auto-java').checked,
autoBackupEnabled: el('s-autobackup').checked,
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,
showConsoleOnLaunch: el('s-console-launch').checked,
autoCloseConsole: el('s-console-autoclose').checked,
showConsoleOnCrash: el('s-console-crash').checked,
@@ -2109,6 +2593,9 @@ async function saveSettings() {
proxyPort: el('s-proxy-port').value.trim(),
proxyUser: el('s-proxy-user').value.trim(),
proxyPass: el('s-proxy-pass').value,
curseforgeApiKey: el('s-cf-key').value.trim(),
curseforgeProxyUrl: el('s-cf-proxy').value.trim(),
curseforgeProxyToken: el('s-cf-proxy-token').value.trim(),
javaPath: el('s-java').value.trim(),
defaultMinMemMb: numOrNull(el('s-min-mem').value) || 2048,
defaultMaxMemMb: numOrNull(el('s-max-mem').value) || 4096,
@@ -2421,11 +2908,16 @@ async function renderAccounts(boxId = 'login-accounts') {
av.replaceWith(d);
});
// "spielt" nur, wenn wirklich ein Spiel dieses Kontos läuft; "aktiv" heißt
// lediglich: dieses Konto wird für den nächsten Start verwendet
const merkmale = [];
if (a.active) merkmale.push('aktiv');
if (a.playing) merkmale.push('spielt gerade');
const main = document.createElement('div');
main.className = 'account-main';
main.innerHTML =
`<div class="account-name">${escapeHtml(a.name)}</div>
<div class="account-tag">${a.active ? '● aktiv (spielt)' : ''}</div>`;
<div class="account-tag">${merkmale.length ? '● ' + merkmale.join(' · ') : ''}</div>`;
const actions = document.createElement('div');
actions.className = 'account-actions';
@@ -2457,10 +2949,16 @@ async function renderAccounts(boxId = 'login-accounts') {
}
}
// steht wortgleich in index.html von dort wird er beim Öffnen zurückgesetzt
const LOGIN_HINWEIS = 'Melde dich mit deinem Microsoft-Konto an (Minecraft Java Edition nötig). '
+ 'Das aktive Konto wird beim nächsten Start verwendet. Für zwei Konten gleichzeitig: '
+ 'eine Instanz starten, hier das zweite Konto aktivieren und eine weitere Instanz starten '
+ 'laufende Spiele behalten ihr Konto.';
function resetLoginHint() {
const h = el('login-hint');
h.classList.remove('login-error');
h.textContent = 'Melde dich mit deinem Microsoft-Konto an (Minecraft Java Edition nötig). Mehrere Konten sind möglich das aktive wird zum Spielen verwendet.';
h.textContent = LOGIN_HINWEIS;
}
async function openLogin() {
@@ -2517,7 +3015,8 @@ function askConfirm(title, text, okLabel, action) {
function wire() {
el('btn-new').addEventListener('click', openCreate);
el('btn-new-2').addEventListener('click', openCreate);
el('btn-settings').addEventListener('click', openSettings);
// ohne Wrapper käme das Klick-Ereignis als Bereichsname an
el('btn-settings').addEventListener('click', () => openSettings());
el('search').addEventListener('input', render);
el('btn-open-dir').addEventListener('click', () => {
@@ -2674,13 +3173,38 @@ function wire() {
// Rechte Aktionsleiste (Instanz-Aktionen)
el('p-play').addEventListener('click', () => currentDetailId && launch(currentDetailId));
el('p-offline').addEventListener('click', () => currentDetailId && launch(currentDetailId));
el('p-shortcut').addEventListener('click', () => toast('Verknüpfung erstellen folgt in einem Update.', false));
el('p-shortcut').addEventListener('click', async () => {
if (!currentDetailId) return;
const res = await window.api.createShortcut(currentDetailId);
if (res.ok) toast('Verknüpfung auf dem Desktop erstellt.');
else toast(res.message || 'Verknüpfung fehlgeschlagen.', true);
});
el('p-edit').addEventListener('click', async () => { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-notes').addEventListener('click', async () => { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-groupline').addEventListener('click', async () => { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-mods').addEventListener('click', openMods);
el('p-worlds').addEventListener('click', openContent);
el('p-screens').addEventListener('click', openScreens);
el('p-logs').addEventListener('click', openLogs);
el('p-servers').addEventListener('click', openServers);
el('srv-add').addEventListener('click', addServer);
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-pick').addEventListener('click', waehleSkinDatei);
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
el('skin-apply').addEventListener('click', setzeSkin);
el('skin-reset').addEventListener('click', setzeSkinZurueck);
el('logs-select').addEventListener('change', (e) => ladeLog(e.target.value));
el('logs-folder').addEventListener('click', async () => {
const ok = await window.api.logsOpenFolder(currentDetailId);
if (!ok) toast('Log-Ordner nicht gefunden.', true);
});
el('logs-copy').addEventListener('click', async () => {
if (!aktuellesLog) return;
try { await navigator.clipboard.writeText(aktuellesLog); toast('Log kopiert.'); }
catch { toast('Kopieren fehlgeschlagen.', true); }
});
el('p-mcfolder').addEventListener('click', () => currentDetailId && window.api.openGameDir(currentDetailId));
el('p-configfolder').addEventListener('click', () => currentDetailId && window.api.openSubdir(currentDetailId, 'config'));
el('p-instfolder').addEventListener('click', () => currentDetailId && window.api.openInstanceFolder(currentDetailId));
@@ -2743,6 +3267,27 @@ function wire() {
el('m-tab-catalog').addEventListener('click', () => switchModTab('catalog'));
el('m-tab-installed').addEventListener('click', () => switchModTab('installed'));
el('m-open-folder').addEventListener('click', () => window.api.openGameDir(currentDetailId));
// 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.');
else toast('Automatische Sicherung fehlgeschlagen: ' + (d.message || 'unbekannter Grund'), true);
});
// Absturzbericht
el('crash-copy').addEventListener('click', async () => {
if (!aktuellerCrash) return;
try { await navigator.clipboard.writeText(aktuellerCrash.text || ''); toast('Bericht kopiert.'); }
catch { toast('Kopieren fehlgeschlagen.', true); }
});
el('crash-folder').addEventListener('click', async () => {
const ok = await window.api.crashOpenFolder(currentDetailId);
if (!ok) toast('Ordner mit den Berichten nicht gefunden.', true);
});
el('crash-console').addEventListener('click', () => { hide('modal-crash'); openConsole(); });
el('m-check-updates').addEventListener('click', checkModUpdates);
el('m-apply-updates').addEventListener('click', applyModUpdates);
window.api.onModUpdateProgress((p) => setUpdateStatus(`Suche … ${p.geprueft}/${p.gesamt} (${p.titel})`));
// Einstellungen
el('s-pick-dir').addEventListener('click', async () => {
@@ -2782,6 +3327,7 @@ function wire() {
// Updates
el('upd-install').addEventListener('click', startUpdate);
el('upd-open').addEventListener('click', openReleasePage);
if (el('s-check-update')) {
el('s-check-update').addEventListener('click', async () => {
const st = el('s-update-status');
@@ -2846,6 +3392,13 @@ window.addEventListener('DOMContentLoaded', async () => {
window.api.onAuthCode(onAuthCode);
window.api.onAuthRefreshed(async () => { await refreshAccountButton(); await applySkinBackground(); });
window.api.onAuthHealth(showAuthHealth);
// Spiel gestartet oder beendet -> "spielt gerade" in den Kontenlisten nachziehen
window.api.onAccountsChanged(() => { refreshAllAccountLists(); });
// über eine Desktop-Verknüpfung gestartet -> Instanz auswählen und anwerfen
window.api.onLaunchRequest(async (id) => {
await selectInstance(id);
launch(id);
});
window.api.onUpdateAvailable(showUpdateDialog);
window.api.onUpdateProgress(onUpdateProgress);
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }