Upload via GUI (194 Dateien)
This commit is contained in:
+187
-7
@@ -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(() => {});
|
||||
|
||||
Reference in New Issue
Block a user