Upload via GUI (22 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-01 21:22:02 +02:00
parent 7a6b31fb11
commit 58b5f8d785
7 changed files with 656 additions and 233 deletions
+328 -41
View File
@@ -97,18 +97,262 @@ const PIX = {
] },
};
// baut aus dem Raster ein SVG (skaliert scharf, da reine Rechtecke)
function pixelIconSvg(key, size) {
// baut aus dem Raster ein SVG
function pixelIconRects(key, scale = 1, offsetX = 0, offsetY = 0) {
const def = PIX[key] || PIX.grass;
let rects = '';
def.r.forEach((row, y) => {
for (let x = 0; x < 16 && x < row.length; x++) {
const col = def.p[row[x]];
if (!col || col === '#00000000') continue;
rects += `<rect x="${x}" y="${y}" width="1" height="1" fill="${col}"/>`;
rects += `<rect x="${offsetX + (x * scale)}" y="${offsetY + (y * scale)}" width="${scale}" height="${scale}" fill="${col}"/>`;
}
});
return `<svg viewBox="0 0 16 16" width="${size}" height="${size}" shape-rendering="crispEdges">${rects}</svg>`;
return rects;
}
function pixelIconSvg(key, size) {
const rects = pixelIconRects(key);
return `<svg viewBox="0 0 16 16" width="${size}" height="${size}">${rects}</svg>`;
}
const HD_ICON_THEMES = {
grass: { glyph: 'loop', bg: '#46b96c', shade: '#1f5b33', glow: '#95f08a' },
dirt: { glyph: 'cube', bg: '#a96d43', shade: '#58311a', glow: '#d9a56e' },
stone: { glyph: 'cube', bg: '#98a3b3', shade: '#495364', glow: '#dfe5ef' },
planks: { glyph: 'cube', bg: '#cf9b61', shade: '#704521', glow: '#f2cb97' },
tnt: { glyph: 'crate', bg: '#db4d45', shade: '#6d1d18', glow: '#fff2d2' },
obsidian: { glyph: 'cube', bg: '#6f5cd7', shade: '#25184d', glow: '#b8abff' },
netherrack: { glyph: 'cube', bg: '#c14f4f', shade: '#5d1717', glow: '#ff9a7b' },
crafting: { glyph: 'grid', bg: '#bf8750', shade: '#613a20', glow: '#f5d5a4' },
furnace: { glyph: 'forge', bg: '#7e8997', shade: '#38414c', glow: '#ffbd67' },
bookshelf: { glyph: 'books', bg: '#b77a4d', shade: '#5b331f', glow: '#ffd966' },
diamondblock: { glyph: 'cube', bg: '#42d0d7', shade: '#14606d', glow: '#dfffff' },
goldblock: { glyph: 'cube', bg: '#f0bf3c', shade: '#8b5a09', glow: '#fff1a1' },
emeraldblock: { glyph: 'cube', bg: '#2ec97b', shade: '#0b5a35', glow: '#b4ffd7' },
redstoneblock: { glyph: 'cube', bg: '#e14d51', shade: '#75191d', glow: '#ffb1b4' },
diamond: { glyph: 'gem', bg: '#47dce0', shade: '#126b79', glow: '#defcff' },
emerald: { glyph: 'gem', bg: '#2fd485', shade: '#0d683c', glow: '#d8ffe8' },
gold: { glyph: 'ingot', bg: '#f2c44e', shade: '#8d6208', glow: '#fff3b4' },
iron: { glyph: 'ingot', bg: '#b9c3d1', shade: '#57606d', glow: '#f5f8fc' },
redstone: { glyph: 'spark', bg: '#ea5956', shade: '#7e1717', glow: '#ffc8c6' },
coal: { glyph: 'coal', bg: '#59606b', shade: '#1f242b', glow: '#cfd5df' },
sword: { glyph: 'sword', bg: '#5d9ff4', shade: '#1f477c', glow: '#eef6ff' },
pickaxe: { glyph: 'pickaxe', bg: '#57a8de', shade: '#1f5276', glow: '#ebf8ff' },
axe: { glyph: 'axe', bg: '#6997df', shade: '#27456c', glow: '#eef4ff' },
bow: { glyph: 'bow', bg: '#b97a58', shade: '#5c3522', glow: '#ffe1cb' },
apple: { glyph: 'apple', bg: '#ea5353', shade: '#7e1717', glow: '#ffd3d3' },
goldenapple: { glyph: 'apple', bg: '#f0c44f', shade: '#8c6208', glow: '#fff1b2' },
book: { glyph: 'book', bg: '#7a5ad3', shade: '#33205f', glow: '#ece4ff' },
enchantedbook: { glyph: 'bookSpark', bg: '#6f63e7', shade: '#272266', glow: '#d9fcff' },
map: { glyph: 'map', bg: '#5aa67d', shade: '#1d563f', glow: '#f0f1c9' },
star: { glyph: 'star', bg: '#5f7ce0', shade: '#22357c', glow: '#ffffff' },
endereye: { glyph: 'eye', bg: '#3da984', shade: '#124a40', glow: '#ccfff0' },
enderpearl: { glyph: 'pearl', bg: '#4cc0c7', shade: '#155b62', glow: '#e6ffff' },
blaze: { glyph: 'flame', bg: '#ef8b38', shade: '#7c3a06', glow: '#ffe3a3' },
xp: { glyph: 'bottle', bg: '#3ea977', shade: '#15503a', glow: '#dcffe0' },
cake: { glyph: 'cake', bg: '#f08cab', shade: '#8a3453', glow: '#fff6dc' },
bucket: { glyph: 'bucket', bg: '#5b94dc', shade: '#1f4a79', glow: '#d8f1ff' },
lava: { glyph: 'bucket', bg: '#ec7134', shade: '#862d08', glow: '#ffd4ab' },
tnt_item: { glyph: 'powder', bg: '#9ba2af', shade: '#404753', glow: '#fff4ce' },
creeper: { glyph: 'creeper', bg: '#67c35b', shade: '#234f1c', glow: '#d7ffd0' },
chest: { glyph: 'chest', bg: '#c4884f', shade: '#653a1d', glow: '#f5d28f' },
potion: { glyph: 'potion', bg: '#8d63d9', shade: '#3b2466', glow: '#ffd7f5' },
nether: { glyph: 'flame', bg: '#d65a3b', shade: '#6b2111', glow: '#ffd1a0' },
end: { glyph: 'eye', bg: '#3f8d7d', shade: '#153f39', glow: '#d6fff4' },
};
const LEGACY_REAL_ICON_MAP = {
chest: 'chest',
creeper: 'creeper',
potion: 'potion',
nether: 'blaze',
end: 'endereye',
tnt_item: 'tnt_item',
};
function hdIconTheme(key) {
return HD_ICON_THEMES[key] || { glyph: 'letter', bg: '#5b6474', shade: '#252b34', glow: '#e8edf7' };
}
function hdIconGlyph(key, theme) {
switch (theme.glyph) {
case 'cube':
return `
<path d="M32 18 44 24 32 30 20 24Z"/>
<path d="M20 24v14l12 7 12-7V24"/>
<path d="M32 30v15"/>
`;
case 'loop':
return `
<path d="M22 33c0-4 3-7 7-7 5 0 7 6 11 6 3 0 6-3 6-7s-3-7-7-7c-5 0-7 6-11 6-3 0-6-3-6-7s3-7 7-7"/>
`;
case 'crate':
return `
<rect x="19" y="20" width="26" height="24" rx="5"/>
<path d="M19 28h26M32 20v24M24 24h16M24 40h16"/>
`;
case 'grid':
return `
<rect x="18" y="18" width="28" height="28" rx="7"/>
<path d="M27 18v28M37 18v28M18 27h28M18 37h28"/>
`;
case 'forge':
return `
<rect x="18" y="18" width="28" height="28" rx="7"/>
<path d="M25 27h14v9H25zM24 40h16"/>
`;
case 'books':
return `
<rect x="18" y="17" width="9" height="30" rx="3"/>
<rect x="28" y="15" width="8" height="32" rx="3"/>
<rect x="37" y="19" width="9" height="28" rx="3"/>
`;
case 'gem':
return `
<path d="M32 17 45 29 32 46 19 29Z"/>
<path d="M24 22h16M20 29h24M32 17v29"/>
`;
case 'ingot':
return `
<path d="M22 26h20l5 12H17Z"/>
<path d="M26 21h12l5 5H21Z"/>
`;
case 'spark':
return `
<path d="M32 17 35 26 45 28 37 34 39 44 32 39 25 44 27 34 19 28 29 26Z"/>
`;
case 'coal':
return `
<path d="M24 19h16l7 11-7 15H24l-7-15Z"/>
<path d="M24 19 32 30 40 19"/>
`;
case 'sword':
return `
<path d="M37 18 46 27 35 38 26 29Z"/>
<path d="M28 31 18 41M24 45l-4 4M27 42l-7 2"/>
`;
case 'pickaxe':
return `
<path d="M19 26c7-7 16-9 26-6"/>
<path d="M33 20 22 45"/>
`;
case 'axe':
return `
<path d="M25 19c8 0 15 5 17 13-8 2-14-1-18-8"/>
<path d="M31 19 22 46"/>
`;
case 'bow':
return `
<path d="M24 18c9 8 9 20 0 28"/>
<path d="M39 18v28M27 22 39 32 27 42"/>
`;
case 'apple':
return `
<path d="M32 23c7 0 13 5 13 12s-6 13-13 13-13-6-13-13 6-12 13-12Z"/>
<path d="M32 19c0-3 2-5 5-6M36 18c3 0 5 1 7 3"/>
`;
case 'book':
return `
<path d="M20 19h12c3 0 5 2 5 5v21H24c-2 0-4 1-4 2Z"/>
<path d="M44 19H32c-3 0-5 2-5 5v21h13c2 0 4 1 4 2Z"/>
`;
case 'bookSpark':
return `
<path d="M20 19h12c3 0 5 2 5 5v21H24c-2 0-4 1-4 2Z"/>
<path d="M44 19H32c-3 0-5 2-5 5v21h13c2 0 4 1 4 2Z"/>
<path d="M45 17 47 22 52 24 47 26 45 31 43 26 38 24 43 22Z"/>
`;
case 'map':
return `
<path d="M18 20 28 16 38 20 46 17v27l-8 3-10-4-10 4Z"/>
<path d="M28 16v27M38 20v27"/>
`;
case 'star':
return `
<path d="M32 17 36 26 46 27 38 34 40 44 32 39 24 44 26 34 18 27 28 26Z"/>
`;
case 'eye':
return `
<path d="M16 32c5-8 10-12 16-12s11 4 16 12c-5 8-10 12-16 12s-11-4-16-12Z"/>
<circle cx="32" cy="32" r="5"/>
`;
case 'pearl':
return `
<circle cx="32" cy="32" r="13"/>
<path d="M26 27c2-2 4-3 7-3"/>
`;
case 'flame':
return `
<path d="M34 17c5 8 9 12 9 19 0 7-4 13-11 13s-11-5-11-12c0-7 5-11 10-17 0 4 1 6 4 10 2-6 1-9-1-13Z"/>
`;
case 'bottle':
return `
<path d="M28 18h8v6l4 6v12c0 5-3 8-8 8s-8-3-8-8V30l4-6Z"/>
<path d="M25 39h14"/>
`;
case 'cake':
return `
<path d="M18 30h28v14H18Z"/>
<path d="M18 30c3-4 7-6 14-6s11 2 14 6M26 24v6M32 23v7M38 24v6"/>
`;
case 'bucket':
return `
<path d="M22 22h20l-3 22H25Z"/>
<path d="M26 22c0-4 2-7 6-7s6 3 6 7"/>
`;
case 'powder':
return `
<circle cx="24" cy="34" r="4"/>
<circle cx="33" cy="28" r="5"/>
<circle cx="41" cy="36" r="3"/>
`;
case 'creeper':
return `
<rect x="21" y="18" width="22" height="28" rx="6"/>
<rect x="25" y="24" width="4" height="5" rx="1" fill="currentColor" stroke="none"/>
<rect x="35" y="24" width="4" height="5" rx="1" fill="currentColor" stroke="none"/>
<path d="M26 40h12l-2-7h-8Z" fill="currentColor" stroke="none"/>
`;
case 'chest':
return `
<rect x="18" y="22" width="28" height="22" rx="5"/>
<path d="M18 29h28M32 29v8"/>
`;
case 'potion':
return `
<path d="M28 17h8v5l4 6v13c0 6-3 9-8 9s-8-3-8-9V28l4-6Z"/>
<path d="M25 39h14"/>
`;
case 'letter':
return `<text x="32" y="40" text-anchor="middle" font-size="22" font-weight="700" font-family="Segoe UI, sans-serif" fill="currentColor" stroke="none">${escapeHtml((key || '?').slice(0, 1).toUpperCase())}</text>`;
default:
return pixelIconSvg(key, 44);
}
}
function hdIconSvg(key, size = 44) {
const theme = hdIconTheme(key);
const gid = `g-${String(key || 'icon').replace(/[^a-z0-9_-]/gi, '-')}-${size}`;
return `
<svg viewBox="0 0 64 64" width="${size}" height="${size}" aria-hidden="true">
<defs>
<clipPath id="${gid}-clip">
<rect x="8" y="8" width="48" height="48" rx="12"/>
</clipPath>
<filter id="${gid}-shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="rgba(0,0,0,.28)"/>
</filter>
</defs>
<g filter="url(#${gid}-shadow)">
<g clip-path="url(#${gid}-clip)">${pixelIconRects('grass', 3, 8, 8)}</g>
<rect x="8" y="8" width="48" height="48" rx="12" fill="rgba(18,28,16,.10)"/>
<rect x="8" y="8" width="48" height="48" rx="12" fill="none" stroke="rgba(255,255,255,.18)" stroke-width="1.5"/>
</g>
<g fill="none" stroke="${theme.glow}" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">
${hdIconGlyph(key, theme)}
</g>
</svg>`;
}
const ICON_KEYS = Object.keys(PIX);
@@ -122,15 +366,40 @@ let mcIcons = {};
async function loadMcIcons() {
try {
let list = await window.api.iconsList();
if (!list.length) {
const needRefresh = !list.length || !['potion', 'chest', 'creeper', 'paper', 'slimeball', 'cookie'].every((key) => list.some((i) => i.key === key));
if (needRefresh) {
const res = await window.api.iconsExtract();
list = (res && res.ok) ? res.icons : [];
if (res && res.ok && Array.isArray(res.icons) && res.icons.length) list = res.icons;
}
mcIcons = Object.fromEntries(list.map((i) => [i.key, i]));
await buildIsoCubes(); // räumliche Block-Symbole vorbereiten
} catch { mcIcons = {}; }
}
function resolveRealIconKey(key) {
if (mcIcons[key]) return key;
const mapped = LEGACY_REAL_ICON_MAP[key];
if (mapped && mcIcons[mapped]) return mapped;
return '';
}
function realItemIconSvg(key, size = 44) {
const realKey = resolveRealIconKey(key);
if (!realKey) return '';
const real = mcIcons[realKey];
if (!real) return '';
const theme = hdIconTheme(realKey);
const src = iso3dEnabled && BLOCK_KEYS.has(realKey) && isoCache[realKey] ? isoCache[realKey] : real.dataUri;
const plate = BLOCK_KEYS.has(realKey)
? `<rect x="8" y="8" width="48" height="48" rx="15" fill="${theme.bg}" opacity=".16"/>`
: `<circle cx="32" cy="32" r="22" fill="${theme.bg}" opacity=".14"/>`;
return `
<svg class="mc-item-icon" viewBox="0 0 64 64" width="${size}" height="${size}" aria-hidden="true">
${plate}
<image href="${src}" x="10" y="10" width="44" height="44" preserveAspectRatio="xMidYMid meet"/>
</svg>`;
}
// erzeugt die Würfel-Darstellungen einmalig im Voraus
async function buildIsoCubes() {
const jobs = Object.values(mcIcons)
@@ -157,10 +426,11 @@ function renderIsoCube(key) {
const img = isoSources[key];
if (!img) return null;
const S = 64, c = document.createElement('canvas');
const S = 96, c = document.createElement('canvas');
c.width = S; c.height = S;
const g = c.getContext('2d');
g.imageSmoothingEnabled = false;
g.imageSmoothingEnabled = true;
if ('imageSmoothingQuality' in g) g.imageSmoothingQuality = 'high';
const w = 26, h = 15, cx = S / 2, top = 8;
const face = (m, bright) => {
@@ -187,15 +457,12 @@ function renderIsoCube(key) {
return out;
}
// liefert das Icon-Markup: echte Textur bevorzugt, sonst eigene Pixel-Grafik
// liefert das Icon-Markup als skalierbares HD-SVG; eigene Bilder der Instanz bleiben separat erhalten
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 real = realItemIconSvg(key, size);
if (real) return real;
if (PIX[key]) return pixelIconSvg(key, size);
return hdIconSvg(key, size);
}
const LOADER_LABEL = {
@@ -687,7 +954,7 @@ function buildCard(inst) {
const iconHtml = inst.iconData
? `<img class="card-icon-img" src="${inst.iconData}" alt="" />`
: `<span class="card-icon">${iconOf(inst.icon)}</span>`;
: `<span class="card-icon">${iconOf(inst.icon, 60)}</span>`;
// Tooltip: Name, Version/Loader und falls vorhanden die Notizen
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
@@ -867,6 +1134,7 @@ async function updateLoaderVersions(preselect) {
// 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 }));
if (real.length) return real;
const own = ICON_KEYS
.filter((k) => !mcIcons[k])
.map((k) => ({ key: k, label: ICON_LABELS[k] || k, real: false }));
@@ -1045,7 +1313,7 @@ async function selectInstance(id) {
const iconData = inst.customImage ? await window.api.instanceIcon(id) : null;
if (iconData) el('p-icon').innerHTML = `<img src="${iconData}" alt="" />`;
else { el('p-icon').innerHTML = iconOf(inst.icon, 56); }
else { el('p-icon').innerHTML = iconOf(inst.icon, 60); }
el('p-name').textContent = inst.name;
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
@@ -1386,10 +1654,13 @@ async function installMod(mod, btn, item) {
}
// ---------- Einstellungen ----------
function setSettingsPane(paneId) {
document.querySelectorAll('.snav').forEach((button) => button.classList.toggle('on', button.dataset.pane === paneId));
document.querySelectorAll('.spane').forEach((pane) => pane.classList.toggle('hidden', pane.id !== paneId));
}
async function openSettings() {
// immer mit der ersten Sektion starten
document.querySelectorAll('.snav').forEach((x, i) => x.classList.toggle('on', i === 0));
document.querySelectorAll('.spane').forEach((p) => p.classList.toggle('hidden', p.id !== 'pane-launcher'));
setSettingsPane('pane-general');
const s = await window.api.getSettings();
el('s-theme').value = s.theme || 'dark';
@@ -1433,8 +1704,25 @@ async function openSettings() {
await refreshJavaStatus(s.javaPath);
const info = await window.api.appInfo();
el('s-appinfo').innerHTML =
`Version ${info.version}<br><span class="mono">${escapeHtml(info.userData)}</span>`;
el('s-appinfo').innerHTML = [
['Name', info.name || 'AeroMC'],
['Version', info.version || ''],
['Autor', info.author || 'Nicht hinterlegt'],
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
['Gitseite', info.gitpage || 'Nicht hinterlegt', info.gitpage || ''],
].map(([label, value, url]) => {
const renderedValue = url
? `<a href="#" class="info-meta-link" data-url="${escapeHtml(url)}">${escapeHtml(value)}</a>`
: escapeHtml(value || '');
return `<div class="settings-kv-row"><span class="settings-kv-label">${escapeHtml(label)}</span><span class="settings-kv-value">${renderedValue}</span></div>`;
}).join('');
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
link.addEventListener('click', (evt) => {
evt.preventDefault();
const url = link.dataset.url;
if (url) window.api.openExternal(url);
});
}
show('modal-settings');
await renderAccounts('s-accounts'); // Konten direkt in den Einstellungen zeigen
applyBedrockVisibility(); // aktualisiert auch den Hinweistext
@@ -1867,29 +2155,28 @@ function wire() {
});
el('s-java').addEventListener('change', (e) => refreshJavaStatus(e.target.value.trim()));
el('btn-save-settings').addEventListener('click', saveSettings);
document.querySelectorAll('.snav').forEach((b) => b.addEventListener('click', () => {
document.querySelectorAll('.snav').forEach((x) => x.classList.toggle('on', x === b));
document.querySelectorAll('.spane').forEach((p) => p.classList.toggle('hidden', p.id !== b.dataset.pane));
}));
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); });
// 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}).`;
}
});
if (el('s-check-update')) {
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);