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