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