Upload via GUI (28 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-02 21:56:31 +02:00
parent b250ef16fd
commit 129ab2d939
9 changed files with 876 additions and 46 deletions
+323 -15
View File
@@ -620,6 +620,99 @@ async function runModpackImport() {
}
}
function fmtDownloads(n) {
n = Number(n) || 0;
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k';
return String(n);
}
async function searchModpacks() {
const query = el('mp-query').value.trim();
const gameVersion = el('mp-mc').value.trim();
const sort = el('mp-sort').value || 'relevance';
const box = el('mp-results');
const st = el('mp-status');
st.textContent = 'Suche …';
box.innerHTML = '<div class="mods-empty">Suche läuft …</div>';
try {
const res = await window.api.modpackSearch({
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack',
});
if (res.error) {
st.textContent = 'Fehler: ' + res.error;
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
return;
}
const hits = res.hits || [];
st.textContent = hits.length
? (res.total + ' Treffer' + (gameVersion ? ' für ' + gameVersion : ''))
: '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';
const meta = [
h.author ? ('von ' + h.author) : '',
h.downloads != null ? (fmtDownloads(h.downloads) + ' Downloads') : '',
].filter(Boolean).join(' · ');
const cats = (h.displayCategories || h.categories || []).slice(0, 4).join(', ');
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.title || h.slug || h.projectId)}</div>
<div class="bd-meta">${escapeHtml(meta)}${cats ? ' · ' + escapeHtml(cats) : ''}</div>
<div class="bd-meta" style="opacity:.85">${escapeHtml((h.description || '').slice(0, 120))}${(h.description || '').length > 120 ? '…' : ''}</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', () => installModpackFromSearch(h, btn));
item.querySelector('.mod-action').appendChild(btn);
box.appendChild(item);
}
} catch (err) {
st.textContent = 'Fehler: ' + (err.message || err);
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
}
}
async function installModpackFromSearch(hit, btn) {
if (!hit || !hit.projectId) return;
const gameVersion = el('mp-mc').value.trim();
const prev = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = '…'; }
status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …');
try {
const res = await window.api.modpackInstallFromModrinth({
projectId: hit.projectId,
title: hit.title || hit.slug,
gameVersion,
});
status('Bereit'); el('status-right').textContent = '';
if (res.ok) {
await reload();
hide('modal-import');
toast(`Modpack installiert: ${res.instance.name} (${res.fileCount || 0} Dateien)`);
if (currentDetailId !== res.instance.id) {
try { await selectInstance(res.instance.id); } catch { /* egal */ }
}
return;
}
toast(res.message || 'Installation fehlgeschlagen.', true);
} catch (err) {
status('Bereit'); el('status-right').textContent = '';
toast('Installation fehlgeschlagen: ' + (err.message || err), true);
} finally {
if (btn) { btn.disabled = false; btn.textContent = prev || 'Installieren'; }
}
}
async function scanMc() {
const folder = el('mc-path').value.trim();
el('mc-status').textContent = 'Suche Launcher-Profile …';
@@ -1689,14 +1782,14 @@ async function selectInstance(id) {
let launching = false;
async function launch(id, accountId) {
async function launch(id, accountId, launchOpts) {
if (launching) return;
launching = true;
status('Vorbereiten …');
el('status-right').textContent = '';
appendConsole('\n=== Start: ' + new Date().toLocaleTimeString('de-DE') + ' ===\n');
try {
const res = await window.api.launchInstance(id, accountId);
const res = await window.api.launchInstance(id, accountId, launchOpts || null);
if (res.ok) {
status('Läuft (PID ' + res.pid + ')');
toast('Minecraft gestartet.');
@@ -1847,6 +1940,37 @@ async function renderServers() {
<div class="mod-action"></div>`;
const aktionen = item.querySelector('.mod-action');
const starten = document.createElement('button');
starten.className = 'btn btn-primary btn-mini';
starten.textContent = 'Starten';
starten.title = 'Instanz starten und direkt diesem Server beitreten (Quick Play, ab MC 1.20)';
starten.addEventListener('click', () => {
hide('modal-servers');
launch(currentDetailId, null, { quickPlayServer: s.ip });
});
aktionen.appendChild(starten);
const umbenennen = document.createElement('button');
umbenennen.className = 'btn btn-mini';
umbenennen.textContent = 'Umbenennen';
umbenennen.title = 'Anzeigenamen ändern';
umbenennen.addEventListener('click', async () => {
const aktuell = s.name || s.ip;
const eingabe = await askPrompt({
title: 'Server umbenennen',
text: 'Adresse: ' + s.ip,
label: 'Name',
value: aktuell,
okLabel: 'Speichern',
});
if (eingabe == null) return; // abgebrochen
const res = await window.api.serversRename(currentDetailId, s.ip, eingabe);
if (!res.ok) { toast(res.message || 'Umbenennen fehlgeschlagen.', true); return; }
if (!res.unveraendert) toast('Umbenannt: ' + eingabe.trim());
await renderServers();
});
aktionen.appendChild(umbenennen);
const kopieren = document.createElement('button');
kopieren.className = 'btn btn-mini';
kopieren.textContent = 'Übertragen…';
@@ -2184,34 +2308,80 @@ function openConsole() {
// ---------- Welten & Ressourcenpakete ----------
async function openContent() {
const inst = await window.api.getInstance(currentDetailId);
el('content-title').textContent = 'Welten & Ressourcenpakete ' + inst.name;
el('content-title').textContent = 'Welten, Ressourcen- & Datenpakete ' + inst.name;
await renderContentLists();
show('modal-content');
}
async function renderContentLists() {
renderContentList('content-worlds', await window.api.worlds(currentDetailId), 'saves', '🌍');
renderContentList('content-rp', await window.api.resourcepacks(currentDetailId), 'resourcepacks', '🎨');
renderContentList('content-dp', await window.api.datapacks(currentDetailId), 'datapacks', '📜');
}
// ---------- Screenshots ----------
let screenSelected = new Set();
function updateScreensSelectionUi() {
const n = screenSelected.size;
const btn = el('screens-delete-sel');
if (btn) {
btn.disabled = n === 0;
btn.textContent = n ? (n + ' löschen') : 'Auswahl löschen';
}
const hint = el('screens-sel-hint');
if (hint) hint.textContent = n ? (n + ' ausgewählt Doppelklick öffnet das Bild') : 'Klick auf Vorschaubild zum Auswählen · Doppelklick öffnet';
document.querySelectorAll('#screens-grid .screen-thumb').forEach((t) => {
const name = t.dataset.name;
const on = name && screenSelected.has(name);
t.classList.toggle('selected', !!on);
const cb = t.querySelector('.screen-check');
if (cb) cb.checked = !!on;
});
}
async function openScreens() {
const inst = await window.api.getInstance(currentDetailId);
el('screens-title').textContent = 'Screenshots ' + inst.name;
const grid = el('screens-grid');
grid.innerHTML = '<div class="screens-empty">Lade …</div>';
screenSelected = new Set();
updateScreensSelectionUi();
show('modal-screens');
const shots = await window.api.screenshots(currentDetailId);
grid.innerHTML = '';
if (!shots.length) { grid.innerHTML = '<div class="screens-empty">Keine Screenshots vorhanden.</div>'; return; }
if (!shots.length) {
grid.innerHTML = '<div class="screens-empty">Keine Screenshots vorhanden.</div>';
updateScreensSelectionUi();
return;
}
for (const s of shots) {
const thumb = document.createElement('div');
thumb.className = 'screen-thumb';
thumb.dataset.name = s.name;
thumb.innerHTML =
`<img alt="" /><div class="screen-name">${escapeHtml(s.name)}</div>
`<input type="checkbox" class="screen-check" title="Auswählen" />
<img alt="" /><div class="screen-name">${escapeHtml(s.name)}</div>
<button class="screen-del" title="Löschen">🗑</button>`;
const img = thumb.querySelector('img');
window.api.screenshotData(currentDetailId, s.name).then((d) => { if (d) img.src = d; });
img.addEventListener('click', () => window.api.openScreenshot(currentDetailId, s.name));
const toggle = () => {
if (screenSelected.has(s.name)) screenSelected.delete(s.name);
else screenSelected.add(s.name);
updateScreensSelectionUi();
};
thumb.querySelector('.screen-check').addEventListener('click', (e) => {
e.stopPropagation();
toggle();
});
thumb.addEventListener('click', (e) => {
if (e.target.closest('.screen-del') || e.target.closest('.screen-check')) return;
toggle();
});
img.addEventListener('dblclick', (e) => {
e.stopPropagation();
window.api.openScreenshot(currentDetailId, s.name);
});
thumb.querySelector('.screen-del').addEventListener('click', (e) => {
e.stopPropagation();
askConfirm('Screenshot löschen', `${s.name}" löschen?`, '🗑 Löschen', async () => {
@@ -2221,6 +2391,22 @@ async function openScreens() {
});
grid.appendChild(thumb);
}
updateScreensSelectionUi();
}
async function deleteSelectedScreenshots() {
const names = [...screenSelected];
if (!names.length || !currentDetailId) return;
const label = names.length === 1
? `${names[0]}" löschen?`
: `${names.length} Screenshots unwiderruflich löschen?`;
askConfirm('Screenshots löschen', label, '🗑 Löschen', async () => {
for (const name of names) {
await window.api.deleteContent(currentDetailId, 'screenshots', name);
}
toast(names.length === 1 ? 'Screenshot gelöscht.' : names.length + ' Screenshots gelöscht.');
openScreens();
});
}
function renderContentList(boxId, items, sub, emoji) {
@@ -2457,28 +2643,52 @@ async function loadCatalog(append) {
if (ART_BRAUCHT_LOADER[modArt] && inst.minecraft.loader === 'vanilla') return;
const box = el('m-results');
if (!append) { modOffset = 0; box.innerHTML = '<div class="mods-empty">Lade …</div>'; el('m-more').classList.add('hidden'); }
const source = (el('m-source') && el('m-source').value) || 'modrinth';
if (el('m-category')) el('m-category').disabled = source === 'curseforge' || modArt !== 'mod';
try {
const res = await window.api.modSearch({
query: el('m-query').value.trim(),
gameVersion: inst.minecraft.version,
loader: inst.minecraft.loader,
category: modArt === 'mod' ? el('m-category').value : '',
category: (source === 'modrinth' && modArt === 'mod') ? el('m-category').value : '',
sort: el('m-sort').value,
limit: 30,
offset: modOffset,
art: modArt,
source,
});
if (!append) box.innerHTML = '';
if (!res.hits.length && !append) {
if (res && (res.error || res.needKey)) {
if (!append) {
box.innerHTML = `<div class="mods-empty">${escapeHtml(res.error || 'Suche nicht möglich.')}</div>`;
if (res.needKey || /API-Key|Weiterleitung reicht/i.test(res.error || '')) {
const wrap = document.createElement('div');
wrap.className = 'mods-empty';
const btn = document.createElement('button');
btn.className = 'btn btn-mini';
btn.textContent = 'Zu den Netzwerk-Einstellungen';
btn.addEventListener('click', () => { hide('modal-mods'); openSettings('pane-network'); });
wrap.appendChild(btn);
box.appendChild(wrap);
}
}
el('m-more').classList.add('hidden');
return;
}
const hits = (res && res.hits) || [];
if (!hits.length && !append) {
box.innerHTML = '<div class="mods-empty">Nichts gefunden.</div>';
el('m-more').classList.add('hidden');
return;
}
for (const mod of res.hits) box.appendChild(buildResultItem(mod));
modOffset += res.hits.length;
el('m-more').classList.toggle('hidden', res.hits.length < 30 || modOffset >= res.total);
for (const mod of hits) box.appendChild(buildResultItem(mod));
modOffset += hits.length;
el('m-more').classList.toggle('hidden', hits.length < 30 || modOffset >= (res.total || 0));
} catch (e) {
if (!append) box.innerHTML = `<div class="mods-empty">Fehler: ${escapeHtml(e.message)}</div>`;
if (!append) {
const msg = (e && e.message) ? e.message : String(e);
box.innerHTML = `<div class="mods-empty">Fehler: ${escapeHtml(msg)}</div>`;
}
}
}
@@ -2502,7 +2712,7 @@ function buildResultItem(mod) {
info.innerHTML =
`<div class="mod-name">${escapeHtml(mod.title)}</div>
<div class="mod-desc">${escapeHtml(mod.description || '')}</div>
<div class="mod-meta">von ${escapeHtml(mod.author)} · ${Number(mod.downloads).toLocaleString('de-DE')} Downloads</div>`;
<div class="mod-meta">von ${escapeHtml(mod.author || 'unbekannt')} · ${Number(mod.downloads).toLocaleString('de-DE')} Downloads${mod.source === 'curseforge' ? ' · CurseForge' : ''}</div>`;
const action = document.createElement('div');
action.className = 'mod-action';
@@ -3119,6 +3329,31 @@ function onAuthCode(code) {
}
// ---------- Bestätigung ----------
let promptResolver = null;
function askPrompt({ title, text, label, value, okLabel } = {}) {
return new Promise((resolve) => {
// falls schon ein Prompt offen ist
if (promptResolver) { promptResolver(null); promptResolver = null; }
promptResolver = resolve;
el('prompt-title').textContent = title || 'Eingabe';
el('prompt-text').textContent = text || '';
el('prompt-text').classList.toggle('hidden', !text);
el('prompt-label').textContent = label || 'Wert';
el('prompt-ok').textContent = okLabel || 'OK';
const input = el('prompt-input');
input.value = value != null ? String(value) : '';
show('modal-prompt');
setTimeout(() => { input.focus(); input.select(); }, 30);
});
}
function closePrompt(wert) {
hide('modal-prompt');
const r = promptResolver;
promptResolver = null;
if (r) r(wert);
}
function askConfirm(title, text, okLabel, action) {
el('confirm-title').textContent = title;
el('confirm-text').textContent = text;
@@ -3189,10 +3424,20 @@ function wire() {
tab.addEventListener('click', async () => {
setImportKind(tab.dataset.kind);
if (tab.dataset.kind === 'mc') await ensureDefaultMcScan();
if (tab.dataset.kind === 'modpack') {
const box = el('mp-results');
if (box && !box.dataset.loaded) {
box.dataset.loaded = '1';
searchModpacks();
}
}
});
});
el('import-zip-do').addEventListener('click', runZipImport);
el('import-modpack-do').addEventListener('click', runModpackImport);
el('mp-search-btn').addEventListener('click', searchModpacks);
el('mp-query').addEventListener('keydown', (e) => { if (e.key === 'Enter') searchModpacks(); });
el('mp-mc').addEventListener('keydown', (e) => { if (e.key === 'Enter') searchModpacks(); });
el('mc-pick').addEventListener('click', async () => {
const p = await window.api.mcPickFolder();
if (p) { el('mc-path').value = p; await scanMc(); }
@@ -3375,12 +3620,25 @@ function wire() {
el('content-open-worlds').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'saves'));
el('content-open-rp').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'resourcepacks'));
el('screens-open').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'screenshots'));
el('screens-select-all').addEventListener('click', () => {
document.querySelectorAll('#screens-grid .screen-thumb').forEach((t) => {
if (t.dataset.name) screenSelected.add(t.dataset.name);
});
updateScreensSelectionUi();
});
el('screens-select-none').addEventListener('click', () => {
screenSelected.clear();
updateScreensSelectionUi();
});
el('screens-delete-sel').addEventListener('click', () => deleteSelectedScreenshots());
el('content-open-dp').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'datapacks'));
// Mod-Dialog
el('m-search-btn').addEventListener('click', () => loadCatalog(false));
el('m-query').addEventListener('keydown', (e) => { if (e.key === 'Enter') loadCatalog(false); });
el('m-sort').addEventListener('change', () => loadCatalog(false));
el('m-category').addEventListener('change', () => loadCatalog(false));
if (el('m-source')) el('m-source').addEventListener('change', () => loadCatalog(false));
el('m-more').addEventListener('click', () => loadCatalog(true));
el('m-tab-catalog').addEventListener('click', () => switchModTab('catalog'));
el('m-tab-installed').addEventListener('click', () => switchModTab('installed'));
@@ -3447,6 +3705,25 @@ function wire() {
el('s-backup-global').addEventListener('click', runGlobalBackup);
el('s-restore-global').addEventListener('click', promptGlobalRestore);
el('btn-save-settings').addEventListener('click', saveSettings);
if (el('s-cf-test')) {
el('s-cf-test').addEventListener('click', async () => {
const st = el('s-cf-test-status');
const key = el('s-cf-key').value.trim();
st.textContent = 'Prüfe bei CurseForge …';
try {
const res = await window.api.curseforgeTestKey(key);
st.textContent = (res.ok ? '✓ ' : '✖ ') + (res.message || '');
if (res.proben) {
st.textContent += ' [' + res.proben.map((p) => p.name + ':' + p.status).join(', ') + ']';
}
if (res.siehtAusWieCurseForge === false && key) {
st.textContent += ' Hinweis: Erwartet wird ein Studios-Key (cfc_pat_…) oder ein klassischer Key ($2a$10$…).';
}
} catch (err) {
st.textContent = '✖ ' + (err.message || err);
}
});
}
document.querySelectorAll('.snav').forEach((b) => b.addEventListener('click', () => setSettingsPane(b.dataset.pane)));
el('s-open-accounts').addEventListener('click', () => { hide('modal-settings'); openLogin(); });
el('s-add-account').addEventListener('click', () => { hide('modal-settings'); openLogin().then(addAccount); });
@@ -3484,6 +3761,27 @@ function wire() {
});
// Bestätigung
el('prompt-ok').addEventListener('click', () => {
const v = el('prompt-input').value;
closePrompt(v);
});
el('prompt-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
closePrompt(el('prompt-input').value);
}
});
// Abbrechen / Schließen des Prompt-Dialogs liefert null
el('modal-prompt').querySelectorAll('[data-close]').forEach((b) => {
b.addEventListener('click', (e) => {
e.stopPropagation();
closePrompt(null);
});
});
el('modal-prompt').addEventListener('mousedown', (e) => {
if (e.target === el('modal-prompt')) closePrompt(null);
});
el('confirm-ok').addEventListener('click', async () => {
hide('modal-confirm');
if (confirmAction) { const a = confirmAction; confirmAction = null; await a(); }
@@ -3491,12 +3789,22 @@ function wire() {
// generisches Schließen
document.querySelectorAll('[data-close]').forEach((b) =>
b.addEventListener('click', () => b.closest('.modal').classList.add('hidden')));
b.addEventListener('click', () => {
const m = b.closest('.modal');
if (!m) return;
if (m.id === 'modal-prompt') { closePrompt(null); return; }
m.classList.add('hidden');
}));
document.querySelectorAll('.modal').forEach((m) =>
m.addEventListener('mousedown', (e) => { if (e.target === m) m.classList.add('hidden'); }));
m.addEventListener('mousedown', (e) => {
if (e.target !== m) return;
if (m.id === 'modal-prompt') { closePrompt(null); return; }
m.classList.add('hidden');
}));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (!el('ctx-menu').classList.contains('hidden')) { closeContextMenu(); return; }
if (!el('modal-prompt').classList.contains('hidden')) { closePrompt(null); return; }
const open = [...document.querySelectorAll('.modal:not(.hidden)')].pop();
if (open) open.classList.add('hidden');
}