Upload via GUI (25 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-02 10:14:02 +02:00
parent dc55e64ca3
commit a803f297ea
15 changed files with 2587 additions and 324 deletions
+680 -65
View File
@@ -473,6 +473,7 @@ const LOADER_LABEL = {
// ---------- Zustand ----------
let instances = [];
let currentDetailId = null;
let selectedInstanceIds = new Set();
let editingId = null;
let confirmAction = null;
let mcVersionData = null; // gecachte Mojang-Versionsliste
@@ -525,14 +526,235 @@ function applyTheme(theme) {
// ---------- Instanzen aus MultiMC übernehmen ----------
let mmcFound = [];
let cfFound = [];
let mcFound = [];
let importKind = 'zip';
function openMmc() {
async function ensureDefaultMcScan() {
if (importKind !== 'mc') return;
if (el('mc-path').value.trim()) return;
if (mcFound.length) return;
await scanMc();
}
function setImportKind(kind) {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc'].forEach((entryKind) => {
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
});
el('import-zip-do').classList.toggle('hidden', importKind !== 'zip');
el('import-modpack-do').classList.toggle('hidden', importKind !== 'modpack');
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('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);
}
function openImportDialog(kind = 'zip') {
mcFound = [];
el('mc-result').classList.add('hidden');
el('mc-do').disabled = true;
el('mc-progress').textContent = '';
el('mc-progress').classList.add('hidden');
el('mc-status').textContent = 'Leer lassen nutzt automatisch den Standardpfad unter Windows.';
el('cf-result').classList.add('hidden');
el('cf-do').disabled = true;
el('cf-progress').textContent = '';
el('cf-progress').classList.add('hidden');
el('cf-status').textContent = 'Standardpfad unter Windows: C:\\Users\\DeinName\\curseforge\\minecraft\\Instances';
el('mmc-result').classList.add('hidden');
el('mmc-do').disabled = true;
el('mmc-progress').textContent = '';
el('mmc-status').textContent = 'Gib den Ordner an, in dem MultiMC.exe liegt.';
show('modal-mmc');
el('mmc-path').focus();
el('mmc-progress').classList.add('hidden');
el('mmc-status').textContent = 'Gib den Ordner an, in dem MultiMC.exe oder PrismLauncher.exe liegt.';
setImportKind(kind);
show('modal-import');
if (importKind === 'mc') {
el('mc-path').focus();
ensureDefaultMcScan();
}
if (importKind === 'cf') el('cf-path').focus();
if (importKind === 'mmc') el('mmc-path').focus();
}
async function runZipImport() {
el('import-zip-do').disabled = true;
try {
const res = await window.api.importInstance();
if (res.canceled) return;
if (res.ok) {
await reload();
hide('modal-import');
toast('Importiert: ' + res.instance.name);
return;
}
toast(res.message || 'Import fehlgeschlagen.', true);
} finally {
el('import-zip-do').disabled = false;
}
}
async function runModpackImport() {
el('import-modpack-do').disabled = true;
status('Importiere Modpack …');
try {
const res = await window.api.importModpack();
status('Bereit'); el('status-right').textContent = '';
if (res.canceled) return;
if (res.ok) {
await reload();
hide('modal-import');
toast(`Modpack importiert: ${res.instance.name} (${res.fileCount} Dateien)`);
return;
}
toast(res.message || 'Modpack-Import fehlgeschlagen.', true);
} finally {
status('Bereit'); el('status-right').textContent = '';
el('import-modpack-do').disabled = false;
}
}
async function scanMc() {
const folder = el('mc-path').value.trim();
el('mc-status').textContent = 'Suche Launcher-Profile …';
const res = await window.api.mcScan(folder);
if (!res.ok) {
el('mc-status').textContent = '✖ Dort wurde kein launcher_profiles.json gefunden.';
el('mc-result').classList.add('hidden');
el('mc-do').disabled = true;
return;
}
mcFound = res.instances;
if (!folder && res.rootDir) el('mc-path').value = res.rootDir;
el('mc-status').textContent = `${res.count} Profil${res.count === 1 ? '' : 'e'} gefunden.`;
el('mc-result').classList.remove('hidden');
el('mc-all').checked = true;
renderMcList();
}
function renderMcList() {
const box = el('mc-list');
box.innerHTML = '';
if (!mcFound.length) { box.innerHTML = '<div class="mods-empty">Keine Profile gefunden.</div>'; return; }
mcFound.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 || 'ohne Version', loaderTxt, inst.usesDefaultGameDir ? 'Standard-Spielordner' : 'eigener Spielordner']
.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);
});
updateMcButton();
}
function selectedMc() {
return [...document.querySelectorAll('#mc-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => mcFound[Number(c.dataset.idx)]);
}
function updateMcButton() {
const n = selectedMc().length;
el('mc-do').disabled = n === 0;
el('mc-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runMcImport() {
const entries = selectedMc();
if (!entries.length) return;
const copy = el('mc-copy').checked;
el('mc-do').disabled = true;
el('mc-progress').textContent = 'Übernehme …';
el('mc-progress').classList.remove('hidden');
const res = await window.api.mcImport(entries, copy);
await reload();
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} Profil${n === 1 ? '' : 'e'} aus dem Minecraft Launcher übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
async function scanCf() {
const folder = el('cf-path').value.trim();
if (!folder) { el('cf-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; }
el('cf-status').textContent = 'Suche Instanzen …';
const res = await window.api.cfScan(folder);
if (!res.ok) {
el('cf-status').textContent = '✖ Dort wurde kein CurseForge-Instances-Ordner gefunden.';
el('cf-result').classList.add('hidden');
el('cf-do').disabled = true;
return;
}
cfFound = res.instances;
el('cf-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden.`;
el('cf-result').classList.remove('hidden');
el('cf-all').checked = true;
renderCfList();
}
function renderCfList() {
const box = el('cf-list');
box.innerHTML = '';
if (!cfFound.length) { box.innerHTML = '<div class="mods-empty">Keine Instanzen gefunden.</div>'; return; }
cfFound.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 || 'ohne Version', 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);
});
updateCfButton();
}
function selectedCf() {
return [...document.querySelectorAll('#cf-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => cfFound[Number(c.dataset.idx)]);
}
function updateCfButton() {
const n = selectedCf().length;
el('cf-do').disabled = n === 0;
el('cf-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runCfImport() {
const entries = selectedCf();
if (!entries.length) return;
const copy = el('cf-copy').checked;
el('cf-do').disabled = true;
el('cf-progress').textContent = 'Übernehme …';
el('cf-progress').classList.remove('hidden');
const res = await window.api.cfImport(entries, copy);
await reload();
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} CurseForge-Instanz${n === 1 ? '' : 'en'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
async function scanMmc() {
@@ -594,10 +816,11 @@ async function runMmcImport() {
const copy = el('mmc-copy').checked;
el('mmc-do').disabled = true;
el('mmc-progress').textContent = 'Übernehme …';
el('mmc-progress').classList.remove('hidden');
const res = await window.api.mmcImport(entries, copy);
await reload();
hide('modal-mmc');
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} Instanz${n === 1 ? '' : 'en'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
@@ -741,11 +964,12 @@ function showUpdateDialog(info) {
pendingUpdate = info;
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-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 Installer im Release';
el('upd-install').textContent = info.asset ? 'Installieren' : `Kein passendes ${info.installKind === 'portable' ? 'Portable' : 'Setup'}-Update im Release`;
show('modal-update');
}
@@ -954,9 +1178,114 @@ 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-mcfolder', 'p-configfolder', 'p-instfolder', 'p-shortcut', 'p-export', 'p-copy',
];
function orderedSelectedInstanceIds() {
return instances.map((inst) => inst.id).filter((id) => selectedInstanceIds.has(id));
}
function syncCardSelectionUi() {
document.querySelectorAll('.card').forEach((card) => {
card.classList.toggle('sel', selectedInstanceIds.has(card.dataset.id));
});
}
function setSingleSelectionActionsDisabled(disabled) {
SINGLE_SELECTION_BUTTON_IDS.forEach((id) => { el(id).disabled = disabled; });
const groupLine = el('p-groupline');
groupLine.style.pointerEvents = disabled ? 'none' : '';
groupLine.style.color = disabled ? 'var(--dim)' : '';
}
async function refreshSelectionPanel() {
const selectedIds = orderedSelectedInstanceIds();
const count = selectedIds.length;
const panelC = el('p-content');
const panelE = el('p-empty');
syncCardSelectionUi();
if (!count) {
currentDetailId = null;
setSingleSelectionActionsDisabled(false);
el('p-delete').disabled = false;
el('p-delete').textContent = 'Löschen';
panelC.classList.add('hidden');
panelE.classList.remove('hidden');
el('status-left').textContent = 'Bereit';
el('status-play').textContent = 'Keine Instanz ausgewählt';
el('status-total').textContent = '';
return;
}
if (count > 1) {
currentDetailId = null;
setSingleSelectionActionsDisabled(true);
el('p-delete').disabled = false;
el('p-delete').textContent = `${count} Instanzen löschen`;
panelE.classList.add('hidden');
panelC.classList.remove('hidden');
el('p-icon').innerHTML = iconOf('chest', 60);
el('p-name').textContent = `${count} Instanzen ausgewählt`;
el('p-groupline').textContent = 'Mehrfachauswahl aktiv';
el('status-left').textContent = `${count} Instanzen ausgewählt`;
el('status-play').textContent = 'Strg+Klick wählt weitere Instanzen aus oder ab';
el('status-total').textContent = 'Sammellöschen verfügbar';
return;
}
const id = selectedIds[0];
currentDetailId = id;
setSingleSelectionActionsDisabled(false);
el('p-delete').disabled = false;
el('p-delete').textContent = 'Löschen';
window.api.saveSettings({ lastSelectedId: id || '' });
const inst = id ? await window.api.getInstance(id) : null;
if (!inst) {
selectedInstanceIds.delete(id);
panelC.classList.add('hidden');
panelE.classList.remove('hidden');
el('status-left').textContent = 'Bereit';
el('status-play').textContent = 'Keine Instanz ausgewählt';
el('status-total').textContent = '';
return;
}
panelE.classList.add('hidden');
panelC.classList.remove('hidden');
const iconData = inst.customImage ? await window.api.instanceIcon(id) : null;
if (iconData) el('p-icon').innerHTML = `<img src="${iconData}" alt="" />`;
else { el('p-icon').innerHTML = iconOf(inst.icon, 60); }
el('p-name').textContent = inst.name;
el('p-groupline').textContent = inst.group ? `Gruppe: ${inst.group}` : 'Ohne Gruppe';
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
el('status-left').textContent = inst.name + ' (' + (inst.minecraft.version || 'ohne Version') + loaderTxt + ')';
el('status-play').textContent = 'Zuletzt gespielt: ' + (inst.lastPlayed ? fmtDate(inst.lastPlayed) : 'nie');
el('status-total').textContent = 'Spielzeit: ' + fmtDuration(inst.totalPlaySeconds);
}
async function applySelection(ids) {
const valid = new Set(instances.map((inst) => inst.id));
selectedInstanceIds = new Set((ids || []).filter((id) => valid.has(id)));
await refreshSelectionPanel();
}
async function toggleInstanceSelection(id) {
const next = new Set(selectedInstanceIds);
if (next.has(id)) next.delete(id);
else next.add(id);
await applySelection([...next]);
}
function buildCard(inst) {
const card = document.createElement('div');
card.className = 'card' + (inst.id === currentDetailId ? ' sel' : '');
card.className = 'card' + (selectedInstanceIds.has(inst.id) ? ' sel' : '');
card.dataset.id = inst.id;
const iconHtml = inst.iconData
@@ -974,14 +1303,17 @@ function buildCard(inst) {
<div class="card-name">${escapeHtml(inst.name)}</div>`;
// einfacher Klick = auswählen (MultiMC), Doppelklick = starten
card.addEventListener('click', () => selectInstance(inst.id));
card.addEventListener('click', (e) => {
if (e.ctrlKey || e.metaKey) toggleInstanceSelection(inst.id);
else selectInstance(inst.id);
});
card.addEventListener('dblclick', () => launch(inst.id));
// Rechtsklick: Instanz auswählen und Kontextmenü öffnen
card.addEventListener('contextmenu', (e) => {
card.addEventListener('contextmenu', async (e) => {
e.preventDefault();
if (!selectedInstanceIds.has(inst.id)) await selectInstance(inst.id);
openContextMenu(e.clientX, e.clientY); // sofort anzeigen
selectInstance(inst.id); // Auswahl folgt
});
// Drag & Drop in Gruppen
@@ -1060,7 +1392,10 @@ function fillGroupDatalist() {
async function reload() {
instances = await window.api.listInstances();
const valid = new Set(instances.map((inst) => inst.id));
selectedInstanceIds = new Set([...selectedInstanceIds].filter((id) => valid.has(id)));
render();
await refreshSelectionPanel();
}
// ---------- Minecraft-Versionen ----------
@@ -1299,35 +1634,7 @@ async function saveInstance() {
// ---------- Auswahl + rechte Aktionsleiste (MultiMC-Stil) ----------
async function selectInstance(id) {
currentDetailId = id;
document.querySelectorAll('.card').forEach((c) => c.classList.toggle('sel', c.dataset.id === id));
// Auswahl merken, damit sie beim nächsten Start wiederhergestellt wird
window.api.saveSettings({ lastSelectedId: id || '' });
const inst = id ? await window.api.getInstance(id) : null;
const panelC = el('p-content');
const panelE = el('p-empty');
if (!inst) {
panelC.classList.add('hidden');
panelE.classList.remove('hidden');
el('status-left').textContent = 'Bereit';
el('status-play').textContent = 'Keine Instanz ausgewählt';
el('status-total').textContent = '';
return;
}
panelE.classList.add('hidden');
panelC.classList.remove('hidden');
const iconData = inst.customImage ? await window.api.instanceIcon(id) : null;
if (iconData) el('p-icon').innerHTML = `<img src="${iconData}" alt="" />`;
else { el('p-icon').innerHTML = iconOf(inst.icon, 60); }
el('p-name').textContent = inst.name;
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
el('status-left').textContent = inst.name + ' (' + (inst.minecraft.version || 'ohne Version') + loaderTxt + ')';
el('status-play').textContent = 'Zuletzt gespielt: ' + (inst.lastPlayed ? fmtDate(inst.lastPlayed) : 'nie');
el('status-total').textContent = 'Spielzeit: ' + fmtDuration(inst.totalPlaySeconds);
await applySelection(id ? [id] : []);
}
let launching = false;
@@ -1703,6 +2010,14 @@ 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-backup-status').className = 'hint';
el('s-backup-status').textContent = 'Noch kein Backup erstellt.';
el('s-backup-global').disabled = false;
el('s-restore-status').className = 'hint';
el('s-restore-status').textContent = 'Noch kein Backup wiederhergestellt.';
await refreshGlobalBackupOptions();
resetBackupProgressUi();
setBackupButtonsBusy(false);
el('s-instances-dir').value = await window.api.getInstancesDir();
el('s-java').value = s.javaPath || '';
el('s-min-mem').value = s.defaultMinMemMb;
@@ -1809,6 +2124,257 @@ async function saveSettings() {
await reload();
}
function resetBackupProgressUi() {
['backup', 'restore'].forEach((kind) => {
clearBackupProgressTimer(kind);
backupProgressUiState[kind].shownPercent = 0;
backupProgressUiState[kind].targetPercent = 0;
backupProgressUiState[kind].detail = '';
backupProgressUiState[kind].packing = false;
el(`s-${kind}-progress`).dataset.percent = '0';
el(`s-${kind}-progress`).dataset.complete = '0';
el(`s-${kind}-progress`).classList.add('hidden');
el(`s-${kind}-progress-fill`).style.width = '0%';
el(`s-${kind}-progress-fill`).classList.remove('backup-progress-fill-busy');
el(`s-${kind}-progress-text`).textContent = '0 %';
});
}
const backupProgressActive = { backup: false, restore: false };
const backupProgressUiState = {
backup: { timer: null, shownPercent: 0, targetPercent: 0, detail: '', packing: false },
restore: { timer: null, shownPercent: 0, targetPercent: 0, detail: '', packing: false },
};
function clearBackupProgressTimer(prefix) {
const state = backupProgressUiState[prefix];
if (!state.timer) return;
clearInterval(state.timer);
state.timer = null;
}
function renderBackupProgress(prefix) {
const state = backupProgressUiState[prefix];
const box = el(`s-${prefix}-progress`);
const fill = el(`s-${prefix}-progress-fill`);
const text = el(`s-${prefix}-progress-text`);
const displayPercent = state.targetPercent >= 100 ? 100 : Math.floor(state.shownPercent);
box.dataset.percent = String(displayPercent);
box.dataset.complete = state.targetPercent >= 100 && state.shownPercent >= 100 ? '1' : '0';
box.classList.remove('hidden');
fill.classList.toggle('backup-progress-fill-busy', state.packing && state.targetPercent < 99);
fill.style.width = `${Math.max(0, Math.min(100, state.shownPercent))}%`;
text.textContent = `${displayPercent} %${state.detail ? ' · ' + state.detail : ''}`;
}
function ensureBackupProgressTimer(prefix) {
const state = backupProgressUiState[prefix];
if (state.timer) return;
state.timer = setInterval(() => {
const gap = state.targetPercent - state.shownPercent;
if (gap > 0.05) {
const step = state.packing ? Math.max(0.18, gap * 0.18) : Math.max(0.8, gap * 0.4);
state.shownPercent = Math.min(state.targetPercent, state.shownPercent + step);
} else {
state.shownPercent = state.targetPercent;
}
renderBackupProgress(prefix);
if (state.targetPercent >= 100 && state.shownPercent >= 100) clearBackupProgressTimer(prefix);
}, 120);
}
function setBackupProgressActive(mode, active) {
const prefix = mode === 'restore' ? 'restore' : 'backup';
backupProgressActive[prefix] = !!active;
}
function applyBackupProgressEvent(mode, percent, detail) {
const prefix = mode === 'restore' ? 'restore' : 'backup';
if (!backupProgressActive[prefix]) return;
updateBackupProgressUi(mode, percent, detail);
}
function setBackupButtonsBusy(busy) {
const restoreSelect = el('s-restore-backup');
const hasPlaceholderOnly = restoreSelect.options.length === 1 && !restoreSelect.options[0].value;
const canRestore = restoreSelect.options.length > 0 && !hasPlaceholderOnly && !!restoreSelect.value;
el('s-backup-global').disabled = busy;
restoreSelect.disabled = busy || !canRestore;
el('s-restore-global').disabled = busy || !canRestore;
}
function formatBackupSize(bytes) {
const value = Number(bytes) || 0;
if (value < 1024 * 1024) return `${Math.max(1, Math.round(value / 1024))} KB`;
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
}
function formatBackupModified(isoString) {
try {
return new Date(isoString).toLocaleString('de-DE');
} catch {
return isoString || '';
}
}
async function refreshGlobalBackupOptions(selectedPath) {
const select = el('s-restore-backup');
const backups = await window.api.listGlobalBackups();
select.innerHTML = '';
if (!backups.length) {
const option = document.createElement('option');
option.value = '';
option.textContent = 'Keine Backups gefunden';
select.appendChild(option);
select.disabled = true;
el('s-restore-global').disabled = true;
return [];
}
backups.forEach((backup) => {
const option = document.createElement('option');
option.value = backup.path;
option.textContent = `${backup.name} · ${formatBackupModified(backup.modifiedAt)} · ${formatBackupSize(backup.size)}`;
select.appendChild(option);
});
select.value = backups.some((backup) => backup.path === selectedPath) ? selectedPath : backups[0].path;
select.disabled = false;
el('s-restore-global').disabled = !select.value;
return backups;
}
function updateBackupProgressUi(mode, percent, detail) {
const prefix = mode === 'restore' ? 'restore' : 'backup';
const state = backupProgressUiState[prefix];
const nextPercent = Math.max(0, Math.min(100, Number(percent) || 0));
const completed = state.targetPercent >= 100;
if (nextPercent === 0) {
state.shownPercent = 0;
state.targetPercent = 0;
state.detail = detail || '';
state.packing = false;
renderBackupProgress(prefix);
return;
}
if ((completed && nextPercent < 100) || nextPercent < state.targetPercent) {
return;
}
state.targetPercent = nextPercent;
state.detail = detail || '';
state.packing = /^Packe ZIP-Archiv/.test(state.detail) && nextPercent < 99;
if (nextPercent >= 99 || (nextPercent - state.shownPercent) >= 20 || state.shownPercent === 0) {
state.shownPercent = nextPercent;
}
renderBackupProgress(prefix);
if (state.shownPercent < state.targetPercent) ensureBackupProgressTimer(prefix);
else if (state.targetPercent >= 100) clearBackupProgressTimer(prefix);
}
async function runGlobalBackup() {
const statusLine = el('s-backup-status');
setBackupButtonsBusy(true);
setBackupProgressActive('create', true);
statusLine.className = 'hint';
updateBackupProgressUi('create', 0, 'Bereite Zielordner vor');
statusLine.textContent = 'Erstelle Backup in Dokumente/AeroMC Launcher/Backups ...';
try {
const res = await window.api.createGlobalBackup();
if (res.canceled) {
statusLine.className = 'hint';
statusLine.textContent = 'Backup abgebrochen.';
resetBackupProgressUi();
setBackupProgressActive('create', false);
return;
}
if (res.ok) {
updateBackupProgressUi('create', 100, 'Backup abgeschlossen');
setBackupProgressActive('create', false);
statusLine.className = 'hint status-ok';
statusLine.textContent = 'Backup erfolgreich erstellt';
await refreshGlobalBackupOptions(res.path);
toast('Globales Backup erstellt: ' + res.path);
return;
}
setBackupProgressActive('create', false);
statusLine.className = 'hint status-bad';
statusLine.textContent = '✖ ' + (res.message || 'Backup fehlgeschlagen.');
toast(res.message || 'Backup fehlgeschlagen.', true);
} finally {
setBackupProgressActive('create', false);
setBackupButtonsBusy(false);
}
}
async function runGlobalRestore() {
const statusLine = el('s-restore-status');
const selectedBackup = el('s-restore-backup').value;
if (!selectedBackup) {
statusLine.className = 'hint status-bad';
statusLine.textContent = 'Bitte ein Backup auswählen.';
return;
}
setBackupButtonsBusy(true);
setBackupProgressActive('restore', true);
updateBackupProgressUi('restore', 0, 'Warte auf Backup-Datei');
statusLine.className = 'hint';
statusLine.textContent = 'Stelle Backup wieder her ...';
try {
const res = await window.api.restoreGlobalBackup(selectedBackup);
if (res.canceled) {
statusLine.className = 'hint';
statusLine.textContent = 'Wiederherstellen abgebrochen.';
resetBackupProgressUi();
setBackupProgressActive('restore', false);
return;
}
if (!res.ok) {
setBackupProgressActive('restore', false);
statusLine.className = 'hint status-bad';
statusLine.textContent = '✖ ' + (res.message || 'Wiederherstellen fehlgeschlagen.');
toast(res.message || 'Wiederherstellen fehlgeschlagen.', true);
return;
}
updateBackupProgressUi('restore', 100, 'Wiederherstellen abgeschlossen');
setBackupProgressActive('restore', false);
statusLine.className = 'hint status-ok';
statusLine.textContent = 'Backup erfolgreich wiederhergestellt';
const s = await window.api.getSettings();
applyTheme(s.theme || 'dark');
iso3dEnabled = s.iso3dIcons !== false;
await applyBedrockVisibility();
await applySkinBackground();
hide('modal-settings');
await reload();
toast('Backup wiederhergestellt: ' + res.path);
} finally {
setBackupProgressActive('restore', false);
setBackupButtonsBusy(false);
}
}
function promptGlobalRestore() {
askConfirm(
'Backup wiederherstellen',
'Das überschreibt alle aktuellen Instanzen mit dem Inhalt des Backups. Fortfahren?',
'Wiederherstellen',
runGlobalRestore,
);
}
// ---------- Microsoft-Login / Kontoverwaltung ----------
async function refreshAccountButton() {
const acc = await window.api.authStatus();
@@ -1960,21 +2526,7 @@ function wire() {
else toast('Noch keine Instanz vorhanden.', false);
});
el('btn-import').addEventListener('click', async () => {
const res = await window.api.importInstance();
if (res.canceled) return;
if (res.ok) { await reload(); toast('Importiert: ' + res.instance.name); }
else toast(res.message || 'Import fehlgeschlagen.', true);
});
el('btn-modpack').addEventListener('click', async () => {
status('Importiere Modpack …');
const res = await window.api.importModpack();
status('Bereit'); el('status-right').textContent = '';
if (res.canceled) return;
if (res.ok) { await reload(); toast(`Modpack importiert: ${res.instance.name} (${res.fileCount} Dateien)`); }
else toast(res.message || 'Modpack-Import fehlgeschlagen.', true);
});
el('btn-import').addEventListener('click', () => { openImportDialog('zip'); });
// Skin-Hintergrund an/aus (wie MultiMCs Katzen-Knopf)
el('btn-skin').addEventListener('click', async () => {
@@ -2017,8 +2569,39 @@ function wire() {
if (h.ok) toast('Anmelde-Dienste wieder erreichbar.');
});
// MultiMC-Import
el('btn-mmc').addEventListener('click', () => { hide('modal-settings'); openMmc(); });
// Import
document.querySelectorAll('.import-tabs .mtab').forEach((tab) => {
tab.addEventListener('click', async () => {
setImportKind(tab.dataset.kind);
if (tab.dataset.kind === 'mc') await ensureDefaultMcScan();
});
});
el('import-zip-do').addEventListener('click', runZipImport);
el('import-modpack-do').addEventListener('click', runModpackImport);
el('mc-pick').addEventListener('click', async () => {
const p = await window.api.mcPickFolder();
if (p) { el('mc-path').value = p; await scanMc(); }
});
el('mc-scan').addEventListener('click', scanMc);
el('mc-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanMc(); });
el('mc-all').addEventListener('change', (e) => {
document.querySelectorAll('#mc-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateMcButton();
});
el('mc-list').addEventListener('change', updateMcButton);
el('mc-do').addEventListener('click', runMcImport);
el('cf-pick').addEventListener('click', async () => {
const p = await window.api.cfPickFolder();
if (p) { el('cf-path').value = p; await scanCf(); }
});
el('cf-scan').addEventListener('click', scanCf);
el('cf-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanCf(); });
el('cf-all').addEventListener('change', (e) => {
document.querySelectorAll('#cf-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateCfButton();
});
el('cf-list').addEventListener('change', updateCfButton);
el('cf-do').addEventListener('click', runCfImport);
el('mmc-pick').addEventListener('click', async () => {
const p = await window.api.mmcPickFolder();
if (p) { el('mmc-path').value = p; await scanMmc(); }
@@ -2031,8 +2614,17 @@ function wire() {
});
el('mmc-list').addEventListener('change', updateMmcButton);
el('mmc-do').addEventListener('click', runMmcImport);
window.api.onMcProgress((p) => {
el('mc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mc-progress').classList.remove('hidden');
});
window.api.onCfProgress((p) => {
el('cf-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('cf-progress').classList.remove('hidden');
});
window.api.onMmcProgress((p) => {
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mmc-progress').classList.remove('hidden');
});
// Bedrock
@@ -2083,9 +2675,9 @@ function wire() {
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-edit').addEventListener('click', async () => { const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-notes').addEventListener('click', async () => { const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-groupline').addEventListener('click', async () => { const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
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);
@@ -2104,16 +2696,36 @@ function wire() {
await reload(); await selectInstance(copy.id); toast('Kopiert: ' + copy.name);
});
el('p-delete').addEventListener('click', async () => {
const inst = await window.api.getInstance(currentDetailId);
if (!inst) return;
askConfirm('Instanz löschen',
`${inst.name}" wird mit allen Dateien unwiderruflich gelöscht. Fortfahren?`,
const ids = orderedSelectedInstanceIds();
if (!ids.length) return;
if (ids.length === 1) {
const inst = await window.api.getInstance(ids[0]);
if (!inst) return;
askConfirm('Instanz löschen',
`${inst.name}" wird mit allen Dateien unwiderruflich gelöscht. Fortfahren?`,
'Endgültig löschen',
async () => {
await window.api.deleteInstance(ids[0]);
selectedInstanceIds.clear();
await reload();
await selectInstance(instances.length ? instances[0].id : null);
toast('Gelöscht.');
});
return;
}
const names = instances.filter((inst) => selectedInstanceIds.has(inst.id)).map((inst) => inst.name);
const preview = names.slice(0, 3).map((name) => `${name}"`).join(', ');
const more = names.length > 3 ? ` und ${names.length - 3} weitere` : '';
askConfirm('Instanzen löschen',
`${ids.length} Instanzen (${preview}${more}) werden mit allen Dateien unwiderruflich gelöscht. Fortfahren?`,
'Endgültig löschen',
async () => {
await window.api.deleteInstance(currentDetailId);
for (const id of ids) await window.api.deleteInstance(id);
selectedInstanceIds.clear();
await reload();
await selectInstance(instances.length ? instances[0].id : null);
toast('Gelöscht.');
toast(`${ids.length} Instanzen gelöscht.`);
});
});
@@ -2161,6 +2773,8 @@ function wire() {
if (e.target.value) { el('s-java').value = e.target.value; await refreshJavaStatus(e.target.value); }
});
el('s-java').addEventListener('change', (e) => refreshJavaStatus(e.target.value.trim()));
el('s-backup-global').addEventListener('click', runGlobalBackup);
el('s-restore-global').addEventListener('click', promptGlobalRestore);
el('btn-save-settings').addEventListener('click', saveSettings);
document.querySelectorAll('.snav').forEach((b) => b.addEventListener('click', () => setSettingsPane(b.dataset.pane)));
el('s-open-accounts').addEventListener('click', () => { hide('modal-settings'); openLogin(); });
@@ -2228,6 +2842,7 @@ window.addEventListener('DOMContentLoaded', async () => {
} catch { applyTheme('dark'); }
await applySkinBackground();
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.onAuthHealth(showAuthHealth);