Upload via GUI (194 Dateien)

This commit is contained in:
2026-10-07 07:32:02 +00:00
parent 7e7a2021ed
commit 3ac5a6c14d
20 changed files with 2196 additions and 489 deletions
+187 -7
View File
@@ -1702,8 +1702,12 @@ function showUpdateDialog(info) {
el('upd-install').textContent = 'Installieren';
el('upd-open').classList.toggle('hidden', hatPaket || !info.url);
el('upd-hint').textContent = hatPaket
? 'Der Launcher lädt das Update herunter und startet die Installation automatisch.'
: (channel
? 'Der Launcher lädt das Update herunter, prüft die Signatur und startet die Installation automatisch.'
: info.signaturHinweis
? info.signaturHinweis
: info.unsigniert
? 'Dieses Release ist nicht signiert. Aus Sicherheitsgründen wird es nicht automatisch installiert – bitte nur von der offiziellen Seite laden.'
: (channel
? `Für den Kanal ${channel} liegt in diesem Release keine passende Datei bereit. Bitte im Release nachsehen und von Hand aktualisieren.`
: 'Die Paketart dieses Launchers lässt sich nicht sicher bestimmen. Bitte das Update von Hand aus dem Release laden.');
show('modal-update');
@@ -1729,7 +1733,7 @@ async function startUpdate() {
el('upd-progress').classList.remove('hidden');
el('upd-status').textContent = 'Wird heruntergeladen …';
const res = await window.api.updateInstall(pendingUpdate.asset);
const res = await window.api.updateInstall();
if (res.ok) {
el('upd-status').textContent = res.kind === 'portable'
? 'Launcher schließt sich, wird ersetzt und startet automatisch neu.'
@@ -2233,6 +2237,7 @@ async function refreshSelectionPanel() {
if (count > 1) {
currentDetailId = null;
zeigeSperre(null);
setSingleSelectionActionsDisabled(true);
el('p-delete').disabled = false;
el('p-delete').textContent = `${count} Instanzen löschen`;
@@ -2283,6 +2288,49 @@ async function refreshSelectionPanel() {
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);
zeigeSperre(inst);
}
// Manipulierte Instanz (siehe integritaet.js im Hauptprozess): nicht startbar,
// nicht kopier-/export-/teilbar – nur ansehen, wiederherstellen oder löschen
const GESPERRT_AUS = ['p-play', 'p-offline', 'p-edit', 'p-mods', 'p-modpack', 'p-record', 'p-servers', 'p-shortcut', 'p-export', 'p-share', 'p-copy'];
function zeigeSperre(inst) {
const sperre = inst && inst.gesperrt && inst.gesperrt.gesperrt ? inst.gesperrt : null;
el('p-gesperrt').classList.toggle('hidden', !sperre);
el('p-icon').classList.toggle('gesperrt', !!sperre);
['p-play', 'p-offline'].forEach((id) => el(id).classList.toggle('hidden', !!sperre));
el('p-share').disabled = false;
el('p-record').disabled = false;
if (!sperre) return;
GESPERRT_AUS.forEach((id) => { el(id).disabled = true; });
if (!el('p-icon').querySelector('.side-verbot')) {
el('p-icon').insertAdjacentHTML('beforeend', '<img class="side-verbot" src="assets/verbot.svg" alt="" />');
}
const gruende = sperre.gruende || [];
const liste = el('p-gesperrt-gruende');
liste.innerHTML = gruende.slice(0, 5).map((g) => `<li>${escapeHtml(g)}</li>`).join('')
+ (gruende.length > 5 ? `<li>… und ${gruende.length - 5} weitere</li>` : '')
+ (sperre.seit ? `<li class="dim">Erkannt: ${escapeHtml(fmtDate(sperre.seit))}</li>` : '');
el('status-play').textContent = 'Gesperrt – bitte aus einer Sicherung wiederherstellen';
}
async function wiederherstellenAusSicherung() {
const id = currentDetailId;
if (!id) return;
const knopf = el('p-wiederherstellen');
knopf.disabled = true;
knopf.textContent = 'Sicherungen werden geprüft …';
try {
const res = await window.api.integritaetWiederherstellen(id);
if (!res.ok) { toast(res.message || 'Wiederherstellen fehlgeschlagen.', true); return; }
toast(`Wiederhergestellt aus ${res.sicherung} als „${res.name}“. Die gesperrte Instanz kannst du jetzt löschen.`);
await reload();
await selectInstance(res.id);
} finally {
knopf.disabled = false;
knopf.textContent = 'Aus Sicherung wiederherstellen';
}
}
async function applySelection(ids) {
@@ -2313,13 +2361,17 @@ function buildCard(inst) {
const taste = schnellstartTasteVon(inst.id);
if (taste) tip += `\nSchnellstart: Strg+${taste}`;
if (inst.notes && inst.notes.trim()) tip += '\n\n' + inst.notes.trim();
const gesperrt = !!(inst.gesperrt && inst.gesperrt.gesperrt);
if (gesperrt) tip = 'GESPERRT – außerhalb des Launchers verändert\n' + tip;
card.title = tip;
card.innerHTML = `
${iconHtml}
<i class="card-riss" aria-hidden="true"></i>
${gesperrt ? '<img class="card-verbot" src="assets/verbot.svg" alt="Gesperrt" />' : ''}
<div class="card-name">${escapeHtml(inst.name)}</div>
${taste ? `<span class="card-taste">Strg+${taste}</span>` : ''}`;
if (gesperrt) card.classList.add('gesperrt');
if (laufendeIds.has(inst.id)) card.classList.add('laeuft');
if (startStand.has(inst.id)) zeigeAbbau(card);
@@ -2516,9 +2568,13 @@ function baueGruppenSektion(knoten, tiefe, showHeaders) {
// Drop-Ziel: Instanz per Drag&Drop in diese (Unter-)Gruppe verschieben –
// stopPropagation, damit die innerste Gruppe gewinnt, nicht die äußere
section.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); section.classList.add('drop-hover'); });
// Dateien aus dem Explorer gehören nicht hierher, sondern zum Datei-Import
// (wireFileDrop) – sonst würde die Gruppe sie verschlucken
const istDateiZug = (e) => e.dataTransfer && [...e.dataTransfer.types].includes('Files');
section.addEventListener('dragover', (e) => { if (istDateiZug(e)) return; e.preventDefault(); e.stopPropagation(); section.classList.add('drop-hover'); });
section.addEventListener('dragleave', (e) => { if (!section.contains(e.relatedTarget)) section.classList.remove('drop-hover'); });
section.addEventListener('drop', async (e) => {
if (istDateiZug(e)) return;
e.preventDefault();
e.stopPropagation();
section.classList.remove('drop-hover');
@@ -2860,6 +2916,11 @@ let launching = false;
async function launch(id, accountId, launchOpts) {
if (launching) return;
const lokal = instances.find((i) => i.id === id);
if (lokal && lokal.gesperrt && lokal.gesperrt.gesperrt) {
toast('Diese Instanz ist gesperrt, weil sie verändert wurde – bitte aus einer Sicherung wiederherstellen.', true);
return;
}
launching = true;
status('Vorbereiten …');
el('status-right').textContent = '';
@@ -4987,6 +5048,101 @@ async function installiereTeilCode() {
el('code-preview').classList.add('hidden');
}
// ---------- Mod-Schalter (Fenster über dem laufenden Spiel) ----------
// Zwei Listen wie beim Ressourcenpaket-Bildschirm: ein Klick schiebt eine Mod in
// die andere Liste. Läuft das Spiel, merkt der Launcher die Änderung vor und
// setzt sie beim Beenden um (geladene Mods lassen sich nicht entladen).
let msInstanz = null;
let msDaten = null;
let msStart = null; // Stand beim Öffnen: filename -> an
async function openModschalter(instanceId) {
if (POPOUT !== 'modschalter') return;
msInstanz = instanceId || new URLSearchParams(location.search).get('instanceId');
msStart = null;
el('ms-suche').value = '';
show('modal-modschalter');
await ladeModschalter();
el('ms-suche').focus();
}
async function ladeModschalter(bewegt) {
const d = await window.api.modschalterDaten(msInstanz);
if (!d.ok) { el('ms-status').textContent = d.message || 'Instanz nicht gefunden.'; return; }
msDaten = d;
if (!msStart) msStart = new Map(d.mods.map((m) => [m.filename, m.enabled !== false]));
el('ms-instanz').textContent = `${d.name} · ${d.version}`;
zeichneModschalter(bewegt);
}
function zeichneModschalter(bewegt) {
const q = el('ms-suche').value.trim().toLowerCase();
const passt = (m) => !q || `${m.title || ''} ${m.filename}`.toLowerCase().includes(q);
const mods = msDaten.mods.filter((m) => !m.fehlt).sort((a, b) => (a.title || a.filename).localeCompare(b.title || b.filename, 'de'));
const an = mods.filter((m) => m.enabled !== false), aus = mods.filter((m) => m.enabled === false);
el('ms-zahl-an').textContent = `(${an.length})`;
el('ms-zahl-aus').textContent = `(${aus.length})`;
for (const [box, liste, istAn] of [[el('ms-an'), an, true], [el('ms-aus'), aus, false]]) {
box.innerHTML = '';
const sichtbar = liste.filter(passt);
if (!sichtbar.length) {
box.innerHTML = `<div class="ms-leer">${q ? 'Keine Treffer.' : istAn ? 'Keine aktiven Mods.' : 'Hierher verschobene Mods werden deaktiviert.'}</div>`;
continue;
}
for (const m of sichtbar) {
const zeile = document.createElement('div');
zeile.className = 'ms-mod' + (bewegt === m.filename ? ' kommt' : '');
zeile.style.setProperty('--ms-von', istAn ? '14px' : '-14px');
zeile.setAttribute('role', 'listitem');
zeile.title = istAn ? 'Klicken zum Deaktivieren' : 'Klicken zum Aktivieren';
const geaendert = msStart && msStart.get(m.filename) !== (m.enabled !== false);
zeile.innerHTML = `<div class="ms-icon">${m.iconUrl ? `<img src="${escapeHtml(m.iconUrl)}" alt="" />` : '🧩'}</div>
<div class="ms-info"><div class="ms-name">${escapeHtml(m.title || m.filename)}${geaendert ? '<span class="ms-neu">ab nächstem Start</span>' : ''}</div>
<div class="ms-meta">${escapeHtml([m.versionNumber && m.versionNumber !== 'Importiert' ? m.versionNumber : '', m.filename].filter(Boolean).join(' · '))}</div>
<div class="ms-warn"></div></div>
<button class="ms-pfeil" tabindex="-1">${istAn ? '›' : '‹'}</button>`;
const img = zeile.querySelector('img');
if (img) img.addEventListener('error', () => { img.parentElement.textContent = '🧩'; });
zeile.addEventListener('click', () => schalteMod(m, !istAn, zeile));
box.appendChild(zeile);
}
}
const geaendert = msStart ? msDaten.mods.filter((m) => msStart.get(m.filename) !== (m.enabled !== false)).length : 0;
el('ms-neustart').classList.toggle('hidden', !(msDaten.laeuft && geaendert));
const z = msDaten.ziel;
el('ms-neustart').title = z && z.quickPlayServer
? `Minecraft speichert, startet mit den neuen Mods neu und verbindet wieder mit ${z.quickPlayServer}`
: z && z.quickPlayWelt && msDaten.quickPlayWelt
? `Minecraft speichert, startet mit den neuen Mods neu und öffnet wieder die Welt „${z.quickPlayWelt}“`
: 'Minecraft speichert und startet mit den neuen Mods neu (ins Hauptmenü)';
el('ms-status').textContent = geaendert
? (msDaten.laeuft
? `${geaendert} Änderung${geaendert === 1 ? '' : 'en'} – gilt ab dem nächsten Start (das Spiel läuft weiter)`
+ (z && z.quickPlayServer ? ` · Neustart verbindet wieder mit ${z.quickPlayServer}` : z && z.quickPlayWelt && msDaten.quickPlayWelt ? ` · Neustart öffnet wieder „${z.quickPlayWelt}“` : '')
: `${geaendert} Änderung${geaendert === 1 ? '' : 'en'} übernommen`)
: 'Klicke eine Mod an, um sie in die andere Liste zu verschieben.';
}
async function schalteMod(m, an, zeile) {
if (!an) {
// Warnen, wenn andere Mods diese brauchen – verschoben wird trotzdem
const brauchen = await window.api.modDependents(msInstanz, m.filename, 'mod').catch(() => []);
if (brauchen && brauchen.length) toast(`${m.title || m.filename} wird gebraucht von: ${brauchen.slice(0, 3).join(', ')}${brauchen.length > 3 ? ' …' : ''}`, true);
}
if (zeile) zeile.style.pointerEvents = 'none';
const res = await window.api.modSetEnabled(msInstanz, m.filename, an, 'mod');
if (!res.ok) toast(res.message || 'Umschalten fehlgeschlagen.', true);
await ladeModschalter(m.filename);
}
function schliesseModschalter() { window.api.modschalterSchliessen(); }
async function modschalterNeustart() {
el('ms-neustart').disabled = true;
el('ms-status').textContent = 'Minecraft speichert und startet neu …';
const res = await window.api.modschalterNeustart(msInstanz);
if (!res.ok) { el('ms-neustart').disabled = false; toast(res.message || 'Neustart nicht möglich.', true); }
}
// ---------- Spielzeit-Statistik ----------
// Gesamtwerte als Kennzahlen, darunter eine nach Spielzeit sortierte Balkenliste
// (eine Reihe = eine Instanz, Balkenlänge relativ zur meistgespielten).
@@ -5704,6 +5860,7 @@ async function refreshInstalled() {
const kennzeichen = [];
if (up) kennzeichen.push('<span class="mod-new">Update</span>');
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
if (m.ausstehend) kennzeichen.push('<span class="mod-new" title="Das Spiel läuft – die Änderung wird beim Beenden übernommen">ab nächstem Start</span>');
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
// OptiFine lässt sich nicht über Modrinth/CurseForge installieren (kein API-Zugriff auf
// deren Downloads) – von Hand hinzugefügt taucht es sonst wie ein beliebiger Mod auf.
@@ -5733,7 +5890,7 @@ async function refreshInstalled() {
schalter.addEventListener('click', async () => {
schalter.disabled = true;
const res = await window.api.modSetEnabled(currentDetailId, m.filename, aus, modArt);
if (res.ok) toast((aus ? 'Eingeschaltet: ' : 'Ausgeschaltet: ') + m.title);
if (res.ok) toast((aus ? 'Eingeschaltet: ' : 'Ausgeschaltet: ') + m.title + (res.ausstehend ? ' – gilt ab dem nächsten Start' : ''));
else toast(res.message || 'Umschalten fehlgeschlagen.', true);
await refreshInstalled();
});
@@ -5981,7 +6138,9 @@ async function openSettings(pane) {
el('s-friends').checked = s.friendsEnabled !== false;
el('s-friends-server').checked = s.friendsShareServer !== false;
el('s-auto-mod-updates').checked = !!s.autoModUpdates;
el('s-mod-schalter').checked = s.modSchalter !== false;
el('s-aeromc-capes').checked = s.aeromcCapes !== false;
el('s-aeromc-design').checked = s.aeromcDesign !== false;
el('s-autostart').checked = !!s.autostart;
el('s-auto-java').checked = s.autoJava !== false;
el('s-autobackup').checked = !!s.autoBackupEnabled;
@@ -6117,7 +6276,9 @@ async function saveSettings() {
friendsEnabled: el('s-friends').checked,
friendsShareServer: el('s-friends-server').checked,
autoModUpdates: el('s-auto-mod-updates').checked,
modSchalter: el('s-mod-schalter').checked,
aeromcCapes: el('s-aeromc-capes').checked,
aeromcDesign: el('s-aeromc-design').checked,
autostart: el('s-autostart').checked,
autoJava: el('s-auto-java').checked,
autoBackupEnabled: el('s-autobackup').checked,
@@ -6950,6 +7111,7 @@ function wire() {
// Rechte Aktionsleiste (Instanz-Aktionen)
el('p-play').addEventListener('click', () => currentDetailId && launch(currentDetailId));
el('p-wiederherstellen').addEventListener('click', wiederherstellenAusSicherung);
el('p-offline').addEventListener('click', () => currentDetailId && launch(currentDetailId));
el('p-shortcut').addEventListener('click', async () => {
if (!currentDetailId) return;
@@ -7457,8 +7619,8 @@ function wireFileDrop() {
e.preventDefault();
dragDepth = 0;
document.body.classList.remove('file-drop-active');
const files = [...(e.dataTransfer.files || [])].filter((f) => f.path);
for (const f of files) await handleDroppedFile(f.path);
const pfade = [...(e.dataTransfer.files || [])].map((f) => window.api.pfadVonDatei(f)).filter(Boolean);
for (const pfad of pfade) await handleDroppedFile(pfad);
});
}
@@ -7523,6 +7685,7 @@ const POPOUT_OPENERS = {
record: () => openRecording(),
friends: () => openFriends(),
stats: (p) => openStats(p.reiter),
modschalter: (p) => openModschalter(p.instanceId),
};
function popoutBoot(params) {
@@ -7612,6 +7775,23 @@ window.addEventListener('DOMContentLoaded', async () => {
await refreshSkinBackground();
window.api.onLaunchProgress(onLaunchEvt);
window.api.onErfolgeNeu(zeigeErfolgMeldung);
// Mod-Schalter
el('ms-suche').addEventListener('input', () => msDaten && zeichneModschalter());
el('ms-x').addEventListener('click', schliesseModschalter);
el('ms-fertig').addEventListener('click', schliesseModschalter);
el('ms-neustart').addEventListener('click', modschalterNeustart);
document.addEventListener('keydown', (e) => { if (POPOUT === 'modschalter' && e.key === 'Escape') schliesseModschalter(); });
// Neustart nach dem Mod-Schalter: wieder in dieselbe Welt bzw. auf denselben Server
if (!POPOUT) {
window.api.onModschalterNeustarten(({ id, ziel }) => {
if (!instances.some((i) => i.id === id)) return;
const wohin = ziel && ziel.quickPlayServer ? ` und verbindet wieder mit ${ziel.quickPlayServer}` : ziel && ziel.quickPlayWelt ? ` in die Welt „${ziel.quickPlayWelt}“` : '';
toast(`Mods übernommen – Minecraft startet neu${wohin} …`);
const opts = ziel && (ziel.quickPlayServer || ziel.quickPlayWelt)
? { quickPlayServer: ziel.quickPlayServer || '', quickPlayWelt: ziel.quickPlayWelt || '' } : null;
launch(id, (ziel && ziel.kontoId) || null, opts);
});
}
el('upd-notes').addEventListener('click', updateLinkKlick);
window.api.onLaufendeInstanzen(setzeLaufende);
window.api.laufendeInstanzen().then(setzeLaufende).catch(() => {});