Upload via GUI (18 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-01 07:43:21 +02:00
parent 1978358f21
commit afe02f81be
10 changed files with 721 additions and 27 deletions
+240 -14
View File
@@ -117,7 +117,86 @@ const ICON_LABELS = {
sword: 'Schwert', pickaxe: 'Spitzhacke', potion: 'Trank', nether: 'Feuer', end: 'Endauge',
redstone: 'Redstone', gold: 'Goldbarren', book: 'Buch', map: 'Karte', star: 'Netherstern',
};
function iconOf(key, size = 44) { return pixelIconSvg(key, size); }
// echte Minecraft-Icons (aus den Spieldateien extrahiert), key -> dataUri
let mcIcons = {};
async function loadMcIcons() {
try {
let list = await window.api.iconsList();
if (!list.length) {
const res = await window.api.iconsExtract();
list = (res && res.ok) ? res.icons : [];
}
mcIcons = Object.fromEntries(list.map((i) => [i.key, i]));
await buildIsoCubes(); // räumliche Block-Symbole vorbereiten
} catch { mcIcons = {}; }
}
// erzeugt die Würfel-Darstellungen einmalig im Voraus
async function buildIsoCubes() {
const jobs = Object.values(mcIcons)
.filter((i) => BLOCK_KEYS.has(i.key))
.map((i) => new Promise((resolve) => {
const img = new Image();
img.onload = () => { isoSources[i.key] = img; resolve(); };
img.onerror = () => resolve();
img.src = i.dataUri;
}));
await Promise.all(jobs);
for (const key of Object.keys(isoSources)) renderIsoCube(key);
}
// Block-Texturen, die als räumlicher Würfel dargestellt werden
const BLOCK_KEYS = new Set(['grass', 'dirt', 'stone', 'planks', 'tnt', 'obsidian', 'netherrack',
'crafting', 'furnace', 'bookshelf', 'diamondblock', 'goldblock', 'emeraldblock', 'redstoneblock']);
let iso3dEnabled = true;
const isoCache = {}; // key -> fertiges Würfelbild
const isoSources = {}; // key -> geladenes Textur-Bild
// zeichnet aus einer 16x16-Textur einen isometrischen Würfel (Canvas -> data:-URI)
function renderIsoCube(key) {
const img = isoSources[key];
if (!img) return null;
const S = 64, c = document.createElement('canvas');
c.width = S; c.height = S;
const g = c.getContext('2d');
g.imageSmoothingEnabled = false;
const w = 26, h = 15, cx = S / 2, top = 8;
const face = (m, bright) => {
g.save();
g.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
g.drawImage(img, 0, 0, 16, 16);
g.restore();
if (bright !== 1) { // Fläche abdunkeln/aufhellen
g.save();
g.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
g.globalCompositeOperation = bright < 1 ? 'source-atop' : 'lighter';
g.fillStyle = bright < 1 ? `rgba(0,0,0,${(1 - bright).toFixed(2)})` : 'rgba(255,255,255,.12)';
g.fillRect(0, 0, 16, 16);
g.restore();
}
};
// Oberseite (Raute), linke und rechte Seitenfläche
face([w / 16, h / 16, -w / 16, h / 16, cx, top], 1);
face([w / 16, h / 16, 0, 30 / 16, cx - w, top + h], 0.72);
face([w / 16, -h / 16, 0, 30 / 16, cx, top + h * 2], 0.55);
const out = c.toDataURL('image/png');
isoCache[key] = out;
return out;
}
// liefert das Icon-Markup: echte Textur bevorzugt, sonst eigene Pixel-Grafik
function iconOf(key, size = 44) {
const real = mcIcons[key];
if (real) {
let src = real.dataUri;
if (iso3dEnabled && BLOCK_KEYS.has(key) && isoCache[key]) src = isoCache[key];
return `<img src="${src}" width="${size}" height="${size}" style="image-rendering:pixelated;display:block" alt="" />`;
}
return pixelIconSvg(key, size);
}
const LOADER_LABEL = {
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
@@ -177,6 +256,66 @@ function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark');
}
// ---------- Kontextmenü (Rechtsklick auf eine Instanz) ----------
function openContextMenu(x, y) {
const menu = el('ctx-menu');
menu.classList.remove('hidden');
// erst anzeigen, dann Größe messen und am Fensterrand ausrichten
const r = menu.getBoundingClientRect();
const px = Math.min(x, window.innerWidth - r.width - 6);
const py = Math.min(y, window.innerHeight - r.height - 6);
menu.style.left = Math.max(6, px) + 'px';
menu.style.top = Math.max(6, py) + 'px';
}
function closeContextMenu() {
el('ctx-menu').classList.add('hidden');
}
// ---------- Automatische Updates ----------
let pendingUpdate = null;
function showUpdateDialog(info) {
pendingUpdate = info;
el('upd-current').textContent = 'v' + info.currentVersion;
el('upd-new').textContent = 'v' + info.latestVersion;
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';
show('modal-update');
}
async function startUpdate() {
if (!pendingUpdate || !pendingUpdate.asset) return;
const btn = el('upd-install');
btn.disabled = true;
btn.textContent = 'Wird installiert …';
el('upd-progress').classList.remove('hidden');
el('upd-status').textContent = 'Wird heruntergeladen …';
const res = await window.api.updateInstall(pendingUpdate.asset);
if (res.ok) {
el('upd-status').textContent = 'Installation wird gestartet der Launcher schließt sich.';
} else {
el('upd-status').textContent = '✖ ' + (res.message || 'Update fehlgeschlagen.');
btn.disabled = false;
btn.textContent = 'Erneut versuchen';
}
}
function onUpdateProgress(p) {
if (p.phase === 'download') {
el('upd-bar-fill').style.width = p.percent + '%';
const mb = (n) => (n / 1048576).toFixed(1);
el('upd-status').textContent = `Wird heruntergeladen … ${p.percent}% (${mb(p.loaded)} / ${mb(p.total)} MB)`;
} else if (p.phase === 'ready') {
el('upd-bar-fill').style.width = '100%';
el('upd-status').textContent = 'Download fertig Installation startet …';
}
}
// ---------- Zustand der Anmelde-Dienste in der Statusleiste ----------
function showAuthHealth(h) {
const box = el('status-auth');
@@ -376,6 +515,13 @@ function buildCard(inst) {
card.addEventListener('click', () => selectInstance(inst.id));
card.addEventListener('dblclick', () => launch(inst.id));
// Rechtsklick: Instanz auswählen und Kontextmenü öffnen
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
openContextMenu(e.clientX, e.clientY); // sofort anzeigen
selectInstance(inst.id); // Auswahl folgt
});
// Drag & Drop in Gruppen
card.draggable = true;
card.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', inst.id); card.classList.add('dragging'); });
@@ -530,14 +676,23 @@ async function updateLoaderVersions(preselect) {
}
// ---------- Instanz-Dialog ----------
// alle wählbaren Icons: echte Minecraft-Texturen zuerst, dann die eigenen
function availableIcons() {
const real = Object.values(mcIcons).map((i) => ({ key: i.key, label: i.label, real: true }));
const own = ICON_KEYS
.filter((k) => !mcIcons[k])
.map((k) => ({ key: k, label: ICON_LABELS[k] || k, real: false }));
return real.concat(own);
}
function fillIconSelect(selected) {
const sel = el('f-icon');
sel.innerHTML = '';
for (const key of ICON_KEYS) {
for (const it of availableIcons()) {
const o = document.createElement('option');
o.value = key;
o.textContent = ICON_LABELS[key] || key;
if (key === selected) o.selected = true;
o.value = it.key;
o.textContent = it.label;
if (it.key === selected) o.selected = true;
sel.appendChild(o);
}
renderIconChoices(selected);
@@ -548,15 +703,15 @@ function renderIconChoices(selected) {
const box = el('f-icon-grid');
if (!box) return;
box.innerHTML = '';
for (const key of ICON_KEYS) {
for (const it of availableIcons()) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'icon-choice' + (key === selected ? ' on' : '');
b.title = ICON_LABELS[key] || key;
b.innerHTML = pixelIconSvg(key, 26);
b.className = 'icon-choice' + (it.key === selected ? ' on' : '');
b.title = it.label;
b.innerHTML = iconOf(it.key, 24);
b.addEventListener('click', () => {
el('f-icon').value = key;
renderIconChoices(key);
el('f-icon').value = it.key;
renderIconChoices(it.key);
});
box.appendChild(b);
}
@@ -725,7 +880,8 @@ async function launch(id, accountId) {
if (res.ok) {
status('Läuft (PID ' + res.pid + ')');
toast('Minecraft gestartet.');
openConsole();
const s = await window.api.getSettings();
if (s.showConsoleOnLaunch) openConsole(); // nur wenn gewünscht
} else if (res.needAuth) {
status('Bereit'); el('status-right').textContent = '';
toast(res.message, false);
@@ -748,6 +904,12 @@ function onLaunchEvt(evt) {
status('Minecraft beendet (Code ' + evt.code + ')');
el('status-right').textContent = '';
appendConsole('\n[Prozess beendet, Code ' + evt.code + ']\n');
// Konsole je nach Einstellung schließen bzw. bei Absturz zeigen
window.api.getSettings().then((s) => {
const crashed = evt.code !== 0 && evt.code !== null;
if (crashed && s.showConsoleOnCrash !== false) { openConsole(); return; }
if (s.autoCloseConsole) hide('modal-console');
}).catch(() => {});
return;
}
const label = labels[evt.phase] || evt.phase;
@@ -774,9 +936,10 @@ function appendLineEl(out, line) {
div.textContent = line;
out.appendChild(div);
}
let consoleMaxChars = 300000; // aus den Einstellungen abgeleitet (Zeilen x ~80 Zeichen)
function appendConsole(text) {
consoleBuffer += text;
if (consoleBuffer.length > 300000) consoleBuffer = consoleBuffer.slice(-220000);
if (consoleBuffer.length > consoleMaxChars) consoleBuffer = consoleBuffer.slice(-Math.round(consoleMaxChars * 0.75));
consolePending += text;
const lines = consolePending.split('\n');
consolePending = lines.pop();
@@ -1057,6 +1220,15 @@ async function openSettings() {
}
sel.value = s.bgSkin || '';
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
el('s-sortby').value = s.sortBy || 'lastPlayed';
el('s-iso3d').checked = s.iso3dIcons !== false;
el('s-console-launch').checked = !!s.showConsoleOnLaunch;
el('s-console-autoclose').checked = !!s.autoCloseConsole;
el('s-console-crash').checked = s.showConsoleOnCrash !== false;
el('s-console-lines').value = s.consoleMaxLines || 100000;
el('s-mc-max').checked = !!s.mcMaximized;
el('s-mc-width').value = s.mcWidth || 854;
el('s-mc-height').value = s.mcHeight || 480;
el('s-bgskin-opacity').value = s.bgSkinOpacity ?? 55;
el('s-bgskin-val').textContent = String(s.bgSkinOpacity ?? 55);
el('s-proxy-enabled').checked = !!s.proxyEnabled;
@@ -1113,6 +1285,15 @@ async function saveSettings() {
globalPostExit: el('s-global-post').value.trim(),
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
sortBy: el('s-sortby').value,
iso3dIcons: el('s-iso3d').checked,
showConsoleOnLaunch: el('s-console-launch').checked,
autoCloseConsole: el('s-console-autoclose').checked,
showConsoleOnCrash: el('s-console-crash').checked,
consoleMaxLines: numOrNull(el('s-console-lines').value) || 100000,
mcMaximized: el('s-mc-max').checked,
mcWidth: numOrNull(el('s-mc-width').value) || 854,
mcHeight: numOrNull(el('s-mc-height').value) || 480,
bgSkinOpacity: numOrNull(el('s-bgskin-opacity').value) || 55,
wrapperCommand: el('s-wrapper').value.trim(),
proxyEnabled: el('s-proxy-enabled').checked,
@@ -1127,6 +1308,7 @@ async function saveSettings() {
};
await window.api.saveSettings(patch);
applyTheme(patch.theme);
iso3dEnabled = patch.iso3dIcons;
await applySkinBackground();
hide('modal-settings');
toast('Einstellungen gespeichert.');
@@ -1308,6 +1490,24 @@ function wire() {
await applySkinBackground();
});
// Kontextmenü: Einträge lösen die Aktionen der rechten Leiste aus
document.querySelectorAll('#ctx-menu .ctx-item').forEach((item) => {
item.addEventListener('click', () => {
const target = el(item.dataset.act);
closeContextMenu();
if (target) target.click();
});
});
// schließen bei Klick daneben, Rechtsklick ins Leere, Scrollen oder ESC
document.addEventListener('click', (e) => {
if (!e.target.closest('#ctx-menu')) closeContextMenu();
});
document.addEventListener('contextmenu', (e) => {
if (!e.target.closest('.card') && !e.target.closest('#ctx-menu')) closeContextMenu();
});
document.addEventListener('scroll', closeContextMenu, true);
window.addEventListener('blur', closeContextMenu);
// Warnung anklicken -> sofort erneut prüfen
el('status-auth').addEventListener('click', async () => {
el('status-auth').textContent = 'Prüfe Anmelde-Dienste …';
@@ -1432,6 +1632,23 @@ function wire() {
}));
el('s-open-accounts').addEventListener('click', () => { hide('modal-settings'); openLogin(); });
// Updates
el('upd-install').addEventListener('click', startUpdate);
el('s-check-update').addEventListener('click', async () => {
const st = el('s-update-status');
st.textContent = 'Suche nach Updates …';
const info = await window.api.updateCheck();
if (info.error) { st.textContent = '✖ ' + info.error; return; }
if (info.noReleases) { st.textContent = 'Noch keine Veröffentlichung vorhanden.'; return; }
if (info.available) {
st.textContent = `Version ${info.latestVersion} verfügbar.`;
hide('modal-settings');
showUpdateDialog(info);
} else {
st.textContent = `Du hast bereits die neueste Version (v${info.currentVersion}).`;
}
});
// Microsoft-Login / Kontoverwaltung
el('btn-account').addEventListener('click', openLogin);
el('login-add').addEventListener('click', addAccount);
@@ -1457,6 +1674,7 @@ function wire() {
m.addEventListener('mousedown', (e) => { if (e.target === m) m.classList.add('hidden'); }));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (!el('ctx-menu').classList.contains('hidden')) { closeContextMenu(); return; }
const open = [...document.querySelectorAll('.modal:not(.hidden)')].pop();
if (open) open.classList.add('hidden');
}
@@ -1466,13 +1684,21 @@ function wire() {
window.addEventListener('DOMContentLoaded', async () => {
wire();
applyIcons();
try { const s = await window.api.getSettings(); applyTheme(s.theme); } catch { applyTheme('dark'); }
try {
const s = await window.api.getSettings();
applyTheme(s.theme);
iso3dEnabled = s.iso3dIcons !== false;
consoleMaxChars = Math.max(50000, (s.consoleMaxLines || 100000) * 3);
} catch { applyTheme('dark'); }
await applySkinBackground();
window.api.onLaunchProgress(onLaunchEvt);
window.api.onAuthCode(onAuthCode);
window.api.onAuthRefreshed(async () => { await refreshAccountButton(); await applySkinBackground(); });
window.api.onAuthHealth(showAuthHealth);
window.api.onUpdateAvailable(showUpdateDialog);
window.api.onUpdateProgress(onUpdateProgress);
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
await refreshAccountButton();
await reload();