Upload via GUI (43 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-05 09:04:48 +02:00
parent ec4992c5c7
commit bafad25059
12 changed files with 1433 additions and 79 deletions
+446 -33
View File
@@ -470,6 +470,17 @@ const LOADER_LABEL = {
neoforge: 'NeoForge', quilt: 'Quilt',
};
// Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren
// bei größeren Modpacks spürbar GC-bedingte Ruckler. Wirkt erst ab genug RAM
// sinnvoll, schadet aber auch bei wenig RAM nicht.
const AIKAR_FLAGS = '-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 '
+ '-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch '
+ '-XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M '
+ '-XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 '
+ '-XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 '
+ '-XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem '
+ '-XX:MaxTenuringThreshold=1';
// ---------- Zustand ----------
let instances = [];
let currentDetailId = null;
@@ -528,6 +539,7 @@ function applyTheme(theme) {
let mmcFound = [];
let cfFound = [];
let mcFound = [];
let technicFound = [];
let importKind = 'zip';
async function ensureDefaultMcScan() {
@@ -538,8 +550,8 @@ async function ensureDefaultMcScan() {
}
function setImportKind(kind) {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc'].forEach((entryKind) => {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].forEach((entryKind) => {
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
});
@@ -548,9 +560,11 @@ function setImportKind(kind) {
el('mc-do').classList.toggle('hidden', importKind !== 'mc');
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('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);
el('technic-progress').classList.toggle('hidden', importKind !== 'technic' || !el('technic-progress').textContent);
}
function openImportDialog(kind = 'zip') {
@@ -923,6 +937,132 @@ async function runMmcImport() {
if (instances.length) await selectInstance(instances[0].id);
}
// ---------- Technic-Plattform: Modpack-Katalog ----------
async function searchTechnicPlatform() {
const query = el('tp-query').value.trim();
const box = el('tp-results');
const st = el('tp-status');
st.textContent = 'Suche …';
box.innerHTML = '<div class="mods-empty">Suche läuft …</div>';
const res = await window.api.technicPlatformSearch(query);
if (!res.ok) {
st.textContent = 'Fehler: ' + (res.message || 'Suche fehlgeschlagen.');
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
return;
}
const hits = res.hits || [];
st.textContent = hits.length ? `${hits.length} Treffer` : 'Keine Treffer.';
box.innerHTML = '';
if (!hits.length) { box.innerHTML = '<div class="mods-empty">Keine Modpacks gefunden.</div>'; return; }
for (const h of hits) {
const item = document.createElement('div');
item.className = 'mod-item';
item.innerHTML =
`<div class="mod-icon">${h.iconUrl ? `<img src="${escapeHtml(h.iconUrl)}" alt="" style="width:32px;height:32px;border-radius:4px;object-fit:cover" />` : '📦'}</div>
<div class="mod-info"><div class="mod-name">${escapeHtml(h.name)}</div></div>
<div class="mod-action"></div>`;
const btn = document.createElement('button');
btn.className = 'btn btn-primary btn-mini';
btn.textContent = 'Installieren';
btn.addEventListener('click', () => installTechnicFromSearch(h, btn));
item.querySelector('.mod-action').appendChild(btn);
box.appendChild(item);
}
}
async function installTechnicFromSearch(hit, btn) {
if (!hit || !hit.slug) return;
const prev = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = '…'; }
const st = el('tp-status');
st.textContent = `Installiere „${hit.name}" …`;
try {
const res = await window.api.technicPlatformInstall(hit.slug);
if (res.ok) {
await reload();
hide('modal-import');
toast(`Modpack installiert: ${res.instance.name}`);
await selectInstance(res.instance.id);
} else {
st.textContent = '✖ ' + (res.message || 'Installation fehlgeschlagen.');
toast(res.message || 'Installation fehlgeschlagen.', true);
}
} finally {
if (btn) { btn.disabled = false; btn.textContent = prev; }
}
}
async function scanTechnic() {
const folder = el('technic-path').value.trim();
el('technic-status').textContent = 'Suche Modpacks …';
const res = await window.api.technicScan(folder);
if (!res.ok) {
el('technic-status').textContent = '✖ Dort wurde kein modpacks-Ordner gefunden.';
el('technic-result').classList.add('hidden');
el('technic-do').disabled = true;
return;
}
technicFound = res.instances;
if (!folder && res.instancesDir) el('technic-path').value = res.instancesDir;
el('technic-status').textContent = `${res.count} Modpack${res.count === 1 ? '' : 's'} gefunden.`;
el('technic-result').classList.remove('hidden');
el('technic-all').checked = true;
renderTechnicList();
}
function renderTechnicList() {
const box = el('technic-list');
box.innerHTML = '';
if (!technicFound.length) { box.innerHTML = '<div class="mods-empty">Keine Modpacks gefunden.</div>'; return; }
technicFound.forEach((inst, idx) => {
const row = document.createElement('label');
row.className = 'mmc-item';
const loaderTxt = inst.loader === 'vanilla'
? 'Vanilla'
: (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : '');
const meta = [inst.version || 'Version unbekannt', loaderTxt]
.filter(Boolean).join(' · ');
row.innerHTML =
`<input type="checkbox" data-idx="${idx}" checked />
<div class="mod-info">
<div class="mmc-name">${escapeHtml(inst.name)}</div>
<div class="mmc-meta">${escapeHtml(meta)}</div>
</div>`;
box.appendChild(row);
});
updateTechnicButton();
}
function selectedTechnic() {
return [...document.querySelectorAll('#technic-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => technicFound[Number(c.dataset.idx)]);
}
function updateTechnicButton() {
const n = selectedTechnic().length;
el('technic-do').disabled = n === 0;
el('technic-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runTechnicImport() {
const entries = selectedTechnic();
if (!entries.length) return;
const copy = el('technic-copy').checked;
el('technic-do').disabled = true;
el('technic-progress').textContent = 'Übernehme …';
el('technic-progress').classList.remove('hidden');
const res = await window.api.technicImport(entries, copy);
await reload();
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} Technic-Modpack${n === 1 ? '' : 's'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
// ---------- Bedrock (Windows-Store-Ausgabe) ----------
let bedrockKind = 'worlds';
@@ -1121,6 +1261,27 @@ function onUpdateProgress(p) {
}
}
// Aktualisiert die Statuszeile im Info-Bereich genutzt sowohl vom manuellen
// "Nach Updates suchen"-Knopf als auch vom automatischen Check beim Start.
function renderUpdateStatus(info) {
const st = el('s-update-status');
if (!st) return;
st.classList.remove('status-ok', 'status-bad');
if (info.checking) {
st.textContent = 'Suche nach Updates …';
} else if (info.error) {
st.classList.add('status-bad');
st.textContent = '✖ ' + info.error;
} else if (info.noReleases) {
st.textContent = 'Noch keine Veröffentlichung vorhanden.';
} else if (info.available) {
st.textContent = `Version ${formatVersionLabel(info.latestVersion)} verfügbar.`;
} else {
st.classList.add('status-ok');
st.textContent = `✓ Du hast bereits die neueste Version (${formatVersionLabel(info.currentVersion)}).`;
}
}
// ---------- Zustand der Anmelde-Dienste in der Statusleiste ----------
function showAuthHealth(h) {
const box = el('status-auth');
@@ -1135,11 +1296,15 @@ function showAuthHealth(h) {
const AUTH_SERVICE_COUNT = 3;
// ---------- Skin-Hintergrund (wie MultiMCs Katze, nur mit eigenem Skin) ----------
async function applySkinBackground() {
// uuidOverride: wird vom Konten-Wechsler (siehe unten) genutzt, um gezielt
// einen bestimmten Skin zu zeigen, ohne die eigentliche Einstellung anzufassen
async function applySkinBackground(uuidOverride) {
const s = await window.api.getSettings();
const root = document.documentElement;
let uuid = '';
if (s.bgSkin === 'active') {
if (uuidOverride) {
uuid = uuidOverride;
} else if (s.bgSkin === 'active') {
const acc = await window.api.authStatus();
uuid = acc && acc.profile ? acc.profile.id : '';
} else if (s.bgSkin) {
@@ -1172,6 +1337,44 @@ async function applySkinBackground() {
root.style.setProperty('--bg-skin', 'none');
}
// ---------- Skin-Hintergrund: automatischer Kontenwechsel ----------
let skinCycleTimer = null;
let skinCycleIndex = 0;
function stopSkinCycle() {
if (skinCycleTimer) { clearInterval(skinCycleTimer); skinCycleTimer = null; }
}
async function skinCycleTick() {
const accs = await window.api.authAccounts();
if (accs.length < 2) { stopSkinCycle(); await applySkinBackground(); return; }
skinCycleIndex = (skinCycleIndex + 1) % accs.length; // am Ende wieder von vorn
await applySkinBackground(accs[skinCycleIndex].id);
}
/*
* Einstiegspunkt für alles, was den Skin-Hintergrund neu anzeigen soll.
* Ersetzt die frühere direkte applySkinBackground()-Aufrufe: kümmert sich
* zusätzlich darum, den Kontenwechsel-Timer je nach Einstellung zu
* starten/stoppen, statt dass jede Aufrufstelle das selbst tun müsste.
*/
async function refreshSkinBackground() {
stopSkinCycle();
const s = await window.api.getSettings();
if (s.bgSkinCycle) {
const accs = await window.api.authAccounts();
if (accs.length >= 2) {
skinCycleIndex = 0;
await applySkinBackground(accs[0].id);
const seconds = Math.max(5, Number(s.bgSkinCycleSeconds) || 30);
skinCycleTimer = setInterval(skinCycleTick, seconds * 1000);
return;
}
// weniger als 2 Konten angemeldet -> normal weiter wie ohne Wechsel
}
await applySkinBackground();
}
// Pose-Vorlagen für den lokalen 3D-Renderer (eigene Nachbildungen bekannter Motive)
const SKIN_POSES = {
walking: { label: 'Laufend', fov: 36, zoom: 0.62, apply: (p) => {
@@ -1936,9 +2139,11 @@ async function renderServers() {
<div class="mod-info">
<div class="mod-name">${escapeHtml(s.name || s.ip)}</div>
<div class="mod-meta">${escapeHtml(s.ip)}</div>
<div class="mod-meta srv-status hint">Wird geprüft …</div>
</div>
<div class="mod-action"></div>`;
const aktionen = item.querySelector('.mod-action');
pingServerRow(s.ip, item.querySelector('.srv-status'));
const starten = document.createElement('button');
starten.className = 'btn btn-primary btn-mini';
@@ -1992,6 +2197,24 @@ async function renderServers() {
}
}
// Fragt Live-Status (MOTD/Spieler/Version) ab und trägt ihn in die Zeile ein.
// Läuft unabhängig für jede Zeile, ohne die Liste selbst zu blockieren.
async function pingServerRow(ip, statusEl) {
if (!statusEl) return;
const res = await window.api.serversPing(ip);
// Zeile kann inzwischen aus dem DOM entfernt worden sein (Liste neu aufgebaut)
if (!statusEl.isConnected) return;
if (!res.ok) {
statusEl.textContent = '⚠ ' + (res.message || 'nicht erreichbar');
statusEl.classList.add('status-bad');
return;
}
const spieler = res.max != null ? `${res.online ?? '?'}/${res.max} Spieler` : '';
statusEl.textContent = [res.motd, spieler, res.version, res.latencyMs + ' ms']
.filter(Boolean).join(' · ');
statusEl.classList.add('status-ok');
}
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);
@@ -2130,7 +2353,7 @@ async function setzeSkin() {
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
await refreshSkinBackground(); // auch der Hintergrund zeigt jetzt den neuen Skin
} else {
el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
btn.disabled = false;
@@ -2145,7 +2368,7 @@ async function setzeSkinZurueck() {
if (res.ok) {
toast('Skin auf Standard zurückgesetzt.');
await openSkin();
await applySkinBackground();
await refreshSkinBackground();
} else {
el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
}
@@ -2431,6 +2654,24 @@ function renderContentList(boxId, items, sub, emoji) {
`<div class="mod-icon">${emoji}</div>
<div class="mod-info"><div class="mod-name">${escapeHtml(it.name)}</div></div>
<div class="mod-action"></div>`;
const aktionen = item.querySelector('.mod-action');
if (sub === 'saves') {
const sichern = document.createElement('button');
sichern.className = 'btn btn-mini';
sichern.textContent = 'Sichern';
sichern.title = 'Nur diese Welt jetzt als ZIP sichern (Dokumente\\AeroMC Launcher\\Welten-Sicherungen)';
sichern.addEventListener('click', async () => {
sichern.disabled = true;
const orig = sichern.textContent;
sichern.textContent = 'Sichere …';
const res = await window.api.backupWorld(currentDetailId, it.name);
sichern.disabled = false;
sichern.textContent = orig;
if (res.ok) toast(`${it.name}" gesichert.`);
else toast(res.message || 'Sicherung fehlgeschlagen.', true);
});
aktionen.appendChild(sichern);
}
const rm = document.createElement('button');
rm.className = 'btn btn-danger btn-mini';
rm.textContent = 'Löschen';
@@ -2438,7 +2679,7 @@ function renderContentList(boxId, items, sub, emoji) {
'Löschen', `${it.name}" wird unwiderruflich gelöscht. Fortfahren?`, '🗑 Löschen',
async () => { await window.api.deleteContent(currentDetailId, sub, it.name); await renderContentLists(); toast('Gelöscht: ' + it.name); },
));
item.querySelector('.mod-action').appendChild(rm);
aktionen.appendChild(rm);
box.appendChild(item);
}
}
@@ -2586,9 +2827,24 @@ async function refreshInstalled() {
btn.className = 'btn btn-danger btn-mini';
btn.textContent = 'Entfernen';
btn.addEventListener('click', async () => {
await window.api.modRemove(currentDetailId, m.filename, modArt);
toast('Entfernt: ' + m.title);
await refreshInstalled();
const tatsaechlichEntfernen = async () => {
await window.api.modRemove(currentDetailId, m.filename, modArt);
toast('Entfernt: ' + m.title);
await refreshInstalled();
};
const abhaenger = await window.api.modDependents(currentDetailId, m.filename, modArt);
if (abhaenger.length) {
const namen = abhaenger.map((n) => `${n}"`).join(', ');
askConfirm(
'Wird noch gebraucht',
`${namen} ${abhaenger.length === 1 ? 'benötigt' : 'benötigen'}${m.title}" als Pflicht-Abhängigkeit. `
+ 'Wird sie trotzdem entfernt, funktionieren diese Mods vermutlich nicht mehr richtig. Trotzdem entfernen?',
'Trotzdem entfernen',
tatsaechlichEntfernen,
);
} else {
await tatsaechlichEntfernen();
}
});
aktionen.appendChild(btn);
box.appendChild(item);
@@ -2791,9 +3047,13 @@ async function openSettings(pane) {
}
sel.value = s.bgSkin || '';
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
el('s-bgskin-cycle').checked = !!s.bgSkinCycle;
el('s-bgskin-cycle-seconds').value = s.bgSkinCycleSeconds || 30;
sel.disabled = !!s.bgSkinCycle;
el('s-sortby').value = s.sortBy || 'lastPlayed';
el('s-iso3d').checked = s.iso3dIcons !== false;
el('s-bedrock').checked = s.bedrockEnabled !== false;
el('s-notify').checked = s.desktopNotifications !== false;
el('s-auto-java').checked = s.autoJava !== false;
el('s-autobackup').checked = !!s.autoBackupEnabled;
el('s-autobackup-every').value = s.autoBackupEvery || 'daily';
@@ -2908,9 +3168,12 @@ async function saveSettings() {
globalPostExit: el('s-global-post').value.trim(),
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
bgSkinCycle: el('s-bgskin-cycle').checked,
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
sortBy: el('s-sortby').value,
iso3dIcons: el('s-iso3d').checked,
bedrockEnabled: el('s-bedrock').checked,
desktopNotifications: el('s-notify').checked,
autoJava: el('s-auto-java').checked,
autoBackupEnabled: el('s-autobackup').checked,
autoBackupEvery: el('s-autobackup-every').value,
@@ -2942,7 +3205,7 @@ async function saveSettings() {
applyTheme(patch.theme);
iso3dEnabled = patch.iso3dIcons;
await applyBedrockVisibility();
await applySkinBackground();
await refreshSkinBackground();
hide('modal-settings');
toast('Einstellungen gespeichert.');
await reload();
@@ -3180,7 +3443,7 @@ async function runGlobalRestore() {
applyTheme(s.theme || 'dark');
iso3dEnabled = s.iso3dIcons !== false;
await applyBedrockVisibility();
await applySkinBackground();
await refreshSkinBackground();
hide('modal-settings');
await reload();
toast('Backup wiederhergestellt: ' + res.path);
@@ -3267,7 +3530,7 @@ async function renderAccounts(boxId = 'login-accounts') {
await window.api.authSetActive(a.id);
await refreshAllAccountLists();
await refreshAccountButton();
await applySkinBackground();
await refreshSkinBackground();
toast('Aktiv: ' + a.name);
});
actions.appendChild(use);
@@ -3392,8 +3655,9 @@ function wire() {
// Skin-Hintergrund an/aus (wie MultiMCs Katzen-Knopf)
el('btn-skin').addEventListener('click', async () => {
const s = await window.api.getSettings();
if (s.bgSkin) {
await window.api.saveSettings({ bgSkin: '' });
if (s.bgSkin || s.bgSkinCycle) {
// auch den Kontenwechsel mit ausschalten, sonst würde er sofort wieder anspringen
await window.api.saveSettings({ bgSkin: '', bgSkinCycle: false });
toast('Skin-Hintergrund aus.');
} else {
const acc = await window.api.authStatus();
@@ -3401,7 +3665,7 @@ function wire() {
await window.api.saveSettings({ bgSkin: 'active' });
toast('Skin-Hintergrund an: ' + acc.profile.name);
}
await applySkinBackground();
await refreshSkinBackground();
});
// Kontextmenü: Einträge lösen die Aktionen der rechten Leiste aus
@@ -3442,6 +3706,13 @@ function wire() {
searchModpacks();
}
}
if (tab.dataset.kind === 'technic') {
const box = el('tp-results');
if (box && !box.dataset.loaded) {
box.dataset.loaded = '1';
searchTechnicPlatform();
}
}
});
});
el('import-zip-do').addEventListener('click', runZipImport);
@@ -3485,6 +3756,23 @@ function wire() {
});
el('mmc-list').addEventListener('change', updateMmcButton);
el('mmc-do').addEventListener('click', runMmcImport);
el('tp-search-btn').addEventListener('click', searchTechnicPlatform);
el('tp-query').addEventListener('keydown', (e) => { if (e.key === 'Enter') searchTechnicPlatform(); });
window.api.onTechnicPlatformProgress((p) => {
el('tp-status').textContent = `Lade Mods … ${p.current}/${p.total}${p.name ? ' (' + p.name + ')' : ''}`;
});
el('technic-pick').addEventListener('click', async () => {
const p = await window.api.technicPickFolder();
if (p) { el('technic-path').value = p; await scanTechnic(); }
});
el('technic-scan').addEventListener('click', scanTechnic);
el('technic-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanTechnic(); });
el('technic-all').addEventListener('change', (e) => {
document.querySelectorAll('#technic-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateTechnicButton();
});
el('technic-list').addEventListener('change', updateTechnicButton);
el('technic-do').addEventListener('click', runTechnicImport);
window.api.onMcProgress((p) => {
el('mc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mc-progress').classList.remove('hidden');
@@ -3497,13 +3785,33 @@ function wire() {
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mmc-progress').classList.remove('hidden');
});
window.api.onTechnicProgress((p) => {
el('technic-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('technic-progress').classList.remove('hidden');
});
// Bedrock
el('btn-bedrock').addEventListener('click', openBedrock);
el('bd-play').addEventListener('click', async () => {
const res = await window.api.bedrockLaunch();
if (res && res.ok) toast('Bedrock wird gestartet …');
else toast('Start fehlgeschlagen: ' + ((res && res.message) || 'unbekannter Fehler'), true);
const btn = el('bd-play');
// Ein Klick löst eine nicht abbrechbare App-Aktivierung aus, die etwas
// dauern kann Button sperren, damit ein ungeduldiger zweiter Klick
// nicht zu einem doppelt geöffneten Spiel führt.
if (btn.disabled) return;
btn.disabled = true;
toast('Bedrock wird gestartet …');
try {
const res = await window.api.bedrockLaunch();
if (res && res.ok) {
if (!res.confirmed) {
toast('Start ausgelöst, konnte aber nicht bestätigt werden. Falls sich nichts öffnet, bitte erneut versuchen.');
}
} else {
toast('Start fehlgeschlagen: ' + ((res && res.message) || 'unbekannter Fehler'), true);
}
} finally {
btn.disabled = false;
}
});
el('bd-import').addEventListener('click', async () => {
const res = await window.api.bedrockInstallAddon();
@@ -3673,6 +3981,16 @@ function wire() {
});
el('crash-console').addEventListener('click', () => { hide('modal-crash'); openConsole(); });
el('m-add-file').addEventListener('click', async () => {
const res = await window.api.modAddFile(currentDetailId, modArt);
if (res.canceled) return;
if (!res.ok) { toast(res.message || 'Datei konnte nicht hinzugefügt werden.', true); return; }
const n = res.added.length;
const f = res.failed.length;
if (n) toast(`${n} Datei${n === 1 ? '' : 'en'} hinzugefügt${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0 && !n);
else if (f) toast(res.failed[0].message || 'Hinzufügen fehlgeschlagen.', true);
await refreshInstalled();
});
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})`));
@@ -3697,10 +4015,20 @@ function wire() {
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
});
await applySkinBackground();
await refreshSkinBackground();
};
el('s-bgskin').addEventListener('change', previewSkin);
el('s-bgskin-pose').addEventListener('change', previewSkin);
const previewSkinCycle = async () => {
el('s-bgskin').disabled = el('s-bgskin-cycle').checked;
await window.api.saveSettings({
bgSkinCycle: el('s-bgskin-cycle').checked,
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
});
await refreshSkinBackground();
};
el('s-bgskin-cycle').addEventListener('change', previewSkinCycle);
el('s-bgskin-cycle-seconds').addEventListener('change', previewSkinCycle);
el('s-scan-java').addEventListener('click', scanJava);
el('f-java-detected').addEventListener('change', (e) => {
if (e.target.value) el('f-java').value = e.target.value;
@@ -3709,6 +4037,13 @@ function wire() {
const p = await window.api.pickJava();
if (p) { el('f-java').value = p; el('f-java-detected').value = ''; }
});
el('f-extra-args-aikar').addEventListener('click', () => {
const feld = el('f-extra-args');
const vorhanden = feld.value.trim();
if (vorhanden.includes('G1NewSizePercent')) { toast('Die GC-Flags stehen schon drin.'); return; }
feld.value = vorhanden ? vorhanden + ' ' + AIKAR_FLAGS : AIKAR_FLAGS;
toast('GC-Flags eingefügt.');
});
el('s-java-detected').addEventListener('change', async (e) => {
if (e.target.value) { el('s-java').value = e.target.value; await refreshJavaStatus(e.target.value); }
});
@@ -3744,18 +4079,10 @@ function wire() {
el('upd-open').addEventListener('click', openReleasePage);
if (el('s-check-update')) {
el('s-check-update').addEventListener('click', async () => {
const st = el('s-update-status');
st.textContent = 'Suche nach Updates …';
renderUpdateStatus({ checking: true });
const info = await window.api.updateCheck();
if (info.error) { st.textContent = '✖ ' + info.error; return; }
if (info.noReleases) { st.textContent = 'Noch keine Veröffentlichung vorhanden.'; return; }
if (info.available) {
st.textContent = `Version ${formatVersionLabel(info.latestVersion)} verfügbar.`;
hide('modal-settings');
showUpdateDialog(info);
} else {
st.textContent = `Du hast bereits die neueste Version (${formatVersionLabel(info.currentVersion)}).`;
}
renderUpdateStatus(info);
if (info.available) { hide('modal-settings'); showUpdateDialog(info); }
});
}
@@ -3818,8 +4145,93 @@ function wire() {
if (!el('modal-prompt').classList.contains('hidden')) { closePrompt(null); return; }
const open = [...document.querySelectorAll('.modal:not(.hidden)')].pop();
if (open) open.classList.add('hidden');
return;
}
const strg = e.ctrlKey || e.metaKey;
const modalOffen = document.querySelector('.modal:not(.hidden)');
const inEingabe = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)
|| document.activeElement.isContentEditable;
// Strg+F: Suche fokussieren nicht wenn gerade ein Dialog offen ist,
// der eine eigene Suche hat (Mods, Modpack-Katalog, Import …)
if (strg && e.key.toLowerCase() === 'f' && !modalOffen) {
e.preventDefault();
el('search').focus();
el('search').select();
return;
}
// Strg+N: neue Instanz anlegen
if (strg && e.key.toLowerCase() === 'n' && !modalOffen) {
e.preventDefault();
el('btn-new').click();
return;
}
// Entf: markierte Instanz(en) löschen nicht während man irgendwo tippt
if (e.key === 'Delete' && !modalOffen && !inEingabe) {
e.preventDefault();
el('p-delete').click();
}
});
wireFileDrop();
}
// ---------- Datei-Drag&Drop aufs Fenster (.jar/.mrpack/.zip) ----------
function wireFileDrop() {
let dragDepth = 0; // dragenter/-leave feuern auch für Kindelemente
const istDatei = (e) => e.dataTransfer && [...e.dataTransfer.types].includes('Files');
window.addEventListener('dragenter', (e) => {
if (!istDatei(e)) return;
e.preventDefault();
dragDepth++;
document.body.classList.add('file-drop-active');
});
window.addEventListener('dragover', (e) => {
if (!istDatei(e)) return;
e.preventDefault();
});
window.addEventListener('dragleave', (e) => {
if (!istDatei(e)) return;
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) document.body.classList.remove('file-drop-active');
});
window.addEventListener('drop', async (e) => {
if (!istDatei(e)) return;
e.preventDefault();
dragDepth = 0;
document.body.classList.remove('file-drop-active');
const files = [...(e.dataTransfer.files || [])].filter((f) => f.path);
for (const f of files) await handleDroppedFile(f.path);
});
}
async function handleDroppedFile(filePath) {
const ext = ('.' + filePath.split('.').pop()).toLowerCase();
if (!['.jar', '.mrpack', '.zip'].includes(ext)) {
toast('Nicht unterstützter Dateityp: ' + filePath.split(/[\\/]/).pop(), true);
return;
}
// Für .jar: läuft der Mods-Dialog gerade, zählt die dort gewählte
// Inhaltsart (Mod/Ressourcenpaket/Shader) sonst landet es als Mod
// direkt in der aktuell ausgewählten Instanz.
const modsOpen = !el('modal-mods').classList.contains('hidden');
const targetArt = modsOpen ? modArt : 'mod';
const res = await window.api.filesDrop(filePath, currentDetailId, targetArt);
if (!res.ok) { toast(res.message || 'Import fehlgeschlagen.', true); return; }
if (res.kind === 'mod') {
toast(`Datei hinzugefügt: ${res.added[0]}`);
if (modsOpen) await refreshInstalled();
return;
}
await reload();
if (res.instance) {
toast(`Instanz übernommen: ${res.instance.name}`);
await selectInstance(res.instance.id);
} else {
toast('Import abgeschlossen.');
}
}
window.addEventListener('DOMContentLoaded', async () => {
@@ -3831,11 +4243,11 @@ window.addEventListener('DOMContentLoaded', async () => {
iso3dEnabled = s.iso3dIcons !== false;
consoleMaxChars = Math.max(50000, (s.consoleMaxLines || 100000) * 3);
} catch { applyTheme('dark'); }
await applySkinBackground();
await refreshSkinBackground();
window.api.onLaunchProgress(onLaunchEvt);
window.api.onBackupProgress((p) => applyBackupProgressEvent(p.mode, p.percent || 0, p.detail || ''));
window.api.onAuthCode(onAuthCode);
window.api.onAuthRefreshed(async () => { await refreshAccountButton(); await applySkinBackground(); });
window.api.onAuthRefreshed(async () => { await refreshAccountButton(); await refreshSkinBackground(); });
window.api.onAuthHealth(showAuthHealth);
// Spiel gestartet oder beendet -> "spielt gerade" in den Kontenlisten nachziehen
window.api.onAccountsChanged(() => { refreshAllAccountLists(); });
@@ -3846,6 +4258,7 @@ window.addEventListener('DOMContentLoaded', async () => {
});
window.api.onUpdateAvailable(showUpdateDialog);
window.api.onUpdateProgress(onUpdateProgress);
window.api.onUpdateChecked(renderUpdateStatus);
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
await applyBedrockVisibility();