'use strict';
/* renderer.js – UI-Logik für AeroMC */
// ---------- Instanz-Icons: eigene Pixel-Grafiken im Minecraft-Stil ----------
// Jedes Icon ist ein 16x16-Raster; jedes Zeichen verweist auf eine Farbe der Palette.
const PIX = {
grass: { p: { a: '#7cbd56', b: '#63a343', c: '#8b6b4a', d: '#6f5238' }, r: [
'aaaaaaaaaaaaaaaa', 'abaabaaabaabaaab', 'aaaaaaaaaaaaaaaa', 'babaabaabaaabaab',
'cccccccccccccccc', 'cdcccdcccccdccdc', 'ccccccccdcccccdc', 'cccdccccccccdccc',
'dcccccdcccccdccc', 'ccccdccccdccccdc', 'cdccccccccccccdc', 'ccccccdcccccdccc',
'ccdcccccccdccccc', 'cccccccdccccccdc', 'cdccccccccccdccc', 'cccccccccccccccc',
] },
creeper: { p: { a: '#7cbd56', b: '#5f9c42', k: '#0d0f0d' }, r: [
'abbaabbaabbaabba', 'baabbaabbaabbaab', 'abbaabbaabbaabba', 'aakkaaaaaaaakkaa',
'aakkaaaaaaaakkaa', 'aaaaaakkkkaaaaaa', 'aaaaakkkkkkaaaaa', 'aaaaakkkkkkaaaaa',
'aaaaakkaakkaaaaa', 'aaaaakkaakkaaaaa', 'abbaabbaabbaabba', 'baabbaabbaabbaab',
'abbaabbaabbaabba', 'baabbaabbaabbaab', 'abbaabbaabbaabba', 'baabbaabbaabbaab',
] },
diamond: { p: { a: '#5ce4e0', b: '#3ec6c9', c: '#2aa3ab', w: '#c9fbff', t: null }, r: [
'tttttttttttttttt', 'tttttttttttttttt', 'tttttaaaaaattttt', 'ttttawwwaaaatttt',
'tttawwbbbaaaattt', 'ttawwbbbbbaaaatt', 'tawwbbbbbbbaaaat', 'abbbbbbbbbbbccca',
'abbbbbbbbbbbccca', 'tabbbbbbbbbcccat', 'ttabbbbbbbcccatt', 'tttabbbbbcccattt',
'ttttabbbcccatttt', 'tttttabcccattttt', 'ttttttaccatttttt', 'tttttttaattttttt',
] },
tnt: { p: { r: '#c0392b', w: '#f2f2f2', k: '#2b2b2b', d: '#8e2a20' }, r: [
'rrrrrrrrrrrrrrrr', 'rdrrrdrrrrdrrrdr', 'wwwwwwwwwwwwwwww', 'wkkwkkwkkwkkwkkw',
'wkwkwkwkwkwkwkwk', 'wwwwwwwwwwwwwwww', 'rrrrrrrrrrrrrrrr', 'rdrrrrrdrrrrrdrr',
'rrrrdrrrrrdrrrrr', 'rrdrrrrrdrrrrrdr', 'rrrrrdrrrrrdrrrr', 'rdrrrrrrdrrrrrrr',
'rrrrdrrrrrrrdrrr', 'rrdrrrrdrrrrrrdr', 'rrrrrrrrrrrrrrrr', 'rrrrrrrrrrrrrrrr',
] },
chest: { p: { a: '#a97142', b: '#8a5a33', c: '#6b4423', k: '#3a2716', y: '#e0c060' }, r: [
'cccccccccccccccc', 'cabababababababc', 'cbababababababac', 'cabababababababc',
'ckkkkkkkkkkkkkkc', 'cabababababababc', 'cababyyyyyybabac', 'cababykkkkybabac',
'cababyyyyyybabac', 'cabababababababc', 'cabababababababc', 'cabababababababc',
'cabababababababc', 'cabababababababc', 'cabababababababc', 'cccccccccccccccc',
] },
sword: { p: { s: '#d8dde6', l: '#f4f7fb', d: '#9aa3b0', h: '#8a5a33', g: '#e0c060', t: null }, r: [
'tttttttttttttsst', 'ttttttttttttslls', 'tttttttttttslldt', 'ttttttttttslldtt',
'tttttttttslldttt', 'ttttttttslldtttt', 'tttttttslldttttt', 'ttttttslldtttttt',
'tttttslldttttttt', 'ttttslldtttttttt', 'tttgllgdtttttttt', 'tthhghhttttttttt',
'ttthhhtttttttttt', 'tttthhtttttttttt', 'ttttthtttttttttt', 'tttttttttttttttt',
] },
pickaxe: { p: { s: '#c9ced6', l: '#eef2f7', d: '#8e959f', h: '#8a5a33', b: '#6b4423', t: null }, r: [
'tttssssttssssttt', 'ttslllsddslllstt', 'tsllddsttsddllst', 'tsldttttttttdlst',
'ttdttttsstttttdt', 'tttttthhtttttttt', 'ttttttbhtttttttt', 'ttttttthbttttttt',
'tttttttbhttttttt', 'ttttttttbhtttttt', 'ttttttttthbttttt', 'ttttttttttbhtttt',
'tttttttttttbhttt', 'ttttttttttttbhtt', 'tttttttttttttbht', 'tttttttttttttttt',
] },
potion: { p: { g: '#cfd8e3', l: '#eef3fa', v: '#c0392b', d: '#8e2a20', k: '#5a4632', t: null }, r: [
'ttttttkkkktttttt', 'tttttkllkktttttt', 'tttttkllkttttttt', 'tttttkllkttttttt',
'ttttkglllgkttttt', 'tttkgllllllgkttt', 'ttkglvvvvvvlgktt', 'tkglvvvvvvvvlgkt',
'tkglvvvvvvvvlgkt', 'tkglvvvvvvvvlgkt', 'tkgdvvvvvvvvdgkt', 'tkgdvvvvvvvvdgkt',
'ttkgdvvvvvvdgktt', 'tttkgdvvvvdgkttt', 'ttttkkgggggkkttt', 'tttttkkkkkkkkttt',
] },
nether: { p: { a: '#e07b39', b: '#c9542a', c: '#8f2f18', y: '#f7c948', t: null }, r: [
'tttttttyyttttttt', 'ttttttyaaytttttt', 'tttttyaaaayttttt', 'ttttyaaaaaaytttt',
'tttyaabbaaaayttt', 'ttyaabbbbaaayttt', 'tyaabbbbbbaaaytt', 'yaabbbcccbbbaayt',
'yabbbcccccbbbayt', 'yabbcccccccbbayt', 'yabbcccccccbbayt', 'tyabcccccccbaytt',
'ttyabcccccbayttt', 'tttyabcccbaytttt', 'ttttyaabbayttttt', 'tttttyyyyytttttt',
] },
end: { p: { a: '#2f6f66', b: '#3f8f83', c: '#1f4a45', w: '#bff5ea', t: null }, r: [
'tttttbbbbbbttttt', 'tttbbaaaaaabbttt', 'ttbaaaawwaaaabtt', 'tbaaawwwwwwaaabt',
'baaawwbbbbwwaaab', 'baawwbbaabbwwaab', 'baawbbaaaabbwaab', 'bawbbaaaaaabbwab',
'bawbbaaaaaabbwab', 'baawbbaaaabbwaab', 'baawwbbaabbwwaab', 'baaawwbbbbwwaaab',
'tbaaawwwwwwaaabt', 'ttbaaaawwaaaabtt', 'tttbbaaaaaabbttt', 'tttttbbbbbbttttt',
] },
redstone: { p: { a: '#c0392b', b: '#8e2a20', c: '#5c1a13', g: '#3a3a3a', t: null }, r: [
'tttttttggttttttt', 'ttttttgaagtttttt', 'tttttgaaaagttttt', 'ttttgaabbaagtttt',
'tttgaabbbbaagttt', 'ttgaabbbbbbaagtt', 'tgaabbbccbbbaagt', 'gaabbbcccccbbaag',
'gaabbbcccccbbaag', 'tgaabbbccbbbaagt', 'ttgaabbbbbbaagtt', 'tttgaabbbbaagttt',
'ttttgaabbaagtttt', 'tttttgaaaagttttt', 'ttttttgaagtttttt', 'tttttttggttttttt',
] },
gold: { p: { a: '#f7d774', b: '#e0b53c', c: '#b8891f', t: null }, r: [
'tttttttttttttttt', 'tttttttttttttttt', 'tttaaaaaaaaaattt', 'ttaaaaaaaaaaaatt',
'taaabbbbbbbbaaat', 'taabbbbbbbbbbaat', 'taabbbbbbbbbbaat', 'tabbbbbbbbbbbbat',
'tabbbbbbbbbbbbat', 'tabbbcccccbbbbat', 'taccccccccccccat', 'taccccccccccccat',
'ttcccccccccccctt', 'tttccccccccccttt', 'tttttttttttttttt', 'tttttttttttttttt',
] },
book: { p: { a: '#8a5a33', b: '#6b4423', w: '#f0e6d2', y: '#e0c060', t: null }, r: [
'tbbbbbbbbbbbbbbt', 'bawwwwwwwwwwwwab', 'bawwwwwwwwwwwwab', 'bawwbbbwwbbbwwab',
'bawwwwwwwwwwwwab', 'bawwbbbbwbbbbwab', 'bawwwwwwwwwwwwab', 'bawwbbbwwbbbwwab',
'bawwwwwwwwwwwwab', 'bawwbbbbwbbbbwab', 'bawwwwwwwwwwwwab', 'bawwwwyywwwwwwab',
'bawwwwyywwwwwwab', 'bawwwwwwwwwwwwab', 'bawwwwwwwwwwwwab', 'tbbbbbbbbbbbbbbt',
] },
map: { p: { a: '#e8dcc0', b: '#cdbb95', c: '#8a5a33', g: '#7cbd56', u: '#4a90d9' }, r: [
'cccccccccccccccc', 'caaaaaaaaaaaaaac', 'cabbaaaaaaaaaabc', 'caaauuuaaggaaaac',
'caaauuuaagggaaac', 'caaaauaaaggaaaac', 'caaaaaaaaaaaaaac', 'caagggaaaaauuaac',
'caaggggaaaauuaac', 'caagggaaaaauuaac', 'caaaaaaaaaaaaaac', 'caaaaaabbaaaaaac',
'caaaaabbbbaaaaac', 'caaaaaabbaaaaaac', 'caaaaaaaaaaaaaac', 'cccccccccccccccc',
] },
star: { p: { a: '#ffffff', b: '#dfe6f2', c: '#9fb0cc', t: null }, r: [
'tttttttaattttttt', 'tttttttaattttttt', 'ttttttbaabtttttt', 'ttttttbaabtttttt',
'tttttbbaabbttttt', 'ttattbbaabbttatt', 'tabaabbaabbaabat', 'taaaaaaaaaaaaaat',
'taaaaaaaaaaaaaat', 'tabaabbaabbaabat', 'ttcttbbaabbttctt', 'tttttbbaabbttttt',
'ttttttbaabtttttt', 'ttttttbaabtttttt', 'tttttttaattttttt', 'tttttttaattttttt',
] },
};
// 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 += ` `;
}
});
return rects;
}
function pixelIconSvg(key, size) {
const rects = pixelIconRects(key);
return `${rects} `;
}
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 `
`;
case 'loop':
return `
`;
case 'crate':
return `
`;
case 'grid':
return `
`;
case 'forge':
return `
`;
case 'books':
return `
`;
case 'gem':
return `
`;
case 'ingot':
return `
`;
case 'spark':
return `
`;
case 'coal':
return `
`;
case 'sword':
return `
`;
case 'pickaxe':
return `
`;
case 'axe':
return `
`;
case 'bow':
return `
`;
case 'apple':
return `
`;
case 'book':
return `
`;
case 'bookSpark':
return `
`;
case 'map':
return `
`;
case 'star':
return `
`;
case 'eye':
return `
`;
case 'pearl':
return `
`;
case 'flame':
return `
`;
case 'bottle':
return `
`;
case 'cake':
return `
`;
case 'bucket':
return `
`;
case 'powder':
return `
`;
case 'creeper':
return `
`;
case 'chest':
return `
`;
case 'potion':
return `
`;
case 'letter':
return `${escapeHtml((key || '?').slice(0, 1).toUpperCase())} `;
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 `
${pixelIconRects('grass', 3, 8, 8)}
${hdIconGlyph(key, theme)}
`;
}
const ICON_KEYS = Object.keys(PIX);
const ICON_LABELS = {
grass: 'Grasblock', creeper: 'Creeper', diamond: 'Diamant', tnt: 'TNT', chest: 'Truhe',
sword: 'Schwert', pickaxe: 'Spitzhacke', potion: 'Trank', nether: 'Feuer', end: 'Endauge',
redstone: 'Redstone', gold: 'Goldbarren', book: 'Buch', map: 'Karte', star: 'Netherstern',
};
// echte Minecraft-Icons (aus den Spieldateien extrahiert), key -> dataUri
let mcIcons = {};
async function loadMcIcons() {
try {
let list = await window.api.iconsList();
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();
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)
? ` `
: ` `;
return `
${plate}
`;
}
// 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 = 96, c = document.createElement('canvas');
c.width = S; c.height = S;
const g = c.getContext('2d');
g.imageSmoothingEnabled = true;
if ('imageSmoothingQuality' in g) g.imageSmoothingQuality = 'high';
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 als skalierbares HD-SVG; eigene Bilder der Instanz bleiben separat erhalten
function iconOf(key, size = 44) {
const real = realItemIconSvg(key, size);
if (real) return real;
if (PIX[key]) return pixelIconSvg(key, size);
return hdIconSvg(key, size);
}
const LOADER_LABEL = {
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
neoforge: 'NeoForge', quilt: 'Quilt', optifine: 'OptiFine',
};
// Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren
// bei größeren Modpacks spürbar GC-bedingte Ruckler. Wirkt erst ab genug RAM
// sinnvoll, schadet aber auch bei wenig RAM nicht.
const AIKAR_FLAGS = '-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 '
+ '-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch '
+ '-XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M '
+ '-XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 '
+ '-XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 '
+ '-XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem '
+ '-XX:MaxTenuringThreshold=1';
// ---------- Zustand ----------
let instances = [];
let currentDetailId = null;
let selectedInstanceIds = new Set();
let editingId = null;
let confirmAction = null;
let mcVersionData = null; // gecachte Mojang-Versionsliste
let currentModsInstalled = new Set(); // projectIds im offenen Mod-Dialog
let pendingImagePath = null; // im Dialog gewähltes eigenes Bild (noch nicht gespeichert)
let pendingClearImage = false; // eigenes Bild soll entfernt werden
let loginVerificationUri = null; // aktueller Device-Code-Link
const collapsedGroups = new Set(); // eingeklappte Gruppen
let consoleBuffer = ''; // gesammelte Log-Ausgabe (roh, für Kopieren)
let consolePending = ''; // noch nicht abgeschlossene Zeile
// ---------- DOM-Helfer ----------
const el = (id) => document.getElementById(id);
function show(id) { el(id).classList.remove('hidden'); }
function hide(id) { el(id).classList.add('hidden'); }
function toast(msg, isError = false) {
const t = el('toast');
t.textContent = msg;
t.classList.toggle('err', isError);
t.classList.remove('hidden');
clearTimeout(toast._t);
toast._t = setTimeout(() => t.classList.add('hidden'), 2800);
}
function fmtDate(iso) {
if (!iso) return '–';
try { return new Date(iso).toLocaleString('de-DE'); } catch { return iso; }
}
function status(text) { el('status-left').textContent = text; }
function fmtDuration(s) {
s = s || 0;
if (s < 60) return s + ' Sek.';
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
return h ? `${h} Std. ${m} Min.` : `${m} Min.`;
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => (
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
));
}
function numOrNull(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : null; }
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark');
}
// ---------- Instanzen aus MultiMC übernehmen ----------
let mmcFound = [];
let cfFound = [];
let mcFound = [];
let technicFound = [];
let importKind = 'zip';
async function ensureDefaultMcScan() {
if (importKind !== 'mc') return;
if (el('mc-path').value.trim()) return;
if (mcFound.length) return;
await scanMc();
}
function setImportKind(kind) {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].forEach((entryKind) => {
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
});
el('import-zip-do').classList.toggle('hidden', importKind !== 'zip');
el('import-modpack-do').classList.toggle('hidden', importKind !== 'modpack');
el('mc-do').classList.toggle('hidden', importKind !== 'mc');
el('cf-do').classList.toggle('hidden', importKind !== 'cf');
el('mmc-do').classList.toggle('hidden', importKind !== 'mmc');
el('technic-do').classList.toggle('hidden', importKind !== 'technic');
el('mc-progress').classList.toggle('hidden', importKind !== 'mc' || !el('mc-progress').textContent);
el('cf-progress').classList.toggle('hidden', importKind !== 'cf' || !el('cf-progress').textContent);
el('mmc-progress').classList.toggle('hidden', importKind !== 'mmc' || !el('mmc-progress').textContent);
el('technic-progress').classList.toggle('hidden', importKind !== 'technic' || !el('technic-progress').textContent);
}
function openImportDialog(kind = 'zip') {
mcFound = [];
el('mc-result').classList.add('hidden');
el('mc-do').disabled = true;
el('mc-progress').textContent = '';
el('mc-progress').classList.add('hidden');
el('mc-status').textContent = 'Leer lassen nutzt automatisch den Standardpfad unter Windows.';
el('cf-result').classList.add('hidden');
el('cf-do').disabled = true;
el('cf-progress').textContent = '';
el('cf-progress').classList.add('hidden');
el('cf-status').textContent = 'Standardpfad unter Windows: C:\\Users\\DeinName\\curseforge\\minecraft\\Instances';
el('mmc-result').classList.add('hidden');
el('mmc-do').disabled = true;
el('mmc-progress').textContent = '';
el('mmc-progress').classList.add('hidden');
el('mmc-status').textContent = 'Gib den Ordner an, in dem MultiMC.exe oder PrismLauncher.exe liegt.';
setImportKind(kind);
show('modal-import');
if (importKind === 'mc') {
el('mc-path').focus();
ensureDefaultMcScan();
}
if (importKind === 'cf') el('cf-path').focus();
if (importKind === 'mmc') el('mmc-path').focus();
}
async function runZipImport() {
el('import-zip-do').disabled = true;
try {
const res = await window.api.importInstance();
if (res.canceled) return;
if (res.ok) {
await reload();
hide('modal-import');
toast('Importiert: ' + res.instance.name);
return;
}
toast(res.message || 'Import fehlgeschlagen.', true);
} finally {
el('import-zip-do').disabled = false;
}
}
async function runModpackImport() {
el('import-modpack-do').disabled = true;
status('Importiere Modpack …');
try {
const res = await window.api.importModpack();
status('Bereit'); el('status-right').textContent = '';
if (res.canceled) return;
if (res.ok) {
await reload();
hide('modal-import');
toast(`Modpack importiert: ${res.instance.name} (${res.fileCount} Dateien)`);
// CurseForge gibt nicht jede Datei heraus – ehrlich benennen statt verschweigen
if (res.fehlend) toast(`${res.fehlend} Datei(en) konnten nicht geladen werden und fehlen in der Instanz.`, true);
return;
}
toast(res.message || 'Modpack-Import fehlgeschlagen.', true);
if (res.needsKey) { hide('modal-import'); openSettings('pane-network'); }
} finally {
status('Bereit'); el('status-right').textContent = '';
el('import-modpack-do').disabled = false;
}
}
function fmtDownloads(n) {
n = Number(n) || 0;
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k';
return String(n);
}
async function searchModpacks() {
const query = el('mp-query').value.trim();
const gameVersion = el('mp-mc').value.trim();
const sort = el('mp-sort').value || 'relevance';
const source = (el('mp-source') && el('mp-source').value) || 'modrinth';
const box = el('mp-results');
const st = el('mp-status');
st.textContent = 'Suche …';
box.innerHTML = '
Suche läuft …
';
try {
const res = await window.api.modpackSearch({
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack', source,
});
if (res.error) {
st.textContent = 'Fehler: ' + res.error;
box.innerHTML = 'Suche fehlgeschlagen.
';
if (res.needKey) {
const wrap = document.createElement('div');
wrap.className = 'mods-empty';
const btn = document.createElement('button');
btn.className = 'btn btn-mini';
btn.textContent = 'Zu den Netzwerk-Einstellungen';
btn.addEventListener('click', () => { hide('modal-import'); openSettings('pane-network'); });
wrap.appendChild(btn);
box.appendChild(wrap);
}
return;
}
const hits = res.hits || [];
st.textContent = hits.length
? (res.total + ' Treffer' + (gameVersion ? ' für ' + gameVersion : ''))
: 'Keine Treffer.';
box.innerHTML = '';
if (!hits.length) {
box.innerHTML = 'Keine Modpacks gefunden.
';
return;
}
for (const h of hits) {
const item = document.createElement('div');
item.className = 'mod-item';
const meta = [
h.author ? ('von ' + h.author) : '',
h.downloads != null ? (fmtDownloads(h.downloads) + ' Downloads') : '',
].filter(Boolean).join(' · ');
const cats = (h.displayCategories || h.categories || []).slice(0, 4).join(', ');
item.innerHTML =
`${h.iconUrl ? `
` : '📦'}
${escapeHtml(h.title || h.slug || h.projectId)}
${escapeHtml(meta)}${cats ? ' · ' + escapeHtml(cats) : ''}
${escapeHtml((h.description || '').slice(0, 120))}${(h.description || '').length > 120 ? '…' : ''}
`;
const btn = document.createElement('button');
btn.className = 'btn btn-primary btn-mini';
btn.textContent = 'Installieren';
btn.addEventListener('click', () => installModpackFromSearch(h, btn));
item.querySelector('.mod-action').appendChild(btn);
box.appendChild(item);
}
} catch (err) {
st.textContent = 'Fehler: ' + (err.message || err);
box.innerHTML = 'Suche fehlgeschlagen.
';
}
}
async function installModpackFromSearch(hit, btn) {
if (!hit || !hit.projectId) return;
const gameVersion = el('mp-mc').value.trim();
const prev = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = '…'; }
status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …');
try {
const res = hit.source === 'curseforge'
? await window.api.modpackInstallFromCurseForge({
cfId: hit.cfId, projectId: hit.projectId, title: hit.title || hit.slug, gameVersion,
})
: await window.api.modpackInstallFromModrinth({
projectId: hit.projectId,
title: hit.title || hit.slug,
gameVersion,
});
status('Bereit'); el('status-right').textContent = '';
if (res.ok) {
await reload();
hide('modal-import');
toast(`Modpack installiert: ${res.instance.name} (${res.fileCount || 0} Dateien)`);
if (currentDetailId !== res.instance.id) {
try { await selectInstance(res.instance.id); } catch { /* egal */ }
}
return;
}
toast(res.message || 'Installation fehlgeschlagen.', true);
} catch (err) {
status('Bereit'); el('status-right').textContent = '';
toast('Installation fehlgeschlagen: ' + (err.message || err), true);
} finally {
if (btn) { btn.disabled = false; btn.textContent = prev || 'Installieren'; }
}
}
async function scanMc() {
const folder = el('mc-path').value.trim();
el('mc-status').textContent = 'Suche Launcher-Profile …';
const res = await window.api.mcScan(folder);
if (!res.ok) {
el('mc-status').textContent = '✖ Dort wurde kein launcher_profiles.json gefunden.';
el('mc-result').classList.add('hidden');
el('mc-do').disabled = true;
return;
}
mcFound = res.instances;
if (!folder && res.rootDir) el('mc-path').value = res.rootDir;
el('mc-status').textContent = `${res.count} Profil${res.count === 1 ? '' : 'e'} gefunden.`;
el('mc-result').classList.remove('hidden');
el('mc-all').checked = true;
renderMcList();
}
function renderMcList() {
const box = el('mc-list');
box.innerHTML = '';
if (!mcFound.length) { box.innerHTML = 'Keine Profile gefunden.
'; return; }
mcFound.forEach((inst, idx) => {
const row = document.createElement('label');
row.className = 'mmc-item';
const loaderTxt = inst.loader === 'vanilla'
? 'Vanilla'
: (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : '');
const meta = [inst.version || 'ohne Version', loaderTxt, inst.usesDefaultGameDir ? 'Standard-Spielordner' : 'eigener Spielordner']
.filter(Boolean).join(' · ');
row.innerHTML =
`
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`;
box.appendChild(row);
});
updateMcButton();
}
function selectedMc() {
return [...document.querySelectorAll('#mc-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => mcFound[Number(c.dataset.idx)]);
}
function updateMcButton() {
const n = selectedMc().length;
el('mc-do').disabled = n === 0;
el('mc-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runMcImport() {
const entries = selectedMc();
if (!entries.length) return;
const copy = el('mc-copy').checked;
el('mc-do').disabled = true;
el('mc-progress').textContent = 'Übernehme …';
el('mc-progress').classList.remove('hidden');
const res = await window.api.mcImport(entries, copy);
await reload();
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} Profil${n === 1 ? '' : 'e'} aus dem Minecraft Launcher übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
async function scanCf() {
const folder = el('cf-path').value.trim();
if (!folder) { el('cf-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; }
el('cf-status').textContent = 'Suche Instanzen …';
const res = await window.api.cfScan(folder);
if (!res.ok) {
el('cf-status').textContent = '✖ Dort wurde kein CurseForge-Instances-Ordner gefunden.';
el('cf-result').classList.add('hidden');
el('cf-do').disabled = true;
return;
}
cfFound = res.instances;
el('cf-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden.`;
el('cf-result').classList.remove('hidden');
el('cf-all').checked = true;
renderCfList();
}
function renderCfList() {
const box = el('cf-list');
box.innerHTML = '';
if (!cfFound.length) { box.innerHTML = 'Keine Instanzen gefunden.
'; return; }
cfFound.forEach((inst, idx) => {
const row = document.createElement('label');
row.className = 'mmc-item';
const loaderTxt = inst.loader === 'vanilla'
? 'Vanilla'
: (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : '');
const meta = [inst.version || 'ohne Version', loaderTxt]
.filter(Boolean).join(' · ');
row.innerHTML =
`
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`;
box.appendChild(row);
});
updateCfButton();
}
function selectedCf() {
return [...document.querySelectorAll('#cf-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => cfFound[Number(c.dataset.idx)]);
}
function updateCfButton() {
const n = selectedCf().length;
el('cf-do').disabled = n === 0;
el('cf-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runCfImport() {
const entries = selectedCf();
if (!entries.length) return;
const copy = el('cf-copy').checked;
el('cf-do').disabled = true;
el('cf-progress').textContent = 'Übernehme …';
el('cf-progress').classList.remove('hidden');
const res = await window.api.cfImport(entries, copy);
await reload();
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} CurseForge-Instanz${n === 1 ? '' : 'en'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
async function scanMmc() {
const folder = el('mmc-path').value.trim();
if (!folder) { el('mmc-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; }
el('mmc-status').textContent = 'Suche Instanzen …';
const res = await window.api.mmcScan(folder);
if (!res.ok) {
el('mmc-status').textContent = '✖ Dort wurde kein instances-Ordner gefunden.';
el('mmc-result').classList.add('hidden');
el('mmc-do').disabled = true;
return;
}
mmcFound = res.instances;
el('mmc-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden.`;
el('mmc-result').classList.remove('hidden');
el('mmc-all').checked = true;
renderMmcList();
}
function renderMmcList() {
const box = el('mmc-list');
box.innerHTML = '';
if (!mmcFound.length) { box.innerHTML = 'Keine Instanzen gefunden.
'; return; }
mmcFound.forEach((inst, idx) => {
const row = document.createElement('label');
row.className = 'mmc-item';
const loaderTxt = inst.loader === 'vanilla'
? 'Vanilla'
: (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : '');
const meta = [inst.version || 'ohne Version', loaderTxt, inst.group ? 'Gruppe: ' + inst.group : '']
.filter(Boolean).join(' · ');
row.innerHTML =
`
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`;
box.appendChild(row);
});
updateMmcButton();
}
function selectedMmc() {
return [...document.querySelectorAll('#mmc-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => mmcFound[Number(c.dataset.idx)]);
}
function updateMmcButton() {
const n = selectedMmc().length;
el('mmc-do').disabled = n === 0;
el('mmc-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runMmcImport() {
const entries = selectedMmc();
if (!entries.length) return;
const copy = el('mmc-copy').checked;
el('mmc-do').disabled = true;
el('mmc-progress').textContent = 'Übernehme …';
el('mmc-progress').classList.remove('hidden');
const res = await window.api.mmcImport(entries, copy);
await reload();
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} Instanz${n === 1 ? '' : 'en'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
// ---------- Technic-Plattform: Modpack-Katalog ----------
async function searchTechnicPlatform() {
const query = el('tp-query').value.trim();
const box = el('tp-results');
const st = el('tp-status');
st.textContent = 'Suche …';
box.innerHTML = 'Suche läuft …
';
const res = await window.api.technicPlatformSearch(query);
if (!res.ok) {
st.textContent = 'Fehler: ' + (res.message || 'Suche fehlgeschlagen.');
box.innerHTML = 'Suche fehlgeschlagen.
';
return;
}
const hits = res.hits || [];
st.textContent = hits.length ? `${hits.length} Treffer` : 'Keine Treffer.';
box.innerHTML = '';
if (!hits.length) { box.innerHTML = 'Keine Modpacks gefunden.
'; return; }
for (const h of hits) {
const item = document.createElement('div');
item.className = 'mod-item';
item.innerHTML =
`${h.iconUrl ? `
` : '📦'}
`;
const btn = document.createElement('button');
btn.className = 'btn btn-primary btn-mini';
btn.textContent = 'Installieren';
btn.addEventListener('click', () => installTechnicFromSearch(h, btn));
item.querySelector('.mod-action').appendChild(btn);
box.appendChild(item);
}
}
async function installTechnicFromSearch(hit, btn) {
if (!hit || !hit.slug) return;
const prev = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = '…'; }
const st = el('tp-status');
st.textContent = `Installiere „${hit.name}" …`;
try {
const res = await window.api.technicPlatformInstall(hit.slug);
if (res.ok) {
await reload();
hide('modal-import');
toast(`Modpack installiert: ${res.instance.name}`);
await selectInstance(res.instance.id);
} else {
st.textContent = '✖ ' + (res.message || 'Installation fehlgeschlagen.');
toast(res.message || 'Installation fehlgeschlagen.', true);
}
} finally {
if (btn) { btn.disabled = false; btn.textContent = prev; }
}
}
async function scanTechnic() {
const folder = el('technic-path').value.trim();
el('technic-status').textContent = 'Suche Modpacks …';
const res = await window.api.technicScan(folder);
if (!res.ok) {
el('technic-status').textContent = '✖ Dort wurde kein modpacks-Ordner gefunden.';
el('technic-result').classList.add('hidden');
el('technic-do').disabled = true;
return;
}
technicFound = res.instances;
if (!folder && res.instancesDir) el('technic-path').value = res.instancesDir;
el('technic-status').textContent = `${res.count} Modpack${res.count === 1 ? '' : 's'} gefunden.`;
el('technic-result').classList.remove('hidden');
el('technic-all').checked = true;
renderTechnicList();
}
function renderTechnicList() {
const box = el('technic-list');
box.innerHTML = '';
if (!technicFound.length) { box.innerHTML = 'Keine Modpacks gefunden.
'; return; }
technicFound.forEach((inst, idx) => {
const row = document.createElement('label');
row.className = 'mmc-item';
const loaderTxt = inst.loader === 'vanilla'
? 'Vanilla'
: (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : '');
const meta = [inst.version || 'Version unbekannt', loaderTxt]
.filter(Boolean).join(' · ');
row.innerHTML =
`
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`;
box.appendChild(row);
});
updateTechnicButton();
}
function selectedTechnic() {
return [...document.querySelectorAll('#technic-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => technicFound[Number(c.dataset.idx)]);
}
function updateTechnicButton() {
const n = selectedTechnic().length;
el('technic-do').disabled = n === 0;
el('technic-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runTechnicImport() {
const entries = selectedTechnic();
if (!entries.length) return;
const copy = el('technic-copy').checked;
el('technic-do').disabled = true;
el('technic-progress').textContent = 'Übernehme …';
el('technic-progress').classList.remove('hidden');
const res = await window.api.technicImport(entries, copy);
await reload();
hide('modal-import');
const n = res.imported.length;
const f = res.failed.length;
toast(`${n} Technic-Modpack${n === 1 ? '' : 's'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0);
if (instances.length) await selectInstance(instances[0].id);
}
// ---------- Bedrock (Windows-Store-Ausgabe) ----------
let bedrockKind = 'worlds';
// Bedrock-Knopf nur zeigen, wenn aktiviert UND tatsächlich installiert
async function applyBedrockVisibility() {
const btn = el('btn-bedrock');
let show = false;
try {
const s = await window.api.getSettings();
if (s.bedrockEnabled !== false) {
const info = await window.api.bedrockDetect();
show = !!info.installed;
const hint = el('s-bedrock-hint');
if (hint) {
hint.textContent = info.installed
? 'Bedrock erkannt (Version ' + info.version + ').'
: 'Bedrock ist auf diesem PC nicht installiert – der Bereich bleibt ausgeblendet.';
}
}
} catch { /* im Zweifel ausblenden */ }
btn.classList.toggle('hidden', !show);
}
async function openBedrock() {
show('modal-bedrock');
el('bd-sub').textContent = 'Wird geprüft …';
const info = await window.api.bedrockDetect();
if (!info.installed) {
el('bd-sub').textContent = 'nicht installiert';
el('bd-missing').classList.remove('hidden');
el('bd-content').classList.add('hidden');
return;
}
el('bd-missing').classList.add('hidden');
el('bd-content').classList.remove('hidden');
el('bd-sub').textContent = 'Version ' + info.version;
switchBedrockTab('worlds');
}
function switchBedrockTab(kind) {
bedrockKind = kind;
document.querySelectorAll('.btab').forEach((t) => t.classList.toggle('on', t.dataset.kind === kind));
renderBedrockList();
}
function fmtBytes(n) {
if (!n) return '';
if (n < 1024) return n + ' B';
if (n < 1048576) return (n / 1024).toFixed(0) + ' KB';
return (n / 1048576).toFixed(1) + ' MB';
}
async function renderBedrockList() {
const box = el('bd-list');
box.innerHTML = 'Lade …
';
const kind = bedrockKind;
const items = kind === 'worlds' ? await window.api.bedrockWorlds() : await window.api.bedrockList(kind);
box.innerHTML = '';
if (!items.length) {
const leer = {
worlds: 'Noch keine Welten vorhanden.',
resource_packs: 'Keine Ressourcenpakete installiert.',
behavior_packs: 'Keine Verhaltenspakete installiert.',
skin_packs: 'Keine Skin-Pakete installiert.',
screenshots: 'Noch keine Bildschirmfotos.',
};
box.innerHTML = `${leer[kind] || 'Nichts vorhanden.'}
`;
return;
}
for (const it of items) {
const name = it.name || it.id;
const item = document.createElement('div');
item.className = 'mod-item';
const meta = [fmtBytes(it.bytes), it.modified ? fmtDate(it.modified) : ''].filter(Boolean).join(' · ');
item.innerHTML =
`${kind === 'worlds' ? '🌍' : kind === 'screenshots' ? '🖼' : '📦'}
${escapeHtml(name)}
${escapeHtml(meta)}
`;
const actions = item.querySelector('.mod-action');
if (kind === 'worlds') {
const open = document.createElement('button');
open.className = 'btn btn-mini';
open.textContent = 'Ordner';
open.addEventListener('click', () => window.api.bedrockOpenWorld(it.dir));
actions.appendChild(open);
}
const del = document.createElement('button');
del.className = 'btn btn-danger btn-mini';
del.textContent = 'Löschen';
del.addEventListener('click', () => askConfirm(
'Löschen', `„${name}" wird unwiderruflich gelöscht. Fortfahren?`, 'Endgültig löschen',
async () => {
await window.api.bedrockRemove(kind, kind === 'worlds' ? it.id : it.name);
await renderBedrockList();
toast('Gelöscht: ' + name);
}));
actions.appendChild(del);
box.appendChild(item);
}
}
// ---------- 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');
}
function formatVersionLabel(version) {
return String(version || '')
.replace(/^v/i, '')
.replace(/-beta$/i, ' Beta')
.trim() || '–';
}
// ---------- Automatische Updates ----------
let pendingUpdate = null;
const CHANNEL_LABEL = { portable: 'Portable', setup: 'Setup' };
function showUpdateDialog(info) {
pendingUpdate = info;
const channel = CHANNEL_LABEL[info.installKind] || null;
el('upd-current').textContent = formatVersionLabel(info.currentVersion);
el('upd-new').textContent = formatVersionLabel(info.latestVersion);
el('upd-channel').textContent = channel
? `Update-Kanal: ${channel}`
: 'Update-Kanal: unbekannt – automatisches Update nicht möglich';
el('upd-notes').textContent = (info.notes || '').trim() || 'Keine Änderungshinweise hinterlegt.';
el('upd-progress').classList.add('hidden');
el('upd-bar-fill').style.width = '0%';
// Ohne passendes Paket wird bewusst nichts installiert – ein Portable-Launcher
// darf nie einen Installer bekommen und umgekehrt. Statt eines toten Knopfes
// führen wir dann zum Release, damit der Nutzer nicht in der Sackgasse steht.
const hatPaket = !!info.asset;
el('upd-install').classList.toggle('hidden', !hatPaket);
el('upd-install').disabled = !hatPaket;
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
? `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');
}
function openReleasePage() {
if (pendingUpdate && pendingUpdate.url) window.api.openExternal(pendingUpdate.url);
}
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 = res.kind === 'portable'
? 'Launcher schließt sich, wird ersetzt und startet automatisch neu.'
: '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 …';
}
}
// Aktualisiert die Statuszeile im Info-Bereich – genutzt sowohl vom manuellen
// "Nach Updates suchen"-Knopf als auch vom automatischen Check beim Start.
function renderUpdateStatus(info) {
const st = el('s-update-status');
if (!st) return;
st.classList.remove('status-ok', 'status-bad');
if (info.checking) {
st.textContent = 'Suche nach Updates …';
} else if (info.error) {
st.classList.add('status-bad');
st.textContent = '✖ ' + info.error;
} else if (info.noReleases) {
st.textContent = 'Noch keine Veröffentlichung vorhanden.';
} else if (info.available) {
st.textContent = `Version ${formatVersionLabel(info.latestVersion)} verfügbar.`;
} else {
st.classList.add('status-ok');
st.textContent = `✓ Du hast bereits die neueste Version (${formatVersionLabel(info.currentVersion)}).`;
}
}
// ---------- Zustand der Anmelde-Dienste in der Statusleiste ----------
function showAuthHealth(h) {
const box = el('status-auth');
if (!h || h.ok) { box.classList.add('hidden'); box.textContent = ''; return; }
const list = (h.down || []).join(', ');
box.textContent = (h.down && h.down.length === AUTH_SERVICE_COUNT)
? 'Anmelde-Server nicht erreichbar – Login/Start derzeit nicht möglich'
: 'Anmelde-Dienst gestört: ' + list;
box.title = 'Zuletzt geprüft: ' + (h.checked ? new Date(h.checked).toLocaleTimeString('de-DE') : '–') + ' · Klicken zum erneut prüfen';
box.classList.remove('hidden');
}
const AUTH_SERVICE_COUNT = 3;
// ---------- Skin-Hintergrund (wie MultiMCs Katze, nur mit eigenem Skin) ----------
// uuidOverride: wird vom Konten-Wechsler (siehe unten) genutzt, um gezielt
// einen bestimmten Skin zu zeigen, ohne die eigentliche Einstellung anzufassen
async function applySkinBackground(uuidOverride) {
const s = await window.api.getSettings();
const root = document.documentElement;
let uuid = '';
if (uuidOverride) {
uuid = uuidOverride;
} else if (s.bgSkin === 'active') {
const acc = await window.api.authStatus();
uuid = acc && acc.profile ? acc.profile.id : '';
} else if (s.bgSkin) {
uuid = s.bgSkin;
}
if (!uuid) { root.style.setProperty('--bg-skin', 'none'); return; }
root.style.setProperty('--bg-skin-opacity', String((s.bgSkinOpacity ?? 55) / 100));
// Jede Pose hat auch eine Entsprechung beim 3D-Dienst (Kamerawinkel/Ansicht),
// damit die Auswahl immer sichtbar wirkt – auch wenn der Pose-Dienst offline ist.
// 1) Skin lokal in 3D rendern – unabhängig von Render-Diensten
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking');
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
// 2) Rückfall: externe Render-Dienste
for (const url of [
`https://nmsr.nickac.dev/fullbody/${uuid}?yaw=25&pitch=8`,
`https://visage.surgeplay.com/full/512/${uuid}`,
`https://crafthead.net/armor/body/${uuid}?size=512`,
]) {
const ok = await new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = url;
});
if (ok) { root.style.setProperty('--bg-skin', `url("${url}")`); return; }
}
root.style.setProperty('--bg-skin', 'none');
}
// ---------- Skin-Hintergrund: automatischer Kontenwechsel ----------
let skinCycleTimer = null;
let skinCycleIndex = 0;
function stopSkinCycle() {
if (skinCycleTimer) { clearInterval(skinCycleTimer); skinCycleTimer = null; }
}
async function skinCycleTick() {
const accs = await window.api.authAccounts();
if (accs.length < 2) { stopSkinCycle(); await applySkinBackground(); return; }
skinCycleIndex = (skinCycleIndex + 1) % accs.length; // am Ende wieder von vorn
await applySkinBackground(accs[skinCycleIndex].id);
}
/*
* Einstiegspunkt für alles, was den Skin-Hintergrund neu anzeigen soll.
* Ersetzt die frühere direkte applySkinBackground()-Aufrufe: kümmert sich
* zusätzlich darum, den Kontenwechsel-Timer je nach Einstellung zu
* starten/stoppen, statt dass jede Aufrufstelle das selbst tun müsste.
*/
async function refreshSkinBackground() {
stopSkinCycle();
const s = await window.api.getSettings();
if (s.bgSkinCycle) {
const accs = await window.api.authAccounts();
if (accs.length >= 2) {
skinCycleIndex = 0;
await applySkinBackground(accs[0].id);
const seconds = Math.max(5, Number(s.bgSkinCycleSeconds) || 30);
skinCycleTimer = setInterval(skinCycleTick, seconds * 1000);
return;
}
// weniger als 2 Konten angemeldet -> normal weiter wie ohne Wechsel
}
await applySkinBackground();
}
// Pose-Vorlagen für den lokalen 3D-Renderer (eigene Nachbildungen bekannter Motive)
const SKIN_POSES = {
walking: { label: 'Laufend', fov: 36, zoom: 0.62, apply: (p) => {
p.rotation.set(0.05, -0.62, 0);
p.skin.leftArm.rotation.set(-0.95, 0, 0.06);
p.skin.rightArm.rotation.set(0.85, 0, -0.06);
p.skin.leftLeg.rotation.set(0.8, 0, 0);
p.skin.rightLeg.rotation.set(-0.8, 0, 0);
} },
standing: { label: 'Stehend', fov: 36, zoom: 0.6, apply: (p) => {
p.rotation.set(0, -0.35, 0);
p.skin.leftArm.rotation.set(0, 0, 0.08);
p.skin.rightArm.rotation.set(0, 0, -0.08);
p.skin.leftLeg.rotation.set(0, 0, 0.02);
p.skin.rightLeg.rotation.set(0, 0, -0.02);
} },
legends: { label: 'Heldenhaft', fov: 34, zoom: 0.58, apply: (p) => {
p.rotation.set(-0.10, -0.5, 0);
p.skin.leftArm.rotation.set(-0.25, 0, 0.35);
p.skin.rightArm.rotation.set(-0.15, 0, -0.42);
p.skin.leftLeg.rotation.set(0.12, 0, 0.16);
p.skin.rightLeg.rotation.set(-0.12, 0, -0.16);
} },
bedwars: { label: 'Kampfhaltung', fov: 36, zoom: 0.6, apply: (p) => {
p.rotation.set(0.06, -0.85, 0);
p.skin.rightArm.rotation.set(-1.45, 0, -0.18); // Arm vorgestreckt
p.skin.leftArm.rotation.set(0.45, 0, 0.30);
p.skin.leftLeg.rotation.set(0.45, 0, 0.10);
p.skin.rightLeg.rotation.set(-0.35, 0, -0.10);
} },
hoverboard: { label: 'Schwebend', fov: 38, zoom: 0.6, apply: (p) => {
p.rotation.set(0.28, -0.7, 0.12); // vorgebeugt, gekippt
p.skin.leftArm.rotation.set(-0.35, 0, 1.05); // Arme zum Ausbalancieren
p.skin.rightArm.rotation.set(-0.35, 0, -1.05);
p.skin.leftLeg.rotation.set(0.5, 0, 0.18);
p.skin.rightLeg.rotation.set(0.15, 0, -0.18);
} },
sitting: { label: 'Sitzend', fov: 36, zoom: 0.62, apply: (p) => {
p.rotation.set(0.05, -0.55, 0);
p.skin.leftArm.rotation.set(-0.55, 0, 0.18);
p.skin.rightArm.rotation.set(-0.55, 0, -0.18);
p.skin.leftLeg.rotation.set(-1.5, 0, 0.12); // Beine angewinkelt
p.skin.rightLeg.rotation.set(-1.5, 0, -0.12);
} },
spooky: { label: 'Unheimlich', fov: 32, zoom: 0.6, apply: (p) => {
p.rotation.set(-0.06, -0.45, 0); // gedreht + leichte Untersicht
p.skin.head.rotation.set(0.22, -0.2, 0.28); // Kopf schief gelegt
p.skin.leftArm.rotation.set(-1.35, 0, 0.12); // Arme nach vorn ausgestreckt
p.skin.rightArm.rotation.set(-1.25, 0, -0.16);
p.skin.leftLeg.rotation.set(0.18, 0, 0.06);
p.skin.rightLeg.rotation.set(-0.12, 0, -0.06);
} },
};
// Rendert den Skin lokal mit WebGL in der gewählten Pose
/*
* Rendert eine Skin-Textur als Spielfigur und gibt ein Bild zurück.
* quelle: Adresse ODER data:-URI – so lässt sich auch eine Datei anzeigen,
* die noch gar nicht hochgeladen ist.
* modell: 'default' (breite Arme) oder 'slim' (Alex)
*/
async function renderSkin3D(quelle, poseKey, opts = {}) {
if (typeof skinview3d === 'undefined') return null;
const canvas = el('skin-canvas');
const breite = opts.breite || 420;
const hoehe = opts.hoehe || 700;
try {
const viewer = new skinview3d.SkinViewer({
canvas, width: breite, height: hoehe,
skin: quelle,
model: opts.modell === 'slim' ? 'slim' : 'default',
preserveDrawingBuffer: true,
});
// Auf das Laden der Skin-Textur warten
await new Promise((resolve, reject) => {
const t = setTimeout(reject, 8000);
const check = () => {
if (viewer.playerObject && viewer.playerObject.skin && viewer.playerObject.skin.map) { clearTimeout(t); resolve(); }
else setTimeout(check, 60);
};
check();
});
const pose = SKIN_POSES[poseKey] || SKIN_POSES.walking;
viewer.fov = pose.fov;
viewer.zoom = pose.zoom;
viewer.autoRotate = false;
viewer.animation = null;
pose.apply(viewer.playerObject);
viewer.render();
const data = canvas.toDataURL('image/png');
viewer.dispose();
return data && data.length > 2000 ? data : null;
} catch {
return null;
}
}
// ---------- Icon-System (SVG statt Emoji) ----------
const ICONS_SVG = {
plus: ' ',
import: ' ',
package: ' ',
folder: ' ',
terminal: ' ',
user: ' ',
settings: ' ',
play: ' ',
blocks: ' ',
globe: ' ',
image: ' ',
edit: ' ',
copy: ' ',
export: ' ',
trash: ' ',
};
function svgIcon(name) {
const p = ICONS_SVG[name];
if (!p) return '';
const filled = name === 'play';
const attrs = filled
? 'fill="currentColor" stroke="none"'
: 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
return `${p} `;
}
function applyIcons(root = document) {
root.querySelectorAll('[data-ic]').forEach((b) => {
if (b.querySelector('svg')) return;
b.insertAdjacentHTML('afterbegin', svgIcon(b.dataset.ic));
});
}
// ---------- Instanz-Kacheln ----------
const SINGLE_SELECTION_BUTTON_IDS = [
'p-play', 'p-offline', 'p-edit', 'p-notes', 'p-mods', 'p-worlds', 'p-screens', 'p-logs', 'p-servers', 'p-modpack',
'p-mcfolder', 'p-configfolder', 'p-instfolder', 'p-shortcut', 'p-export', 'p-copy',
];
function orderedSelectedInstanceIds() {
return instances.map((inst) => inst.id).filter((id) => selectedInstanceIds.has(id));
}
function syncCardSelectionUi() {
document.querySelectorAll('.card').forEach((card) => {
card.classList.toggle('sel', selectedInstanceIds.has(card.dataset.id));
});
}
function setSingleSelectionActionsDisabled(disabled) {
SINGLE_SELECTION_BUTTON_IDS.forEach((id) => { el(id).disabled = disabled; });
const groupLine = el('p-groupline');
groupLine.style.pointerEvents = disabled ? 'none' : '';
groupLine.style.color = disabled ? 'var(--dim)' : '';
}
async function refreshSelectionPanel() {
const selectedIds = orderedSelectedInstanceIds();
const count = selectedIds.length;
const panelC = el('p-content');
const panelE = el('p-empty');
syncCardSelectionUi();
if (!count) {
currentDetailId = null;
setSingleSelectionActionsDisabled(false);
el('p-delete').disabled = false;
el('p-delete').textContent = 'Löschen';
panelC.classList.add('hidden');
panelE.classList.remove('hidden');
el('status-left').textContent = 'Bereit';
el('status-play').textContent = 'Keine Instanz ausgewählt';
el('status-total').textContent = '';
return;
}
if (count > 1) {
currentDetailId = null;
setSingleSelectionActionsDisabled(true);
el('p-delete').disabled = false;
el('p-delete').textContent = `${count} Instanzen löschen`;
panelE.classList.add('hidden');
panelC.classList.remove('hidden');
el('p-icon').innerHTML = iconOf('chest', 60);
el('p-name').textContent = `${count} Instanzen ausgewählt`;
el('p-groupline').textContent = 'Mehrfachauswahl aktiv';
el('status-left').textContent = `${count} Instanzen ausgewählt`;
el('status-play').textContent = 'Strg+Klick wählt weitere Instanzen aus oder ab';
el('status-total').textContent = 'Sammellöschen verfügbar';
return;
}
const id = selectedIds[0];
currentDetailId = id;
setSingleSelectionActionsDisabled(false);
el('p-delete').disabled = false;
el('p-delete').textContent = 'Löschen';
window.api.saveSettings({ lastSelectedId: id || '' });
const inst = id ? await window.api.getInstance(id) : null;
if (!inst) {
selectedInstanceIds.delete(id);
panelC.classList.add('hidden');
panelE.classList.remove('hidden');
el('status-left').textContent = 'Bereit';
el('status-play').textContent = 'Keine Instanz ausgewählt';
el('status-total').textContent = '';
return;
}
panelE.classList.add('hidden');
panelC.classList.remove('hidden');
// "Modpack aktualisieren" nur bei Instanzen, die aus einem Modrinth-Paket stammen
el('p-modpack').classList.toggle('hidden', !(inst.modpack && inst.modpack.projectId));
const iconData = inst.customImage ? await window.api.instanceIcon(id) : null;
if (iconData) el('p-icon').innerHTML = ` `;
else { el('p-icon').innerHTML = iconOf(inst.icon, 60); }
el('p-name').textContent = inst.name;
el('p-groupline').textContent = inst.group ? `Gruppe: ${inst.group}` : 'Ohne Gruppe';
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
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);
}
async function applySelection(ids) {
const valid = new Set(instances.map((inst) => inst.id));
selectedInstanceIds = new Set((ids || []).filter((id) => valid.has(id)));
await refreshSelectionPanel();
}
async function toggleInstanceSelection(id) {
const next = new Set(selectedInstanceIds);
if (next.has(id)) next.delete(id);
else next.add(id);
await applySelection([...next]);
}
function buildCard(inst) {
const card = document.createElement('div');
card.className = 'card' + (selectedInstanceIds.has(inst.id) ? ' sel' : '');
card.dataset.id = inst.id;
const iconHtml = inst.iconData
? ` `
: `${iconOf(inst.icon, 60)} `;
// 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) : '';
let tip = inst.name + '\n' + (inst.minecraft.version || 'ohne Version') + loaderTxt;
if (inst.notes && inst.notes.trim()) tip += '\n\n' + inst.notes.trim();
card.title = tip;
card.innerHTML = `
${iconHtml}
${escapeHtml(inst.name)}
`;
// einfacher Klick = auswählen (MultiMC), Doppelklick = starten
card.addEventListener('click', (e) => {
if (e.ctrlKey || e.metaKey) toggleInstanceSelection(inst.id);
else selectInstance(inst.id);
});
card.addEventListener('dblclick', () => launch(inst.id));
// Rechtsklick: Instanz auswählen und Kontextmenü öffnen
card.addEventListener('contextmenu', async (e) => {
e.preventDefault();
if (!selectedInstanceIds.has(inst.id)) await selectInstance(inst.id);
openContextMenu(e.clientX, e.clientY); // sofort anzeigen
});
// Drag & Drop in Gruppen
card.draggable = true;
card.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', inst.id); card.classList.add('dragging'); });
card.addEventListener('dragend', () => card.classList.remove('dragging'));
return card;
}
function render() {
const q = el('search').value.trim().toLowerCase();
const grid = el('grid');
grid.innerHTML = '';
const filtered = instances.filter((i) =>
!q || (i.name + ' ' + i.minecraft.version + ' ' + i.minecraft.loader + ' ' + (i.group || '')).toLowerCase().includes(q));
el('empty').classList.toggle('hidden', instances.length !== 0);
grid.classList.toggle('hidden', instances.length === 0);
// nach Gruppen bündeln
const groups = new Map();
for (const inst of filtered) {
const g = (inst.group && inst.group.trim()) || '';
if (!groups.has(g)) groups.set(g, []);
groups.get(g).push(inst);
}
// benannte Gruppen alphabetisch, "ohne Gruppe" zuletzt
const names = [...groups.keys()].sort((a, b) => (a === '' ? 1 : b === '' ? -1 : a.localeCompare(b)));
const showHeaders = names.some((n) => n !== '');
for (const g of names) {
const list = groups.get(g);
const section = document.createElement('div');
section.className = 'group-section' + (collapsedGroups.has(g) ? ' collapsed' : '');
if (showHeaders) {
const header = document.createElement('div');
header.className = 'group-header' + (collapsedGroups.has(g) ? ' collapsed' : '');
header.innerHTML = `${collapsedGroups.has(g) ? '▸' : '▾'} ${escapeHtml(g || 'Ohne Gruppe')} (${list.length}) `;
header.addEventListener('click', () => {
if (collapsedGroups.has(g)) collapsedGroups.delete(g); else collapsedGroups.add(g);
render();
});
section.appendChild(header);
}
const gridEl = document.createElement('div');
gridEl.className = 'group-grid';
for (const inst of list) gridEl.appendChild(buildCard(inst));
section.appendChild(gridEl);
// Drop-Ziel: Instanz per Drag&Drop in diese Gruppe verschieben
section.addEventListener('dragover', (e) => { e.preventDefault(); section.classList.add('drop-hover'); });
section.addEventListener('dragleave', (e) => { if (!section.contains(e.relatedTarget)) section.classList.remove('drop-hover'); });
section.addEventListener('drop', async (e) => {
e.preventDefault();
section.classList.remove('drop-hover');
const dropId = e.dataTransfer.getData('text/plain');
const dragged = instances.find((i) => i.id === dropId);
if (dragged && (dragged.group || '') !== g) {
await window.api.updateInstance(dropId, { group: g });
await reload();
toast('Verschoben nach: ' + (g || 'Ohne Gruppe'));
}
});
grid.appendChild(section);
}
status(`${instances.length} Instanz${instances.length === 1 ? '' : 'en'}`);
}
function fillGroupDatalist() {
const groups = [...new Set(instances.map((i) => i.group).filter(Boolean))].sort();
el('group-list').innerHTML = groups.map((g) => ` `).join('');
}
async function reload() {
instances = await window.api.listInstances();
const valid = new Set(instances.map((inst) => inst.id));
selectedInstanceIds = new Set([...selectedInstanceIds].filter((id) => valid.has(id)));
render();
await refreshSelectionPanel();
}
// ---------- Minecraft-Versionen ----------
async function loadVersions() {
if (mcVersionData) return mcVersionData;
try {
mcVersionData = await window.api.mcVersions();
} catch (e) {
mcVersionData = { offline: true, versions: [], latest: {} };
}
return mcVersionData;
}
function fillVersionSelect(selectedId) {
const type = el('f-version-type').value;
const sel = el('f-version');
const hint = el('f-version-hint');
sel.innerHTML = '';
const data = mcVersionData || { versions: [], latest: {} };
let list = data.versions || [];
if (type !== 'all') list = list.filter((v) => v.type === type);
// gewählte Version muss vorhanden sein (auch wenn Filter sie ausblendet)
if (selectedId && !list.some((v) => v.id === selectedId)) {
const found = (data.versions || []).find((v) => v.id === selectedId);
list = [found || { id: selectedId, type: 'unknown' }, ...list];
}
if (!list.length) {
const o = document.createElement('option');
o.value = selectedId || '';
o.textContent = selectedId || '(keine Versionsliste – offline?)';
sel.appendChild(o);
} else {
for (const v of list.slice(0, 1200)) {
const o = document.createElement('option');
o.value = v.id;
o.textContent = v.id;
if (v.id === selectedId) o.selected = true;
sel.appendChild(o);
}
}
if (data.offline) hint.textContent = '⚠ Offline – Versionsliste evtl. veraltet.';
else if (data.latest) hint.textContent = `Neuestes Release: ${data.latest.release || '?'} · Snapshot: ${data.latest.snapshot || '?'}`;
}
// Loader-Versionen (Fabric/Quilt) passend zur MC-Version laden
async function updateLoaderVersions(preselect) {
const loader = el('f-loader').value;
const mc = el('f-version').value;
const sel = el('f-loader-version');
const hint = el('f-loader-hint');
sel.innerHTML = 'neueste (empfohlen) ';
const ofTools = el('f-optifine-tools');
if (ofTools) ofTools.classList.toggle('hidden', loader !== 'optifine');
if (loader === 'vanilla') { sel.disabled = true; hint.textContent = 'Vanilla hat keinen Loader.'; return; }
if (loader === 'optifine') {
// Kein Loader-API bei OptiFine – die Liste zeigt bereits eingerichtete
// Fassungen (siehe f-optifine-tools weiter unten), keine automatisch
// ladbaren Versionen.
sel.innerHTML = '– keine gewählt – ';
sel.disabled = false;
if (el('f-optifine-status')) el('f-optifine-status').textContent = '';
if (el('f-optifine-confirm')) el('f-optifine-confirm').classList.add('hidden');
if (!editingId) {
hint.textContent = 'Erst speichern, dann lässt sich OptiFine im Bearbeiten-Dialog einrichten.';
if (el('f-optifine-install')) el('f-optifine-install').disabled = true;
return;
}
if (el('f-optifine-install')) el('f-optifine-install').disabled = false;
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
hint.textContent = 'Suche bereits eingerichtete OptiFine-Fassungen …';
try {
const found = await window.api.optifineFind(mc);
for (const v of found) {
const o = document.createElement('option');
o.value = v; o.textContent = v;
if (v === preselect) o.selected = true;
sel.appendChild(o);
}
hint.textContent = found.length
? `${found.length} eingerichtete OptiFine-Fassung(en) für ${mc}.`
: 'Noch keine OptiFine-Installation für diese Version – über den Knopf unten einrichten.';
} catch {
hint.textContent = 'Suche fehlgeschlagen.';
}
return;
}
sel.disabled = false;
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
hint.textContent = 'Lade Loader-Versionen …';
try {
const versions = await window.api.loaderVersions(loader, mc);
for (const v of versions.slice(0, 80)) {
const o = document.createElement('option');
o.value = v.version;
o.textContent = v.version + (v.stable ? '' : ' (beta)');
if (v.version === preselect) o.selected = true;
sel.appendChild(o);
}
hint.textContent = versions.length ? `${versions.length} Version(en) verfügbar` : 'Keine Loader-Version für diese MC-Version.';
} catch {
hint.textContent = 'Loader-Versionen konnten nicht geladen werden.';
}
}
// ---------- 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 }));
if (real.length) return real;
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 it of availableIcons()) {
const o = document.createElement('option');
o.value = it.key;
o.textContent = it.label;
if (it.key === selected) o.selected = true;
sel.appendChild(o);
}
renderIconChoices(selected);
}
// klickbare Item-Vorschau unter der Auswahl
function renderIconChoices(selected) {
const box = el('f-icon-grid');
if (!box) return;
box.innerHTML = '';
for (const it of availableIcons()) {
const b = document.createElement('button');
b.type = 'button';
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 = it.key;
renderIconChoices(it.key);
});
box.appendChild(b);
}
}
// zeigt/versteckt die Bildvorschau im Dialog
function setIconPreview(dataUri) {
const box = el('f-icon-preview');
const clr = el('f-clear-image');
if (dataUri) {
box.innerHTML = ` `;
box.classList.remove('hidden');
clr.classList.remove('hidden');
} else {
box.innerHTML = '';
box.classList.add('hidden');
clr.classList.add('hidden');
}
}
function resetIconControls() {
pendingImagePath = null;
pendingClearImage = false;
setIconPreview(null);
}
async function openCreate() {
editingId = null;
el('instance-modal-title').textContent = 'Neue Instanz';
el('f-name').value = '';
el('f-group').value = '';
fillGroupDatalist();
el('f-loader').value = 'vanilla';
el('f-loader-version').value = '';
el('f-notes').value = '';
fillIconSelect('grass');
resetIconControls();
el('f-advanced').classList.add('hidden');
show('modal-instance');
el('f-name').focus();
el('f-version-hint').textContent = 'Lade Versionsliste …';
await loadVersions();
el('f-version-type').value = 'release';
fillVersionSelect(mcVersionData.latest && mcVersionData.latest.release);
updateLoaderVersions();
}
async function openEdit(inst) {
editingId = inst.id;
el('instance-modal-title').textContent = 'Instanz bearbeiten';
el('f-name').value = inst.name;
el('f-group').value = inst.group || '';
fillGroupDatalist();
el('f-loader').value = inst.minecraft.loader || 'vanilla';
el('f-loader-version').value = inst.minecraft.loaderVersion || '';
el('f-notes').value = inst.notes || '';
fillIconSelect(inst.icon);
resetIconControls();
if (inst.customImage) {
const data = await window.api.instanceIcon(inst.id);
if (data) setIconPreview(data);
}
el('f-min-mem').value = inst.java.minMemMb || '';
el('f-max-mem').value = inst.java.maxMemMb || '';
zeigeSpeicherHinweis();
fuelleInstanzJava(inst);
el('f-extra-args').value = inst.java.extraArgs || '';
el('f-prelaunch').value = inst.preLaunchCommand || '';
el('f-postexit').value = inst.postExitCommand || '';
el('f-advanced').classList.remove('hidden');
show('modal-instance');
el('f-name').focus();
el('f-version-hint').textContent = 'Lade Versionsliste …';
await loadVersions();
el('f-version-type').value = inst.minecraft.version ? 'all' : 'release';
fillVersionSelect(inst.minecraft.version || (mcVersionData.latest && mcVersionData.latest.release));
await updateLoaderVersions(inst.minecraft.loaderVersion);
}
async function saveInstance() {
const name = el('f-name').value.trim();
if (!name) { toast('Bitte einen Namen angeben.', true); el('f-name').focus(); return; }
const payload = {
name,
group: el('f-group').value.trim(),
icon: el('f-icon').value,
notes: el('f-notes').value.trim(),
minecraft: {
version: el('f-version').value.trim(),
loader: el('f-loader').value,
loaderVersion: el('f-loader-version').value.trim(),
},
};
// Speicherangaben prüfen, bevor gespeichert wird
const minMem = numOrNull(el('f-min-mem').value);
const maxMem = numOrNull(el('f-max-mem').value);
if (minMem || maxMem) {
const pruefung = await window.api.checkMemory(minMem, maxMem);
if (!pruefung.ok) { toast(pruefung.message, true); return; }
if (pruefung.warnung) toast(pruefung.warnung, false);
}
try {
let targetId = editingId;
if (editingId) {
payload.java = {
path: el('f-java').value.trim(),
minMemMb: minMem,
maxMemMb: maxMem,
extraArgs: el('f-extra-args').value.trim(),
};
payload.preLaunchCommand = el('f-prelaunch').value.trim();
payload.postExitCommand = el('f-postexit').value.trim();
await window.api.updateInstance(editingId, payload);
} else {
const created = await window.api.createInstance(payload);
targetId = created.id;
}
// eigenes Bild anwenden bzw. entfernen
if (pendingImagePath) await window.api.setInstanceImage(targetId, pendingImagePath);
else if (pendingClearImage && editingId) await window.api.clearInstanceImage(targetId);
toast(editingId ? 'Instanz gespeichert.' : 'Instanz angelegt.');
hide('modal-instance');
await reload();
await selectInstance(targetId);
} catch (e) {
toast('Fehler: ' + e.message, true);
}
}
// ---------- Auswahl + rechte Aktionsleiste (MultiMC-Stil) ----------
async function selectInstance(id) {
await applySelection(id ? [id] : []);
}
let launching = false;
async function launch(id, accountId, launchOpts) {
if (launching) return;
launching = true;
status('Vorbereiten …');
el('status-right').textContent = '';
appendConsole('\n=== Start: ' + new Date().toLocaleTimeString('de-DE') + ' ===\n');
try {
const res = await window.api.launchInstance(id, accountId, launchOpts || null);
if (res.ok) {
status('Läuft (PID ' + res.pid + ')');
toast('Minecraft gestartet.');
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);
} else {
status('Bereit'); el('status-right').textContent = '';
toast(res.message || 'Start fehlgeschlagen.', true);
}
} catch (e) {
status('Bereit'); toast('Fehler: ' + e.message, true);
} finally {
launching = false;
}
}
// ---------- Java je Instanz ----------
async function fuelleInstanzJava(inst) {
const feld = el('f-java');
if (!feld) return;
feld.value = (inst && inst.java && inst.java.path) || '';
const sel = el('f-java-detected');
sel.innerHTML = '– gefundene Java-Installationen – ';
try {
for (const j of await window.api.listJava()) {
const o = document.createElement('option');
o.value = j.path;
o.textContent = `Java ${j.major} (${j.arch}) – ${j.path}`;
sel.appendChild(o);
}
} catch { /* Liste bleibt leer */ }
sel.value = feld.value && [...sel.options].some((o) => o.value === feld.value) ? feld.value : '';
// Der häufigste Stolperstein: global ist eine feste Fassung eingetragen,
// die dann auch für alte Minecraft-Versionen genommen wird.
try {
const s = await window.api.getSettings();
el('f-java-hint').textContent = s.javaPath
? `Achtung: In den Einstellungen ist ${s.javaPath} fest vorgegeben und gilt für alle Instanzen. `
+ 'Trage hier etwas ein, wenn diese Instanz eine andere Fassung braucht – ältere Minecraft-Versionen verlangen oft Java 8.'
: 'Leer lassen: Der Launcher nimmt die Fassung, die diese Minecraft-Version verlangt, und lädt sie bei Bedarf selbst.';
} catch { /* egal */ }
}
// ---------- Arbeitsspeicher ----------
let systemSpeicher = null;
async function zeigeSpeicherHinweis() {
const feld = el('f-mem-hint');
if (!feld) return;
try {
if (!systemSpeicher) systemSpeicher = await window.api.systemMemory();
} catch { return; }
const gesamt = systemSpeicher.totalMb;
const empfohlen = Math.floor(gesamt * 0.5 / 256) * 256;
feld.textContent = `Im Rechner stecken ${gesamt} MB. Leer lassen übernimmt die globale Vorgabe. `
+ `Für die meisten Modpacks reichen 4096–8192 MB; mehr als ${Math.floor(gesamt * 0.8 / 256) * 256} MB sollten es nicht sein `
+ `(Vorschlag: ${empfohlen} MB).`;
}
// ---------- Modpack aktualisieren ----------
let modpackNeueVersion = null;
async function openModpackUpdate() {
if (!currentDetailId) return;
modpackNeueVersion = null;
el('mp-status').textContent = 'Suche nach einer neueren Fassung …';
el('mp-kern').classList.add('hidden');
el('mp-do').classList.add('hidden');
show('modal-modpack');
const res = await window.api.modpackCheck(currentDetailId);
if (!res.ok) { el('mp-status').textContent = '✖ ' + (res.message || 'Suche fehlgeschlagen.'); return; }
if (!res.verfuegbar) {
el('mp-status').textContent = `${res.name || 'Das Modpack'} ist auf dem neuesten Stand (${res.aktuelleNummer}).`;
return;
}
modpackNeueVersion = res.neueVersionId;
el('mp-status').textContent = `Für ${res.name || 'dieses Modpack'} gibt es eine neuere Fassung.`;
const zeile = (t, w) => (w ? `` : '');
el('mp-kern').innerHTML = zeile('Installiert', res.aktuelleNummer) + zeile('Neu', res.neueNummer)
+ zeile('Minecraft', res.spielVersion)
+ (res.hinweise ? zeile('Änderungen', res.hinweise.slice(0, 400)) : '');
el('mp-kern').classList.remove('hidden');
el('mp-do').classList.remove('hidden');
}
async function doModpackUpdate() {
if (!modpackNeueVersion) return;
const btn = el('mp-do');
btn.disabled = true;
el('mp-status').textContent = 'Wird aktualisiert – das kann einige Minuten dauern …';
const res = await window.api.modpackUpdate(currentDetailId, modpackNeueVersion);
btn.disabled = false;
if (!res.ok) { el('mp-status').textContent = '✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.'); return; }
el('mp-status').textContent = `✔ Auf ${res.neueNummer} aktualisiert: ${res.eingespielt} Dateien eingespielt`
+ (res.entfernt ? `, ${res.entfernt} entfernt` : '') + '.';
el('mp-do').classList.add('hidden');
toast('Modpack aktualisiert.');
await reload();
await selectInstance(currentDetailId);
}
// ---------- Serverliste ----------
let zuUebertragenderServer = null;
async function openServers() {
if (!currentDetailId) return;
const inst = await window.api.getInstance(currentDetailId);
el('servers-title').textContent = 'Server – ' + (inst ? inst.name : '');
el('srv-name').value = '';
el('srv-ip').value = '';
show('modal-servers');
await renderServers();
}
async function renderServers() {
const box = el('srv-list');
box.innerHTML = 'Lade …
';
const res = await window.api.serversList(currentDetailId);
box.innerHTML = '';
if (!res.ok) { box.innerHTML = `${escapeHtml(res.message || 'Liste nicht lesbar.')}
`; return; }
if (!res.server.length) {
box.innerHTML = 'Noch keine Server eingetragen.
';
return;
}
for (const s of res.server) {
const item = document.createElement('div');
item.className = 'mod-item';
item.innerHTML =
`🖥
${escapeHtml(s.name || s.ip)}
${escapeHtml(s.ip)}
Wird geprüft …
`;
const aktionen = item.querySelector('.mod-action');
pingServerRow(s.ip, item.querySelector('.srv-status'));
const starten = document.createElement('button');
starten.className = 'btn btn-primary btn-mini';
starten.textContent = 'Starten';
starten.title = 'Instanz starten und direkt diesem Server beitreten (Quick Play, ab MC 1.20)';
starten.addEventListener('click', () => {
hide('modal-servers');
launch(currentDetailId, null, { quickPlayServer: s.ip });
});
aktionen.appendChild(starten);
const favorit = document.createElement('button');
favorit.className = 'btn btn-mini';
favorit.textContent = '★ Verknüpfung';
favorit.title = 'Desktop-Verknüpfung erstellen, die Minecraft startet und direkt mit diesem Server verbindet (Quick Play, ab MC 1.20)';
favorit.addEventListener('click', async () => {
const inst = await window.api.getInstance(currentDetailId);
const vorschlag = `${inst ? inst.name : 'Instanz'} - ${s.name || s.ip}`;
const eingabe = await askPrompt({
title: 'Server-Verknüpfung erstellen',
text: 'Name der Desktop-Verknüpfung (ohne .lnk):',
label: 'Name',
value: vorschlag,
okLabel: 'Erstellen',
});
if (eingabe == null) return; // abgebrochen
const res = await window.api.serversShortcut(currentDetailId, s.ip, s.name, eingabe.trim());
if (res.ok) toast('Verknüpfung auf dem Desktop erstellt: ' + (s.name || s.ip));
else toast(res.message || 'Verknüpfung fehlgeschlagen.', true);
});
aktionen.appendChild(favorit);
const umbenennen = document.createElement('button');
umbenennen.className = 'btn btn-mini';
umbenennen.textContent = 'Umbenennen';
umbenennen.title = 'Anzeigenamen ändern';
umbenennen.addEventListener('click', async () => {
const aktuell = s.name || s.ip;
const eingabe = await askPrompt({
title: 'Server umbenennen',
text: 'Adresse: ' + s.ip,
label: 'Name',
value: aktuell,
okLabel: 'Speichern',
});
if (eingabe == null) return; // abgebrochen
const res = await window.api.serversRename(currentDetailId, s.ip, eingabe);
if (!res.ok) { toast(res.message || 'Umbenennen fehlgeschlagen.', true); return; }
if (!res.unveraendert) toast('Umbenannt: ' + eingabe.trim());
await renderServers();
});
aktionen.appendChild(umbenennen);
const kopieren = document.createElement('button');
kopieren.className = 'btn btn-mini';
kopieren.textContent = 'Übertragen…';
kopieren.title = 'Diesen Server in andere Instanzen eintragen';
kopieren.addEventListener('click', () => openServerCopy(s));
aktionen.appendChild(kopieren);
const weg = document.createElement('button');
weg.className = 'btn btn-danger btn-mini';
weg.textContent = 'Entfernen';
weg.addEventListener('click', async () => {
const r = await window.api.serversRemove(currentDetailId, s.ip);
if (r.ok) toast('Entfernt: ' + (s.name || s.ip));
else toast(r.message || 'Entfernen fehlgeschlagen.', true);
await renderServers();
});
aktionen.appendChild(weg);
box.appendChild(item);
}
}
// Fragt Live-Status (MOTD/Spieler/Version) ab und trägt ihn in die Zeile ein.
// Läuft unabhängig für jede Zeile, ohne die Liste selbst zu blockieren.
async function pingServerRow(ip, statusEl) {
if (!statusEl) return;
const res = await window.api.serversPing(ip);
// Zeile kann inzwischen aus dem DOM entfernt worden sein (Liste neu aufgebaut)
if (!statusEl.isConnected) return;
if (!res.ok) {
statusEl.textContent = '⚠ ' + (res.message || 'nicht erreichbar');
statusEl.classList.add('status-bad');
return;
}
const spieler = res.max != null ? `${res.online ?? '?'}/${res.max} Spieler` : '';
statusEl.textContent = [res.motd, spieler, res.version, res.latencyMs + ' ms']
.filter(Boolean).join(' · ');
statusEl.classList.add('status-ok');
}
async function addServer() {
const eintrag = { name: el('srv-name').value.trim(), ip: el('srv-ip').value.trim() };
const res = await window.api.serversAdd(currentDetailId, eintrag);
if (!res.ok) { toast(res.message || 'Hinzufügen fehlgeschlagen.', true); return; }
el('srv-name').value = '';
el('srv-ip').value = '';
toast('Server hinzugefügt.');
await renderServers();
}
async function openServerCopy(server) {
zuUebertragenderServer = server;
el('srv-copy-sub').textContent = `„${server.name || server.ip}" (${server.ip}) in diese Instanzen eintragen:`;
const box = el('srv-copy-list');
box.innerHTML = '';
const alle = await window.api.listInstances();
const andere = alle.filter((i) => i.id !== currentDetailId);
if (!andere.length) {
box.innerHTML = 'Es gibt keine weitere Instanz.
';
}
for (const i of andere) {
const zeile = document.createElement('label');
zeile.className = 'checkbox-field';
zeile.style.padding = '6px 2px';
zeile.innerHTML = ` ${escapeHtml(i.name)} `;
box.appendChild(zeile);
}
show('modal-srv-copy');
}
async function doServerCopy() {
if (!zuUebertragenderServer) return;
const ids = [...el('srv-copy-list').querySelectorAll('input:checked')].map((c) => c.value);
if (!ids.length) { toast('Keine Instanz ausgewählt.', true); return; }
const res = await window.api.serversCopyTo(ids, zuUebertragenderServer);
hide('modal-srv-copy');
if (res.erledigt.length) toast(`Eingetragen in: ${res.erledigt.join(', ')}`);
for (const f of res.fehler) toast(`${f.name || f.id}: ${f.message}`, true);
}
// ---------- Skin ändern ----------
let gewaehlterSkin = null;
/*
* Bild nur zeigen, wenn es wirklich geladen wurde – ein ohne Quelle
* stellt sonst das Platzhalter-Symbol des Browsers dar.
*/
function zeigeSkinBild(bildId, leerId, quelle, leerText) {
const bild = el(bildId);
const leer = el(leerId);
leer.textContent = leerText;
if (!quelle) {
bild.classList.add('hidden');
bild.removeAttribute('src');
leer.classList.remove('hidden');
return;
}
bild.onload = () => { bild.classList.remove('hidden'); leer.classList.add('hidden'); };
bild.onerror = () => {
bild.classList.add('hidden');
bild.removeAttribute('src');
leer.textContent = 'Vorschau nicht ladbar';
leer.classList.remove('hidden');
};
bild.src = quelle;
}
// Textur einer Seite merken, damit bei einem Wechsel der Armform beide
// Figuren neu gerendert werden können
let skinTexturAktuell = '';
let skinTexturNeu = '';
// Skin-Textur als Figur rendern; klappt das nicht, wird die Textur gezeigt
async function zeigeSkinFigur(bildId, leerId, textur, leerText) {
if (!textur) { zeigeSkinBild(bildId, leerId, '', leerText); return; }
el(leerId).textContent = 'wird gerendert …';
const modell = el('skin-variante').value === 'slim' ? 'slim' : 'default';
const figur = await renderSkin3D(textur, 'standing', { modell, breite: 260, hoehe: 420 });
zeigeSkinBild(bildId, leerId, figur || textur, leerText);
}
async function openSkin() {
gewaehlterSkin = null;
skinTexturAktuell = '';
skinTexturNeu = '';
el('skin-pfad').value = '';
el('skin-apply').disabled = true;
el('skin-status').textContent = '';
zeigeSkinBild('skin-neu', 'skin-neu-leer', '', 'noch keine Datei gewählt');
zeigeSkinBild('skin-aktuell', 'skin-aktuell-leer', '', 'wird geladen …');
el('skin-konto').textContent = 'Profil wird geladen …';
show('modal-skin');
const p = await window.api.skinProfile();
if (!p.ok) {
el('skin-konto').textContent = p.message || 'Profil konnte nicht geladen werden.';
zeigeSkinBild('skin-aktuell', 'skin-aktuell-leer', '', 'kein Profil geladen');
return;
}
el('skin-konto').textContent = `Angemeldet als ${p.name}. Die Änderung gilt für dieses Konto.`;
// Armform zuerst setzen – davon hängt ab, wie die Figur gerendert wird
if (p.variante) el('skin-variante').value = p.variante === 'slim' ? 'slim' : 'classic';
skinTexturAktuell = p.skinUrl || '';
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell,
'Standard-Skin (keine eigene Datei hinterlegt)');
}
// Armform gewechselt -> beide Figuren neu zeichnen
async function skinVarianteGewechselt() {
if (skinTexturAktuell) await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, '');
if (skinTexturNeu) await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, '');
}
async function waehleSkinDatei() {
const res = await window.api.skinChoose();
if (res.canceled) return;
if (!res.ok) { el('skin-status').textContent = '✖ ' + res.message; return; }
gewaehlterSkin = res.pfad;
el('skin-pfad').value = res.pfad;
skinTexturNeu = res.dataUri || '';
await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, 'Vorschau nicht verfügbar');
el('skin-status').textContent = `${res.breite}×${res.hoehe} Pixel – sieht gut aus.`;
el('skin-apply').disabled = false;
}
async function setzeSkin() {
if (!gewaehlterSkin) return;
const btn = el('skin-apply');
btn.disabled = true;
el('skin-status').textContent = 'Wird hochgeladen …';
const res = await window.api.skinApply(gewaehlterSkin, el('skin-variante').value);
if (res.ok) {
el('skin-status').textContent = '✔ Skin gesetzt. Im Spiel kann es einen Moment dauern.';
toast('Skin geändert.');
await openSkin(); // Vorschau mit dem neuen Stand auffrischen
await refreshSkinBackground(); // auch der Hintergrund zeigt jetzt den neuen Skin
} else {
el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
btn.disabled = false;
}
}
async function setzeSkinZurueck() {
const btn = el('skin-reset');
btn.disabled = true;
el('skin-status').textContent = 'Wird zurückgesetzt …';
const res = await window.api.skinReset();
if (res.ok) {
toast('Skin auf Standard zurückgesetzt.');
await openSkin();
await refreshSkinBackground();
} else {
el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
}
btn.disabled = false;
}
// ---------- Logdateien ----------
let aktuellesLog = '';
async function openLogs() {
if (!currentDetailId) return;
const inst = await window.api.getInstance(currentDetailId);
el('logs-title').textContent = 'Logdateien – ' + (inst ? inst.name : '');
const auswahl = el('logs-select');
auswahl.innerHTML = '';
el('logs-text').textContent = 'Lade …';
el('logs-info').textContent = '';
aktuellesLog = '';
show('modal-logs');
const dateien = await window.api.logsList(currentDetailId);
if (!dateien.length) {
el('logs-text').textContent = 'Für diese Instanz gibt es noch keine Logdateien. '
+ 'Sie entstehen beim ersten Spielstart.';
return;
}
for (const d of dateien) {
const o = document.createElement('option');
o.value = d.name;
const kb = Math.max(1, Math.round(d.groesse / 1024));
o.textContent = `${d.name} · ${kb} KB · ${new Date(d.geaendert).toLocaleString('de-DE')}`
+ (d.gepackt ? ' · gepackt' : '');
auswahl.appendChild(o);
}
await ladeLog(dateien[0].name);
}
async function ladeLog(name) {
el('logs-text').textContent = 'Lade …';
const res = await window.api.logsRead(currentDetailId, name);
if (!res.ok) {
el('logs-text').textContent = 'Datei konnte nicht gelesen werden.';
el('logs-info').textContent = '';
aktuellesLog = '';
return;
}
aktuellesLog = res.text;
el('logs-text').textContent = res.text || '(leer)';
el('logs-info').textContent = res.gekuerzt
? `${res.zeilenGesamt} Zeilen – die letzten 5000 werden gezeigt`
: `${res.zeilenGesamt} Zeilen`;
// ans Ende springen: dort steht, was zuletzt passierte
const feld = el('logs-text');
feld.scrollTop = feld.scrollHeight;
}
// ---------- Absturzberichte ----------
let letzterStartMs = 0;
let aktuellerCrash = null;
async function zeigeAbsturzbericht(instanzId) {
const id = instanzId || currentDetailId;
if (!id) return;
// Minecraft schreibt den Bericht erst kurz nach dem Ende
await new Promise((r) => setTimeout(r, 900));
let res;
try { res = await window.api.crashLatest(id, letzterStartMs); }
catch { return; }
if (!res || !res.ok) return; // kein Bericht -> Konsole muss reichen
aktuellerCrash = res;
const inst = await window.api.getInstance(id);
el('crash-sub').textContent = `${inst ? inst.name + ' · ' : ''}${res.datei} · `
+ new Date(res.zeitpunkt).toLocaleString('de-DE');
const zeile = (titel, wert) => wert
? `${titel}
${escapeHtml(wert)}
`
: '';
let html = zeile('Diagnose', res.diagnose) + zeile('Empfohlene Lösung', res.empfehlung)
+ zeile('Beschreibung', res.beschreibung) + zeile('Fehler', res.fehler)
+ zeile('Verdächtige Mods', res.verdaechtigeMods);
if (res.verlauf && res.verlauf.length) {
html += `Aufrufverlauf
`
+ `
${escapeHtml(res.verlauf.join('\n'))}
`;
}
el('crash-kern').innerHTML = html || 'Der Bericht enthält keine der üblichen Kennzeichen.
';
el('crash-voll').textContent = res.text || '';
show('modal-crash');
}
// Fortschrittsmeldungen der Download-/Start-Pipeline
function onLaunchEvt(evt) {
const labels = { version: 'Version', client: 'Client', libraries: 'Libraries', assets: 'Assets', loader: 'Loader', java: 'Java', launch: 'Start', modpack: 'Modpack', backup: 'Sicherung' };
if (evt.phase === 'log') { appendConsole(evt.line); return; }
if (evt.phase === 'exit') {
status('Minecraft beendet (Code ' + evt.code + ')');
el('status-right').textContent = '';
appendConsole('\n[Prozess beendet, Code ' + evt.code + ']\n');
// War vorher "Stopping!" im Log (regulärer Client-Shutdown), zählt ein
// nachträglicher harter Exit-Code nicht als Absturz während des Spiels –
// z. B. wenn Minecrafts interner Shutdown-Watchdog wegen hängender
// Mod-Threads (z. B. JourneyMap WebMap) die JVM nach Ablauf einer
// Wartezeit zwangsweise beendet. Der Bericht bleibt trotzdem gespeichert.
const crashed = evt.code !== 0 && evt.code !== null && !evt.cleanShutdown;
if (evt.cleanShutdown && evt.code !== 0 && evt.code !== null) {
appendConsole('[Hinweis] Der Client wurde regulär beendet (Stopping!), aber danach vom '
+ 'internen Shutdown-Watchdog hart terminiert (z. B. durch einen Mod, dessen Threads '
+ 'nicht rechtzeitig beenden – etwa JourneyMap WebMap). Kein Absturz während des Spiels.\n');
}
// Absturzbericht suchen, bevor über die Konsole entschieden wird –
// ein Bericht sagt mehr als der Java-Auswurf
if (crashed) zeigeAbsturzbericht(evt.id);
window.api.getSettings().then((s) => {
if (crashed && s.showConsoleOnCrash !== false) { openConsole(); return; }
if (s.autoCloseConsole) hide('modal-console');
}).catch(() => {});
return;
}
// Startzeitpunkt merken: nur Berichte ab hier gehören zu diesem Lauf
if (evt.phase === 'launch') letzterStartMs = Date.now();
const label = labels[evt.phase] || evt.phase;
if (evt.total) {
status(`${label} … ${evt.current}/${evt.total}`);
el('status-right').textContent = Math.round((evt.current / evt.total) * 100) + '%';
} else {
status(evt.detail || label + ' …');
el('status-right').textContent = '';
appendConsole('[' + label + '] ' + (evt.detail || '') + '\n');
}
}
// ---------- Konsole ----------
function classifyLine(l) {
if (/\/(ERROR|FATAL|SEVERE)\]/.test(l) || /Exception(:| in )|^\s+at\s|Caused by:/.test(l)) return 'log-error';
if (/\/WARN(ING)?\]/.test(l) || /\bWARN\b/.test(l)) return 'log-warn';
if (/\[Pre-Launch\]|\[Post-Exit\]|=== Start:|\[Prozess beendet/.test(l)) return 'log-info';
return '';
}
function appendLineEl(out, line) {
const div = document.createElement('div');
div.className = 'log-line ' + classifyLine(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 > consoleMaxChars) consoleBuffer = consoleBuffer.slice(-Math.round(consoleMaxChars * 0.75));
consolePending += text;
const lines = consolePending.split('\n');
consolePending = lines.pop();
const out = el('console-out');
if (!el('modal-console').classList.contains('hidden')) {
for (const line of lines) appendLineEl(out, line);
out.scrollTop = out.scrollHeight;
}
}
function openConsole() {
const out = el('console-out');
out.innerHTML = '';
const lines = (consoleBuffer || '(noch keine Ausgabe)').split('\n');
for (const line of lines) appendLineEl(out, line);
consolePending = '';
show('modal-console');
out.scrollTop = out.scrollHeight;
}
// ---------- Welten & Ressourcenpakete ----------
async function openContent() {
const inst = await window.api.getInstance(currentDetailId);
el('content-title').textContent = 'Welten, Ressourcen- & Datenpakete – ' + inst.name;
await renderContentLists();
show('modal-content');
}
async function renderContentLists() {
renderContentList('content-worlds', await window.api.worlds(currentDetailId), 'saves', '🌍');
renderContentList('content-rp', await window.api.resourcepacks(currentDetailId), 'resourcepacks', '🎨');
renderContentList('content-dp', await window.api.datapacks(currentDetailId), 'datapacks', '📜');
}
// ---------- Screenshots ----------
let screenSelected = new Set();
function updateScreensSelectionUi() {
const n = screenSelected.size;
const btn = el('screens-delete-sel');
if (btn) {
btn.disabled = n === 0;
btn.textContent = n ? (n + ' löschen') : 'Auswahl löschen';
}
const hint = el('screens-sel-hint');
if (hint) hint.textContent = n ? (n + ' ausgewählt – Doppelklick öffnet das Bild') : 'Klick auf Vorschaubild zum Auswählen · Doppelklick öffnet';
document.querySelectorAll('#screens-grid .screen-thumb').forEach((t) => {
const name = t.dataset.name;
const on = name && screenSelected.has(name);
t.classList.toggle('selected', !!on);
const cb = t.querySelector('.screen-check');
if (cb) cb.checked = !!on;
});
}
async function openScreens() {
const inst = await window.api.getInstance(currentDetailId);
el('screens-title').textContent = 'Screenshots – ' + inst.name;
const grid = el('screens-grid');
grid.innerHTML = 'Lade …
';
screenSelected = new Set();
updateScreensSelectionUi();
show('modal-screens');
const shots = await window.api.screenshots(currentDetailId);
grid.innerHTML = '';
if (!shots.length) {
grid.innerHTML = 'Keine Screenshots vorhanden.
';
updateScreensSelectionUi();
return;
}
for (const s of shots) {
const thumb = document.createElement('div');
thumb.className = 'screen-thumb';
thumb.dataset.name = s.name;
thumb.innerHTML =
`
${escapeHtml(s.name)}
🗑 `;
const img = thumb.querySelector('img');
window.api.screenshotData(currentDetailId, s.name).then((d) => { if (d) img.src = d; });
const toggle = () => {
if (screenSelected.has(s.name)) screenSelected.delete(s.name);
else screenSelected.add(s.name);
updateScreensSelectionUi();
};
thumb.querySelector('.screen-check').addEventListener('click', (e) => {
e.stopPropagation();
toggle();
});
thumb.addEventListener('click', (e) => {
if (e.target.closest('.screen-del') || e.target.closest('.screen-check')) return;
toggle();
});
img.addEventListener('dblclick', (e) => {
e.stopPropagation();
window.api.openScreenshot(currentDetailId, s.name);
});
thumb.querySelector('.screen-del').addEventListener('click', (e) => {
e.stopPropagation();
askConfirm('Screenshot löschen', `„${s.name}" löschen?`, '🗑 Löschen', async () => {
await window.api.deleteContent(currentDetailId, 'screenshots', s.name);
openScreens();
});
});
grid.appendChild(thumb);
}
updateScreensSelectionUi();
}
async function deleteSelectedScreenshots() {
const names = [...screenSelected];
if (!names.length || !currentDetailId) return;
const label = names.length === 1
? `„${names[0]}" löschen?`
: `${names.length} Screenshots unwiderruflich löschen?`;
askConfirm('Screenshots löschen', label, '🗑 Löschen', async () => {
for (const name of names) {
await window.api.deleteContent(currentDetailId, 'screenshots', name);
}
toast(names.length === 1 ? 'Screenshot gelöscht.' : names.length + ' Screenshots gelöscht.');
openScreens();
});
}
function renderContentList(boxId, items, sub, emoji) {
const box = el(boxId);
box.innerHTML = '';
if (!items.length) { box.innerHTML = 'Nichts vorhanden.
'; return; }
for (const it of items) {
const item = document.createElement('div');
item.className = 'mod-item';
item.innerHTML =
`${emoji}
`;
const aktionen = item.querySelector('.mod-action');
if (sub === 'saves') {
const sichern = document.createElement('button');
sichern.className = 'btn btn-mini';
sichern.textContent = 'Sichern';
sichern.title = 'Nur diese Welt jetzt als ZIP sichern (Dokumente\\AeroMC Launcher\\Welten-Sicherungen)';
sichern.addEventListener('click', async () => {
sichern.disabled = true;
const orig = sichern.textContent;
sichern.textContent = 'Sichere …';
const res = await window.api.backupWorld(currentDetailId, it.name);
sichern.disabled = false;
sichern.textContent = orig;
if (res.ok) toast(`„${it.name}" gesichert.`);
else toast(res.message || 'Sicherung fehlgeschlagen.', true);
});
aktionen.appendChild(sichern);
}
const rm = document.createElement('button');
rm.className = 'btn btn-danger btn-mini';
rm.textContent = 'Löschen';
rm.addEventListener('click', () => askConfirm(
'Löschen', `„${it.name}" wird unwiderruflich gelöscht. Fortfahren?`, '🗑 Löschen',
async () => { await window.api.deleteContent(currentDetailId, sub, it.name); await renderContentLists(); toast('Gelöscht: ' + it.name); },
));
aktionen.appendChild(rm);
box.appendChild(item);
}
}
// ---------- Mod-Browser ----------
// Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader
let modArt = 'mod';
const ART_LABEL = {
mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader', datapack: 'Datenpakete',
};
const ART_SUCHE = {
mod: 'Mods durchsuchen … (z. B. Sodium, JEI, Create)',
resourcepack: 'Ressourcenpakete durchsuchen … (z. B. Faithful, Bare Bones)',
shader: 'Shader durchsuchen … (z. B. Complementary, BSL)',
datapack: 'Datenpakete durchsuchen … (z. B. Terralith, Vanilla Tweaks)',
};
// Nur Mods brauchen einen Loader – Ressourcenpakete, Shader und Datenpakete laufen auch in Vanilla
const ART_BRAUCHT_LOADER = {
mod: true, resourcepack: false, shader: false, datapack: false,
};
// CurseForge hat für Datenpakete keinen verlässlich zugeordneten API-Klassencode in
// diesem Launcher – dort deshalb nur Modrinth als Quelle anbieten, statt falsche
// Treffer (z. B. Mods) unter "Datenpakete" anzuzeigen.
const ART_NUR_MODRINTH = { datapack: true };
async function openMods() {
const inst = await window.api.getInstance(currentDetailId);
if (!inst) return;
modArt = 'mod';
el('m-art').value = 'mod';
await zeigeModAnsicht(inst);
show('modal-mods');
}
// Wechsel der Inhaltsart: alles neu aufbauen
async function modArtGewechselt() {
modArt = el('m-art').value || 'mod';
const inst = await window.api.getInstance(currentDetailId);
if (inst) await zeigeModAnsicht(inst);
}
async function zeigeModAnsicht(inst) {
el('m-title').textContent = ART_LABEL[modArt] + ' – ' + inst.name;
const loaderTxt = LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader;
el('m-sub').textContent = (inst.minecraft.version || 'keine Version') + ' · ' + loaderTxt;
el('m-query').placeholder = ART_SUCHE[modArt];
const warn = el('m-warn');
const brauchtLoader = ART_BRAUCHT_LOADER[modArt];
const blocked = !inst.minecraft.version || (brauchtLoader && inst.minecraft.loader === 'vanilla');
if (blocked) {
warn.classList.remove('hidden');
warn.textContent = !inst.minecraft.version
? '⚠ Diese Instanz hat keine Minecraft-Version. Bitte zuerst im Bearbeiten-Dialog eine Version wählen.'
: '⚠ Vanilla-Instanzen unterstützen keine Mods. Wähle beim Bearbeiten einen Mod-Loader (Fabric/Forge/NeoForge/Quilt). Ressourcenpakete und Shader gehen auch ohne.';
} else {
warn.classList.add('hidden');
}
['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; });
// Kategorien gelten nur für Mods
el('m-category').disabled = blocked || modArt !== 'mod';
// Manche Quellen decken nicht jede Inhaltsart ab (z. B. CurseForge <-> Datenpakete)
const cfOption = el('m-source').querySelector('option[value="curseforge"]');
if (cfOption) cfOption.disabled = !!ART_NUR_MODRINTH[modArt];
if (ART_NUR_MODRINTH[modArt] && el('m-source').value === 'curseforge') el('m-source').value = 'modrinth';
el('m-query').value = '';
el('m-more').classList.add('hidden');
// Ergebnisse der vorigen Ansicht nicht mitschleppen
modUpdates = new Map();
setUpdateStatus('');
el('m-apply-updates').classList.add('hidden');
el('m-check-updates').disabled = blocked;
switchModTab('catalog');
await refreshInstalled();
if (blocked) {
const box = el('m-results');
box.innerHTML = `${!inst.minecraft.version
? 'Ohne Minecraft-Version geht hier nichts.'
: 'Für Mods braucht diese Instanz einen Mod-Loader.'}
`;
const b = document.createElement('button');
b.className = 'btn btn-primary';
b.style.margin = '0 auto';
b.style.display = 'block';
b.textContent = inst.minecraft.version ? 'Mod-Loader hinzufügen' : 'Version wählen';
b.addEventListener('click', async () => {
hide('modal-mods');
const i = await window.api.getInstance(currentDetailId);
if (i) openEdit(i);
});
box.appendChild(b);
} else {
await loadCatalog(false); // beliebte Einträge sofort anzeigen
}
}
function switchModTab(which) {
const cat = which === 'catalog';
el('m-tab-catalog').classList.toggle('on', cat);
el('m-tab-installed').classList.toggle('on', !cat);
el('m-pane-catalog').classList.toggle('hidden', !cat);
el('m-pane-installed').classList.toggle('hidden', cat);
}
// gefundene Aktualisierungen: projectId -> { title, alt, neu }
let modUpdates = new Map();
async function refreshInstalled() {
const list = await window.api.modList(currentDetailId, modArt);
currentModsInstalled = new Set(list.map((m) => m.projectId));
el('m-installed-count').textContent = String(list.length);
const box = el('m-installed');
box.innerHTML = '';
if (!list.length) { box.innerHTML = 'Noch keine Mods.
'; return; }
for (const m of list) {
const up = m.projectId ? modUpdates.get(m.projectId) : null;
const aus = m.enabled === false;
const item = document.createElement('div');
item.className = 'mod-item' + (aus ? ' mod-aus' : '');
const kennzeichen = [];
if (up) kennzeichen.push('Update ');
if (aus) kennzeichen.push('aus ');
if (m.fehlt) kennzeichen.push('Datei fehlt ');
// 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.
const istOptiFine = modArt === 'mod' && /^optifine[_-]/i.test(m.filename || '');
if (istOptiFine) kennzeichen.push('OptiFine ');
item.innerHTML =
`${aus ? '🚫' : istOptiFine ? '⚡' : '🧩'}
${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}
${up
? escapeHtml(up.alt.versionNumber) + ' → ' + escapeHtml(up.neu.versionNumber)
: escapeHtml(m.versionNumber || '') + ' · ' + escapeHtml(m.filename)}
`;
const aktionen = item.querySelector('.mod-action');
// Ab-/Anschalten: die Datei bleibt liegen, Minecraft übergeht sie nur
if (!m.fehlt) {
const schalter = document.createElement('button');
schalter.className = 'btn btn-mini';
schalter.textContent = aus ? 'Einschalten' : 'Ausschalten';
schalter.title = aus
? 'Datei wieder auf .jar umbenennen'
: 'Datei auf .jar.disabled umbenennen – Minecraft lädt sie dann nicht';
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);
else toast(res.message || 'Umschalten fehlgeschlagen.', true);
await refreshInstalled();
});
aktionen.appendChild(schalter);
}
const btn = document.createElement('button');
btn.className = 'btn btn-danger btn-mini';
btn.textContent = 'Entfernen';
btn.addEventListener('click', async () => {
const tatsaechlichEntfernen = async () => {
await window.api.modRemove(currentDetailId, m.filename, modArt);
toast('Entfernt: ' + m.title);
await refreshInstalled();
};
const abhaenger = await window.api.modDependents(currentDetailId, m.filename, modArt);
if (abhaenger.length) {
const namen = abhaenger.map((n) => `„${n}"`).join(', ');
askConfirm(
'Wird noch gebraucht',
`${namen} ${abhaenger.length === 1 ? 'benötigt' : 'benötigen'} „${m.title}" als Pflicht-Abhängigkeit. `
+ 'Wird sie trotzdem entfernt, funktionieren diese Mods vermutlich nicht mehr richtig. Trotzdem entfernen?',
'Trotzdem entfernen',
tatsaechlichEntfernen,
);
} else {
await tatsaechlichEntfernen();
}
});
aktionen.appendChild(btn);
box.appendChild(item);
}
}
// ---------- Mod-Aktualisierung ----------
function setUpdateStatus(text) { el('m-update-status').textContent = text || ''; }
async function checkModUpdates() {
const btn = el('m-check-updates');
btn.disabled = true;
el('m-apply-updates').classList.add('hidden');
setUpdateStatus('Suche …');
try {
const res = await window.api.modCheckUpdates(currentDetailId, modArt);
if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Suche fehlgeschlagen.')); return; }
modUpdates = new Map((res.treffer || []).map((t) => [t.projectId, t]));
await refreshInstalled();
const teile = [];
if (!res.treffer.length) teile.push('Alles aktuell');
else teile.push(res.treffer.length + (res.treffer.length === 1 ? ' Update verfügbar' : ' Updates verfügbar'));
teile.push(res.geprueft + ' geprüft');
// von Hand abgelegte Dateien lassen sich nicht zuordnen
if (res.uebersprungen) teile.push(res.uebersprungen + ' ohne Quelle übersprungen');
setUpdateStatus(teile.join(' · '));
el('m-apply-updates').classList.toggle('hidden', !res.treffer.length);
} finally {
btn.disabled = false;
}
}
async function applyModUpdates() {
if (!modUpdates.size) return;
const btn = el('m-apply-updates');
btn.disabled = true;
setUpdateStatus('Wird aktualisiert …');
try {
const res = await window.api.modApplyUpdates(currentDetailId, [...modUpdates.keys()], modArt);
if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.')); return; }
modUpdates = new Map();
await refreshInstalled();
const n = res.erneuert.length;
const wort = n === 1 ? 'Eintrag' : 'Einträge';
setUpdateStatus(`${n} ${wort} aktualisiert`
+ (res.fehler.length ? ' · ' + res.fehler.length + ' fehlgeschlagen' : ''));
if (n) toast(`${n} ${wort} aktualisiert.`);
for (const f of res.fehler) toast(f.title + ': ' + f.message, true);
btn.classList.add('hidden');
} finally {
btn.disabled = false;
}
}
let modOffset = 0;
async function loadCatalog(append) {
const inst = await window.api.getInstance(currentDetailId);
if (!inst || !inst.minecraft.version) return;
// Loader nur für Mods verlangen
if (ART_BRAUCHT_LOADER[modArt] && inst.minecraft.loader === 'vanilla') return;
const box = el('m-results');
if (!append) { modOffset = 0; box.innerHTML = 'Lade …
'; el('m-more').classList.add('hidden'); }
const source = (el('m-source') && el('m-source').value) || 'modrinth';
if (el('m-category')) el('m-category').disabled = source === 'curseforge' || modArt !== 'mod';
try {
const res = await window.api.modSearch({
query: el('m-query').value.trim(),
gameVersion: inst.minecraft.version,
loader: inst.minecraft.loader,
category: (source === 'modrinth' && modArt === 'mod') ? el('m-category').value : '',
sort: el('m-sort').value,
limit: 30,
offset: modOffset,
art: modArt,
source,
});
if (!append) box.innerHTML = '';
if (res && (res.error || res.needKey)) {
if (!append) {
box.innerHTML = `${escapeHtml(res.error || 'Suche nicht möglich.')}
`;
if (res.needKey || /API-Key|Weiterleitung reicht/i.test(res.error || '')) {
const wrap = document.createElement('div');
wrap.className = 'mods-empty';
const btn = document.createElement('button');
btn.className = 'btn btn-mini';
btn.textContent = 'Zu den Netzwerk-Einstellungen';
btn.addEventListener('click', () => { hide('modal-mods'); openSettings('pane-network'); });
wrap.appendChild(btn);
box.appendChild(wrap);
}
}
el('m-more').classList.add('hidden');
return;
}
const hits = (res && res.hits) || [];
if (!hits.length && !append) {
box.innerHTML = 'Nichts gefunden.
';
el('m-more').classList.add('hidden');
return;
}
for (const mod of hits) box.appendChild(buildResultItem(mod));
modOffset += hits.length;
el('m-more').classList.toggle('hidden', hits.length < 30 || modOffset >= (res.total || 0));
} catch (e) {
if (!append) {
const msg = (e && e.message) ? e.message : String(e);
box.innerHTML = `Fehler: ${escapeHtml(msg)}
`;
}
}
}
function buildResultItem(mod) {
const item = document.createElement('div');
item.className = 'mod-item';
const icon = document.createElement(mod.iconUrl ? 'img' : 'div');
icon.className = 'mod-icon';
if (mod.iconUrl) {
icon.src = mod.iconUrl;
icon.addEventListener('error', () => {
const d = document.createElement('div');
d.className = 'mod-icon'; d.textContent = '🧩';
icon.replaceWith(d);
});
} else { icon.textContent = '🧩'; }
const info = document.createElement('div');
info.className = 'mod-info';
info.innerHTML =
`${escapeHtml(mod.title)}
${escapeHtml(mod.description || '')}
von ${escapeHtml(mod.author || 'unbekannt')} · ${Number(mod.downloads).toLocaleString('de-DE')} Downloads${mod.source === 'curseforge' ? ' · CurseForge' : ''}
`;
const action = document.createElement('div');
action.className = 'mod-action';
const btn = document.createElement('button');
btn.className = 'btn btn-primary btn-mini';
const already = currentModsInstalled.has(mod.projectId);
if (already) {
btn.textContent = '✓ Installiert'; btn.disabled = true;
} else {
btn.textContent = 'Installieren';
btn.addEventListener('click', (e) => { e.stopPropagation(); installMod(mod, btn, item); });
// ganze Zeile klickbar: ein Klick installiert Mod + alle Abhängigkeiten
item.classList.add('clickable');
item.title = 'Klicken zum Installieren (inkl. Abhängigkeiten)';
item.addEventListener('click', () => { if (!btn.disabled) installMod(mod, btn, item); });
}
action.appendChild(btn);
item.appendChild(icon); item.appendChild(info); item.appendChild(action);
return item;
}
async function installMod(mod, btn, item) {
btn.disabled = true; btn.textContent = 'Lädt …';
if (item) { item.classList.add('installing'); item.classList.remove('clickable'); }
const res = await window.api.modInstall(currentDetailId, { projectId: mod.projectId, title: mod.title }, modArt);
if (item) item.classList.remove('installing');
if (res.ok) {
const dep = res.depCount ? ` (+${res.depCount} Abhängigkeit${res.depCount > 1 ? 'en' : ''})` : '';
toast('Installiert: ' + mod.title + dep);
btn.textContent = '✓ Installiert';
currentModsInstalled.add(mod.projectId);
await refreshInstalled();
} else {
toast(res.message || 'Installation fehlgeschlagen.', true);
btn.disabled = false; btn.textContent = 'Installieren';
if (item) item.classList.add('clickable');
}
}
// ---------- 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));
}
// pane: optionaler Bereich, der direkt geöffnet werden soll (z. B. 'pane-network')
async function openSettings(pane) {
setSettingsPane(pane || 'pane-general');
const s = await window.api.getSettings();
el('s-theme').value = s.theme || 'dark';
el('s-global-pre').value = s.globalPreLaunch || '';
el('s-global-post').value = s.globalPostExit || '';
el('s-wrapper').value = s.wrapperCommand || '';
// Skin-Hintergrund: Auswahl aus allen angemeldeten Konten
const accs = await window.api.authAccounts();
const sel = el('s-bgskin');
sel.innerHTML = 'Aus Aktives Konto ';
for (const a of accs) {
const o = document.createElement('option');
o.value = a.id; o.textContent = a.name;
sel.appendChild(o);
}
sel.value = s.bgSkin || '';
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
el('s-bgskin-cycle').checked = !!s.bgSkinCycle;
el('s-bgskin-cycle-seconds').value = s.bgSkinCycleSeconds || 30;
sel.disabled = !!s.bgSkinCycle;
el('s-sortby').value = s.sortBy || 'lastPlayed';
el('s-iso3d').checked = s.iso3dIcons !== false;
el('s-bedrock').checked = s.bedrockEnabled !== false;
el('s-notify').checked = s.desktopNotifications !== false;
el('s-autostart').checked = !!s.autostart;
el('s-auto-java').checked = s.autoJava !== false;
el('s-autobackup').checked = !!s.autoBackupEnabled;
el('s-autobackup-every').value = s.autoBackupEvery || 'daily';
el('s-autobackup-keep').value = s.autoBackupKeep || 5;
el('s-autobackup-launch').checked = !!s.autoBackupBeforeLaunch;
el('s-autobackup-last').textContent = s.autoBackupLast
? 'Zuletzt automatisch gesichert: ' + new Date(s.autoBackupLast).toLocaleString('de-DE')
: 'Noch keine automatische Sicherung.';
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;
el('s-proxy-host').value = s.proxyHost || '';
el('s-proxy-port').value = s.proxyPort || '';
el('s-proxy-user').value = s.proxyUser || '';
el('s-proxy-pass').value = s.proxyPass || '';
el('s-cf-key').value = s.curseforgeApiKey || '';
el('s-cf-proxy').value = s.curseforgeProxyUrl || '';
el('s-cf-proxy-token').value = s.curseforgeProxyToken || '';
el('s-backup-status').className = 'hint';
el('s-backup-status').textContent = 'Noch kein Backup erstellt.';
el('s-backup-global').disabled = false;
el('s-restore-status').className = 'hint';
el('s-restore-status').textContent = 'Noch kein Backup wiederhergestellt.';
await refreshGlobalBackupOptions();
resetBackupProgressUi();
setBackupButtonsBusy(false);
el('s-instances-dir').value = await window.api.getInstancesDir();
el('s-java').value = s.javaPath || '';
el('s-min-mem').value = s.defaultMinMemMb;
el('s-max-mem').value = s.defaultMaxMemMb;
el('s-java-detected').innerHTML = '– gefundene Java-Installationen – ';
await refreshJavaStatus(s.javaPath);
const info = await window.api.appInfo();
el('s-appinfo').innerHTML = [
['Name', info.name || 'AeroMC'],
['Version', formatVersionLabel(info.version)],
['Autor', info.author || 'Nicht hinterlegt'],
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
['Launcher-Webseite', info.launcherWebsite || 'Nicht hinterlegt', info.launcherWebsite || ''],
['Lizenz', 'Nur privater, nicht-kommerzieller Gebrauch – Details anzeigen', null, 'license'],
].map(([label, value, url, action]) => {
const renderedValue = url
? `${escapeHtml(value)} `
: action
? `${escapeHtml(value)} `
: escapeHtml(value || '–');
return `${escapeHtml(label)} ${renderedValue}
`;
}).join('');
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
link.addEventListener('click', async (evt) => {
evt.preventDefault();
const url = link.dataset.url;
if (url) { window.api.openExternal(url); return; }
if (link.dataset.action === 'license') {
show('modal-license');
el('license-text').textContent = await window.api.appLicense();
}
});
}
show('modal-settings');
await renderAccounts('s-accounts'); // Konten direkt in den Einstellungen zeigen
applyBedrockVisibility(); // aktualisiert auch den Hinweistext
}
async function refreshJavaStatus(javaPath) {
const box = el('s-java-status');
box.className = 'hint';
box.textContent = 'Prüfe Java …';
const r = await window.api.detectJava(javaPath);
if (r.ok) {
box.classList.add('status-ok'); // grün, wenn Java passt
box.textContent = '✔ ' + r.version;
} else {
box.classList.add('status-bad');
box.textContent = '✖ Java nicht gefunden (' + (r.error || 'unbekannt') + ')';
}
}
async function scanJava() {
const btn = el('s-scan-java');
btn.disabled = true; btn.textContent = 'Suche …';
try {
const jvms = await window.api.scanJava();
const sel = el('s-java-detected');
sel.innerHTML = '– gefundene Java-Installationen – ';
for (const j of jvms) {
const o = document.createElement('option');
o.value = j.path;
o.textContent = `Java ${j.major} (${j.arch}) – ${j.path}`;
sel.appendChild(o);
}
toast(jvms.length ? `${jvms.length} Java-Installation(en) gefunden.` : 'Keine Java-Installation gefunden.', !jvms.length);
} catch (e) {
toast('Java-Suche fehlgeschlagen: ' + e.message, true);
} finally {
btn.disabled = false; btn.textContent = '🔍 Erkennen';
}
}
async function saveSettings() {
// globale Speicher-Vorgaben genauso prüfen wie die je Instanz
const gMin = numOrNull(el('s-min-mem').value) || 2048;
const gMax = numOrNull(el('s-max-mem').value) || 4096;
const pruefung = await window.api.checkMemory(gMin, gMax);
if (!pruefung.ok) { toast(pruefung.message, true); setSettingsPane('pane-minecraft'); return; }
if (pruefung.warnung) toast(pruefung.warnung, false);
const patch = {
theme: el('s-theme').value,
globalPreLaunch: el('s-global-pre').value.trim(),
globalPostExit: el('s-global-post').value.trim(),
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
bgSkinCycle: el('s-bgskin-cycle').checked,
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
sortBy: el('s-sortby').value,
iso3dIcons: el('s-iso3d').checked,
bedrockEnabled: el('s-bedrock').checked,
desktopNotifications: el('s-notify').checked,
autostart: el('s-autostart').checked,
autoJava: el('s-auto-java').checked,
autoBackupEnabled: el('s-autobackup').checked,
autoBackupEvery: el('s-autobackup-every').value,
autoBackupKeep: Math.min(50, Math.max(1, numOrNull(el('s-autobackup-keep').value) || 5)),
autoBackupBeforeLaunch: el('s-autobackup-launch').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,
proxyHost: el('s-proxy-host').value.trim(),
proxyPort: el('s-proxy-port').value.trim(),
proxyUser: el('s-proxy-user').value.trim(),
proxyPass: el('s-proxy-pass').value,
curseforgeApiKey: el('s-cf-key').value.trim(),
curseforgeProxyUrl: el('s-cf-proxy').value.trim(),
curseforgeProxyToken: el('s-cf-proxy-token').value.trim(),
javaPath: el('s-java').value.trim(),
defaultMinMemMb: numOrNull(el('s-min-mem').value) || 2048,
defaultMaxMemMb: numOrNull(el('s-max-mem').value) || 4096,
instancesDir: el('s-instances-dir').value.trim() || null,
};
await window.api.saveSettings(patch);
applyTheme(patch.theme);
iso3dEnabled = patch.iso3dIcons;
await applyBedrockVisibility();
await refreshSkinBackground();
hide('modal-settings');
toast('Einstellungen gespeichert.');
await reload();
}
function resetBackupProgressUi() {
['backup', 'restore'].forEach((kind) => {
clearBackupProgressTimer(kind);
backupProgressUiState[kind].shownPercent = 0;
backupProgressUiState[kind].targetPercent = 0;
backupProgressUiState[kind].detail = '';
backupProgressUiState[kind].packing = false;
el(`s-${kind}-progress`).dataset.percent = '0';
el(`s-${kind}-progress`).dataset.complete = '0';
el(`s-${kind}-progress`).classList.add('hidden');
el(`s-${kind}-progress-fill`).style.width = '0%';
el(`s-${kind}-progress-fill`).classList.remove('backup-progress-fill-busy');
el(`s-${kind}-progress-text`).textContent = '0 %';
});
}
const backupProgressActive = { backup: false, restore: false };
const backupProgressUiState = {
backup: { timer: null, shownPercent: 0, targetPercent: 0, detail: '', packing: false },
restore: { timer: null, shownPercent: 0, targetPercent: 0, detail: '', packing: false },
};
function clearBackupProgressTimer(prefix) {
const state = backupProgressUiState[prefix];
if (!state.timer) return;
clearInterval(state.timer);
state.timer = null;
}
function renderBackupProgress(prefix) {
const state = backupProgressUiState[prefix];
const box = el(`s-${prefix}-progress`);
const fill = el(`s-${prefix}-progress-fill`);
const text = el(`s-${prefix}-progress-text`);
const displayPercent = state.targetPercent >= 100 ? 100 : Math.floor(state.shownPercent);
box.dataset.percent = String(displayPercent);
box.dataset.complete = state.targetPercent >= 100 && state.shownPercent >= 100 ? '1' : '0';
box.classList.remove('hidden');
fill.classList.toggle('backup-progress-fill-busy', state.packing && state.targetPercent < 99);
fill.style.width = `${Math.max(0, Math.min(100, state.shownPercent))}%`;
text.textContent = `${displayPercent} %${state.detail ? ' · ' + state.detail : ''}`;
}
function ensureBackupProgressTimer(prefix) {
const state = backupProgressUiState[prefix];
if (state.timer) return;
state.timer = setInterval(() => {
const gap = state.targetPercent - state.shownPercent;
if (gap > 0.05) {
const step = state.packing ? Math.max(0.18, gap * 0.18) : Math.max(0.8, gap * 0.4);
state.shownPercent = Math.min(state.targetPercent, state.shownPercent + step);
} else {
state.shownPercent = state.targetPercent;
}
renderBackupProgress(prefix);
if (state.targetPercent >= 100 && state.shownPercent >= 100) clearBackupProgressTimer(prefix);
}, 120);
}
function setBackupProgressActive(mode, active) {
const prefix = mode === 'restore' ? 'restore' : 'backup';
backupProgressActive[prefix] = !!active;
}
function applyBackupProgressEvent(mode, percent, detail) {
const prefix = mode === 'restore' ? 'restore' : 'backup';
if (!backupProgressActive[prefix]) return;
updateBackupProgressUi(mode, percent, detail);
}
function setBackupButtonsBusy(busy) {
const restoreSelect = el('s-restore-backup');
const hasPlaceholderOnly = restoreSelect.options.length === 1 && !restoreSelect.options[0].value;
const canRestore = restoreSelect.options.length > 0 && !hasPlaceholderOnly && !!restoreSelect.value;
el('s-backup-global').disabled = busy;
restoreSelect.disabled = busy || !canRestore;
el('s-restore-global').disabled = busy || !canRestore;
}
function formatBackupSize(bytes) {
const value = Number(bytes) || 0;
if (value < 1024 * 1024) return `${Math.max(1, Math.round(value / 1024))} KB`;
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
}
function formatBackupModified(isoString) {
try {
return new Date(isoString).toLocaleString('de-DE');
} catch {
return isoString || '';
}
}
async function refreshGlobalBackupOptions(selectedPath) {
const select = el('s-restore-backup');
const backups = await window.api.listGlobalBackups();
select.innerHTML = '';
if (!backups.length) {
const option = document.createElement('option');
option.value = '';
option.textContent = 'Keine Backups gefunden';
select.appendChild(option);
select.disabled = true;
el('s-restore-global').disabled = true;
return [];
}
backups.forEach((backup) => {
const option = document.createElement('option');
option.value = backup.path;
option.textContent = `${backup.name} · ${formatBackupModified(backup.modifiedAt)} · ${formatBackupSize(backup.size)}`;
select.appendChild(option);
});
select.value = backups.some((backup) => backup.path === selectedPath) ? selectedPath : backups[0].path;
select.disabled = false;
el('s-restore-global').disabled = !select.value;
return backups;
}
function updateBackupProgressUi(mode, percent, detail) {
const prefix = mode === 'restore' ? 'restore' : 'backup';
const state = backupProgressUiState[prefix];
const nextPercent = Math.max(0, Math.min(100, Number(percent) || 0));
const completed = state.targetPercent >= 100;
if (nextPercent === 0) {
state.shownPercent = 0;
state.targetPercent = 0;
state.detail = detail || '';
state.packing = false;
renderBackupProgress(prefix);
return;
}
if ((completed && nextPercent < 100) || nextPercent < state.targetPercent) {
return;
}
state.targetPercent = nextPercent;
state.detail = detail || '';
state.packing = /^Packe ZIP-Archiv/.test(state.detail) && nextPercent < 99;
if (nextPercent >= 99 || (nextPercent - state.shownPercent) >= 20 || state.shownPercent === 0) {
state.shownPercent = nextPercent;
}
renderBackupProgress(prefix);
if (state.shownPercent < state.targetPercent) ensureBackupProgressTimer(prefix);
else if (state.targetPercent >= 100) clearBackupProgressTimer(prefix);
}
async function runGlobalBackup() {
const statusLine = el('s-backup-status');
setBackupButtonsBusy(true);
setBackupProgressActive('create', true);
statusLine.className = 'hint';
updateBackupProgressUi('create', 0, 'Bereite Zielordner vor');
statusLine.textContent = 'Erstelle Backup in Dokumente/AeroMC Launcher/Backups ...';
try {
const res = await window.api.createGlobalBackup();
if (res.canceled) {
statusLine.className = 'hint';
statusLine.textContent = 'Backup abgebrochen.';
resetBackupProgressUi();
setBackupProgressActive('create', false);
return;
}
if (res.ok) {
updateBackupProgressUi('create', 100, 'Backup abgeschlossen');
setBackupProgressActive('create', false);
statusLine.className = 'hint status-ok';
statusLine.textContent = 'Backup erfolgreich erstellt';
await refreshGlobalBackupOptions(res.path);
toast('Globales Backup erstellt: ' + res.path);
return;
}
setBackupProgressActive('create', false);
statusLine.className = 'hint status-bad';
statusLine.textContent = '✖ ' + (res.message || 'Backup fehlgeschlagen.');
toast(res.message || 'Backup fehlgeschlagen.', true);
} finally {
setBackupProgressActive('create', false);
setBackupButtonsBusy(false);
}
}
async function runGlobalRestore() {
const statusLine = el('s-restore-status');
const selectedBackup = el('s-restore-backup').value;
if (!selectedBackup) {
statusLine.className = 'hint status-bad';
statusLine.textContent = 'Bitte ein Backup auswählen.';
return;
}
setBackupButtonsBusy(true);
setBackupProgressActive('restore', true);
updateBackupProgressUi('restore', 0, 'Warte auf Backup-Datei');
statusLine.className = 'hint';
statusLine.textContent = 'Stelle Backup wieder her ...';
try {
const res = await window.api.restoreGlobalBackup(selectedBackup);
if (res.canceled) {
statusLine.className = 'hint';
statusLine.textContent = 'Wiederherstellen abgebrochen.';
resetBackupProgressUi();
setBackupProgressActive('restore', false);
return;
}
if (!res.ok) {
setBackupProgressActive('restore', false);
statusLine.className = 'hint status-bad';
statusLine.textContent = '✖ ' + (res.message || 'Wiederherstellen fehlgeschlagen.');
toast(res.message || 'Wiederherstellen fehlgeschlagen.', true);
return;
}
updateBackupProgressUi('restore', 100, 'Wiederherstellen abgeschlossen');
setBackupProgressActive('restore', false);
statusLine.className = 'hint status-ok';
statusLine.textContent = 'Backup erfolgreich wiederhergestellt';
const s = await window.api.getSettings();
applyTheme(s.theme || 'dark');
iso3dEnabled = s.iso3dIcons !== false;
await applyBedrockVisibility();
await refreshSkinBackground();
hide('modal-settings');
await reload();
toast('Backup wiederhergestellt: ' + res.path);
} finally {
setBackupProgressActive('restore', false);
setBackupButtonsBusy(false);
}
}
function promptGlobalRestore() {
askConfirm(
'Backup wiederherstellen',
'Das überschreibt alle aktuellen Instanzen mit dem Inhalt des Backups. Fortfahren?',
'Wiederherstellen',
runGlobalRestore,
);
}
// ---------- Microsoft-Login / Kontoverwaltung ----------
async function refreshAccountButton() {
const acc = await window.api.authStatus();
const av = el('account-av');
if (acc && acc.profile) {
av.innerHTML = '';
const img = document.createElement('img');
img.className = 'acct-head';
img.src = `https://crafthead.net/bust/${acc.profile.id}?size=64`;
img.addEventListener('error', () => { av.innerHTML = svgIcon('user'); });
av.appendChild(img);
el('account-name').textContent = acc.profile.name;
} else {
av.innerHTML = svgIcon('user');
el('account-name').textContent = 'Konten';
}
}
// aktualisiert Kontoliste im Login-Dialog und in den Einstellungen
async function refreshAllAccountLists() {
await renderAccounts('login-accounts');
if (!el('modal-settings').classList.contains('hidden')) await renderAccounts('s-accounts');
}
async function renderAccounts(boxId = 'login-accounts') {
const accounts = await window.api.authAccounts();
const box = el(boxId);
if (!box) return;
box.innerHTML = '';
if (!accounts.length) {
box.innerHTML = 'Noch kein Konto angemeldet.
';
return;
}
for (const a of accounts) {
const item = document.createElement('div');
item.className = 'account-item' + (a.active ? ' active' : '');
const av = document.createElement('img');
av.className = 'account-av';
av.src = `https://crafthead.net/avatar/${a.id}?size=64`;
av.addEventListener('error', () => {
const d = document.createElement('div');
d.className = 'account-av'; d.textContent = '🎮';
av.replaceWith(d);
});
// "spielt" nur, wenn wirklich ein Spiel dieses Kontos läuft; "aktiv" heißt
// lediglich: dieses Konto wird für den nächsten Start verwendet
const merkmale = [];
if (a.active) merkmale.push('aktiv');
if (a.playing) merkmale.push('spielt gerade');
const main = document.createElement('div');
main.className = 'account-main';
main.innerHTML =
`${escapeHtml(a.name)}
${merkmale.length ? '● ' + merkmale.join(' · ') : ''}
`;
const actions = document.createElement('div');
actions.className = 'account-actions';
item.append(av, main, actions);
if (!a.active) {
const use = document.createElement('button');
use.className = 'btn btn-mini';
use.textContent = 'Aktivieren';
use.addEventListener('click', async () => {
await window.api.authSetActive(a.id);
await refreshAllAccountLists();
await refreshAccountButton();
await refreshSkinBackground();
toast('Aktiv: ' + a.name);
});
actions.appendChild(use);
}
const rm = document.createElement('button');
rm.className = 'btn btn-danger btn-mini';
rm.textContent = 'Entfernen';
rm.addEventListener('click', async () => {
await window.api.authRemove(a.id);
await refreshAllAccountLists();
await refreshAccountButton();
toast('Entfernt: ' + a.name);
});
actions.appendChild(rm);
box.appendChild(item);
}
}
// steht wortgleich in index.html – von dort wird er beim Öffnen zurückgesetzt
const LOGIN_HINWEIS = 'Melde dich mit deinem Microsoft-Konto an (Minecraft Java Edition nötig). '
+ 'Das aktive Konto wird beim nächsten Start verwendet. Für zwei Konten gleichzeitig: '
+ 'eine Instanz starten, hier das zweite Konto aktivieren und eine weitere Instanz starten – '
+ 'laufende Spiele behalten ihr Konto.';
function resetLoginHint() {
const h = el('login-hint');
h.classList.remove('login-error');
h.textContent = LOGIN_HINWEIS;
}
async function openLogin() {
el('login-code').classList.add('hidden');
resetLoginHint();
await refreshAllAccountLists();
show('modal-login');
}
async function addAccount() {
resetLoginHint();
el('login-code').classList.remove('hidden');
el('login-usercode').textContent = '– – – –';
el('login-url').textContent = 'Lade …';
el('login-status').textContent = 'Fordere Code an …';
el('login-add').disabled = true;
try {
const res = await window.api.authLogin(); // läuft bis zur Bestätigung
el('login-code').classList.add('hidden');
if (res.ok) {
await refreshAllAccountLists();
await refreshAccountButton();
toast('Angemeldet als ' + res.account.profile.name);
} else {
const msg = res.message || 'Login fehlgeschlagen.';
el('login-hint').textContent = '✖ ' + msg;
el('login-hint').classList.add('login-error');
toast(msg, true);
}
} finally {
el('login-add').disabled = false;
}
}
// Device-Code-Ereignis aus dem Main-Prozess
function onAuthCode(code) {
loginVerificationUri = code.verificationUri;
el('login-code').classList.remove('hidden');
el('login-url').textContent = code.verificationUri;
el('login-usercode').textContent = code.userCode;
el('login-status').textContent = 'Warte auf Bestätigung im Browser …';
}
// ---------- Bestätigung ----------
let promptResolver = null;
function askPrompt({ title, text, label, value, okLabel } = {}) {
return new Promise((resolve) => {
// falls schon ein Prompt offen ist
if (promptResolver) { promptResolver(null); promptResolver = null; }
promptResolver = resolve;
el('prompt-title').textContent = title || 'Eingabe';
el('prompt-text').textContent = text || '';
el('prompt-text').classList.toggle('hidden', !text);
el('prompt-label').textContent = label || 'Wert';
el('prompt-ok').textContent = okLabel || 'OK';
const input = el('prompt-input');
input.value = value != null ? String(value) : '';
show('modal-prompt');
setTimeout(() => { input.focus(); input.select(); }, 30);
});
}
function closePrompt(wert) {
hide('modal-prompt');
const r = promptResolver;
promptResolver = null;
if (r) r(wert);
}
function askConfirm(title, text, okLabel, action) {
el('confirm-title').textContent = title;
el('confirm-text').textContent = text;
el('confirm-ok').textContent = okLabel;
confirmAction = action;
show('modal-confirm');
}
// ---------- Events ----------
function wire() {
el('btn-new').addEventListener('click', openCreate);
el('btn-new-2').addEventListener('click', openCreate);
// ohne Wrapper käme das Klick-Ereignis als Bereichsname an
el('btn-settings').addEventListener('click', () => openSettings());
el('search').addEventListener('input', render);
el('btn-open-dir').addEventListener('click', () => {
if (currentDetailId) window.api.openInstanceFolder(currentDetailId);
else if (instances[0]) window.api.openInstanceFolder(instances[0].id);
else toast('Noch keine Instanz vorhanden.', false);
});
el('btn-import').addEventListener('click', () => { openImportDialog('zip'); });
// Skin-Hintergrund an/aus (wie MultiMCs Katzen-Knopf)
el('btn-skin').addEventListener('click', async () => {
const s = await window.api.getSettings();
if (s.bgSkin || s.bgSkinCycle) {
// auch den Kontenwechsel mit ausschalten, sonst würde er sofort wieder anspringen
await window.api.saveSettings({ bgSkin: '', bgSkinCycle: false });
toast('Skin-Hintergrund aus.');
} else {
const acc = await window.api.authStatus();
if (!acc || !acc.profile) { toast('Erst ein Microsoft-Konto anmelden.', true); return; }
await window.api.saveSettings({ bgSkin: 'active' });
toast('Skin-Hintergrund an: ' + acc.profile.name);
}
await refreshSkinBackground();
});
// 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 …';
const h = await window.api.authRecheck();
showAuthHealth(h);
if (h.ok) toast('Anmelde-Dienste wieder erreichbar.');
});
// Import
document.querySelectorAll('.import-tabs .mtab').forEach((tab) => {
tab.addEventListener('click', async () => {
setImportKind(tab.dataset.kind);
if (tab.dataset.kind === 'mc') await ensureDefaultMcScan();
if (tab.dataset.kind === 'modpack') {
const box = el('mp-results');
if (box && !box.dataset.loaded) {
box.dataset.loaded = '1';
searchModpacks();
}
}
if (tab.dataset.kind === 'technic') {
const box = el('tp-results');
if (box && !box.dataset.loaded) {
box.dataset.loaded = '1';
searchTechnicPlatform();
}
}
});
});
el('import-zip-do').addEventListener('click', runZipImport);
el('import-modpack-do').addEventListener('click', runModpackImport);
el('mp-search-btn').addEventListener('click', searchModpacks);
el('mp-query').addEventListener('keydown', (e) => { if (e.key === 'Enter') searchModpacks(); });
el('mp-mc').addEventListener('keydown', (e) => { if (e.key === 'Enter') searchModpacks(); });
el('mc-pick').addEventListener('click', async () => {
const p = await window.api.mcPickFolder();
if (p) { el('mc-path').value = p; await scanMc(); }
});
el('mc-scan').addEventListener('click', scanMc);
el('mc-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanMc(); });
el('mc-all').addEventListener('change', (e) => {
document.querySelectorAll('#mc-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateMcButton();
});
el('mc-list').addEventListener('change', updateMcButton);
el('mc-do').addEventListener('click', runMcImport);
el('cf-pick').addEventListener('click', async () => {
const p = await window.api.cfPickFolder();
if (p) { el('cf-path').value = p; await scanCf(); }
});
el('cf-scan').addEventListener('click', scanCf);
el('cf-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanCf(); });
el('cf-all').addEventListener('change', (e) => {
document.querySelectorAll('#cf-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateCfButton();
});
el('cf-list').addEventListener('change', updateCfButton);
el('cf-do').addEventListener('click', runCfImport);
el('mmc-pick').addEventListener('click', async () => {
const p = await window.api.mmcPickFolder();
if (p) { el('mmc-path').value = p; await scanMmc(); }
});
el('mmc-scan').addEventListener('click', scanMmc);
el('mmc-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanMmc(); });
el('mmc-all').addEventListener('change', (e) => {
document.querySelectorAll('#mmc-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateMmcButton();
});
el('mmc-list').addEventListener('change', updateMmcButton);
el('mmc-do').addEventListener('click', runMmcImport);
el('tp-search-btn').addEventListener('click', searchTechnicPlatform);
el('tp-query').addEventListener('keydown', (e) => { if (e.key === 'Enter') searchTechnicPlatform(); });
window.api.onTechnicPlatformProgress((p) => {
el('tp-status').textContent = `Lade Mods … ${p.current}/${p.total}${p.name ? ' (' + p.name + ')' : ''}`;
});
el('technic-pick').addEventListener('click', async () => {
const p = await window.api.technicPickFolder();
if (p) { el('technic-path').value = p; await scanTechnic(); }
});
el('technic-scan').addEventListener('click', scanTechnic);
el('technic-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanTechnic(); });
el('technic-all').addEventListener('change', (e) => {
document.querySelectorAll('#technic-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateTechnicButton();
});
el('technic-list').addEventListener('change', updateTechnicButton);
el('technic-do').addEventListener('click', runTechnicImport);
window.api.onMcProgress((p) => {
el('mc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mc-progress').classList.remove('hidden');
});
window.api.onCfProgress((p) => {
el('cf-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('cf-progress').classList.remove('hidden');
});
window.api.onMmcProgress((p) => {
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mmc-progress').classList.remove('hidden');
});
window.api.onTechnicProgress((p) => {
el('technic-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('technic-progress').classList.remove('hidden');
});
// Bedrock
el('btn-bedrock').addEventListener('click', openBedrock);
el('bd-play').addEventListener('click', async () => {
const btn = el('bd-play');
// Ein Klick löst eine nicht abbrechbare App-Aktivierung aus, die etwas
// dauern kann – Button sperren, damit ein ungeduldiger zweiter Klick
// nicht zu einem doppelt geöffneten Spiel führt.
if (btn.disabled) return;
btn.disabled = true;
toast('Bedrock wird gestartet …');
try {
const res = await window.api.bedrockLaunch();
if (res && res.ok) {
if (!res.confirmed) {
toast('Start ausgelöst, konnte aber nicht bestätigt werden. Falls sich nichts öffnet, bitte erneut versuchen.');
}
} else {
toast('Start fehlgeschlagen: ' + ((res && res.message) || 'unbekannter Fehler'), true);
}
} finally {
btn.disabled = false;
}
});
el('bd-import').addEventListener('click', async () => {
const res = await window.api.bedrockInstallAddon();
if (res.canceled) return;
if (res.ok) toast('Import gestartet – Minecraft übernimmt die Installation.');
else toast(res.message || 'Import fehlgeschlagen.', true);
});
el('bd-folder').addEventListener('click', () => window.api.bedrockOpenFolder(bedrockKind));
document.querySelectorAll('.btab').forEach((t) =>
t.addEventListener('click', () => switchBedrockTab(t.dataset.kind)));
// Konsole
el('btn-console').addEventListener('click', openConsole);
el('console-clear').addEventListener('click', () => { consoleBuffer = ''; el('console-out').textContent = ''; });
el('console-copy').addEventListener('click', async () => {
try { await navigator.clipboard.writeText(consoleBuffer); toast('Log kopiert.'); }
catch { toast('Kopieren nicht möglich.', true); }
});
// Instanz-Dialog
el('btn-save-instance').addEventListener('click', saveInstance);
el('f-version-type').addEventListener('change', () => fillVersionSelect(el('f-version').value));
el('f-loader').addEventListener('change', () => updateLoaderVersions());
el('f-version').addEventListener('change', () => updateLoaderVersions());
el('f-optifine-install').addEventListener('click', async () => {
if (!editingId) return;
const btn = el('f-optifine-install');
const status = el('f-optifine-status');
btn.disabled = true;
status.textContent = 'Minecraft wird vorbereitet …';
try {
const res = await window.api.optifineSetup(editingId);
if (res.canceled) { status.textContent = ''; return; }
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Fehlgeschlagen.'); toast(res.message || 'OptiFine-Installer konnte nicht gestartet werden.', true); return; }
status.textContent = `Installer gestartet – Zielordner wurde in die Zwischenablage kopiert: ${res.sharedDir} – `
+ 'im OptiFine-Fenster als Minecraft-Ordner einfügen, „Install" klicken, danach hier bestätigen.';
el('f-optifine-confirm').classList.remove('hidden');
} finally {
btn.disabled = false;
}
});
el('f-optifine-confirm').addEventListener('click', async () => {
if (!editingId) return;
const btn = el('f-optifine-confirm');
const status = el('f-optifine-status');
btn.disabled = true;
try {
const res = await window.api.optifineConfirm(editingId);
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Nicht gefunden.'); toast(res.message || 'OptiFine-Version wurde nicht gefunden.', true); return; }
toast('OptiFine ' + res.versionId + ' übernommen.');
status.textContent = '✔ Übernommen: ' + res.versionId;
const sel = el('f-loader-version');
if (![...sel.options].some((o) => o.value === res.versionId)) {
const o = document.createElement('option');
o.value = res.versionId; o.textContent = res.versionId;
sel.appendChild(o);
}
sel.value = res.versionId;
btn.classList.add('hidden');
} finally {
btn.disabled = false;
}
});
el('f-pick-image').addEventListener('click', async () => {
const p = await window.api.pickImage();
if (!p) return;
pendingImagePath = p;
pendingClearImage = false;
const data = await window.api.fileDataUri(p);
setIconPreview(data);
});
el('f-clear-image').addEventListener('click', () => {
pendingImagePath = null;
pendingClearImage = true;
setIconPreview(null);
});
// Rechte Aktionsleiste (Instanz-Aktionen)
el('p-play').addEventListener('click', () => currentDetailId && launch(currentDetailId));
el('p-offline').addEventListener('click', () => currentDetailId && launch(currentDetailId));
el('p-shortcut').addEventListener('click', async () => {
if (!currentDetailId) return;
const inst = await window.api.getInstance(currentDetailId);
const eingabe = await askPrompt({
title: 'Verknüpfung erstellen',
text: 'Name der Desktop-Verknüpfung (ohne .lnk):',
label: 'Name',
value: inst ? inst.name : '',
okLabel: 'Erstellen',
});
if (eingabe == null) return; // abgebrochen
const res = await window.api.createShortcut(currentDetailId, eingabe.trim());
if (res.ok) toast('Verknüpfung auf dem Desktop erstellt.');
else toast(res.message || 'Verknüpfung fehlgeschlagen.', true);
});
el('p-edit').addEventListener('click', async () => { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-notes').addEventListener('click', async () => { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-groupline').addEventListener('click', async () => { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); if (inst) openEdit(inst); });
el('p-mods').addEventListener('click', openMods);
el('p-worlds').addEventListener('click', openContent);
el('p-screens').addEventListener('click', openScreens);
el('p-logs').addEventListener('click', openLogs);
el('p-servers').addEventListener('click', openServers);
el('p-modpack').addEventListener('click', openModpackUpdate);
el('mp-do').addEventListener('click', doModpackUpdate);
el('srv-add').addEventListener('click', addServer);
el('srv-ip').addEventListener('keydown', (e) => { if (e.key === 'Enter') addServer(); });
el('srv-copy-do').addEventListener('click', doServerCopy);
el('login-skin').addEventListener('click', openSkin);
el('skin-pick').addEventListener('click', waehleSkinDatei);
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
el('skin-apply').addEventListener('click', setzeSkin);
el('skin-reset').addEventListener('click', setzeSkinZurueck);
el('logs-select').addEventListener('change', (e) => ladeLog(e.target.value));
el('logs-folder').addEventListener('click', async () => {
const ok = await window.api.logsOpenFolder(currentDetailId);
if (!ok) toast('Log-Ordner nicht gefunden.', true);
});
el('logs-copy').addEventListener('click', async () => {
if (!aktuellesLog) return;
try { await navigator.clipboard.writeText(aktuellesLog); toast('Log kopiert.'); }
catch { toast('Kopieren fehlgeschlagen.', true); }
});
el('p-mcfolder').addEventListener('click', () => currentDetailId && window.api.openGameDir(currentDetailId));
el('p-configfolder').addEventListener('click', () => currentDetailId && window.api.openSubdir(currentDetailId, 'config'));
el('p-instfolder').addEventListener('click', () => currentDetailId && window.api.openInstanceFolder(currentDetailId));
el('p-export').addEventListener('click', async () => {
if (!currentDetailId) return;
const res = await window.api.exportInstance(currentDetailId);
if (res.canceled) return;
if (res.ok) toast('Exportiert: ' + res.path); else toast(res.message || 'Export fehlgeschlagen.', true);
});
el('p-copy').addEventListener('click', async () => {
if (!currentDetailId) return;
const copy = await window.api.duplicateInstance(currentDetailId);
await reload(); await selectInstance(copy.id); toast('Kopiert: ' + copy.name);
});
el('p-delete').addEventListener('click', async () => {
const ids = orderedSelectedInstanceIds();
if (!ids.length) return;
if (ids.length === 1) {
const inst = await window.api.getInstance(ids[0]);
if (!inst) return;
askConfirm('Instanz löschen',
`„${inst.name}" wird mit allen Dateien unwiderruflich gelöscht. Fortfahren?`,
'Endgültig löschen',
async () => {
await window.api.deleteInstance(ids[0]);
selectedInstanceIds.clear();
await reload();
await selectInstance(instances.length ? instances[0].id : null);
toast('Gelöscht.');
});
return;
}
const names = instances.filter((inst) => selectedInstanceIds.has(inst.id)).map((inst) => inst.name);
const preview = names.slice(0, 3).map((name) => `„${name}"`).join(', ');
const more = names.length > 3 ? ` und ${names.length - 3} weitere` : '';
askConfirm('Instanzen löschen',
`${ids.length} Instanzen (${preview}${more}) werden mit allen Dateien unwiderruflich gelöscht. Fortfahren?`,
'Endgültig löschen',
async () => {
for (const id of ids) await window.api.deleteInstance(id);
selectedInstanceIds.clear();
await reload();
await selectInstance(instances.length ? instances[0].id : null);
toast(`${ids.length} Instanzen gelöscht.`);
});
});
// Ordner-Buttons in den Inhalte-/Screenshots-Dialogen
el('content-open-worlds').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'saves'));
el('content-open-rp').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'resourcepacks'));
el('screens-open').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'screenshots'));
el('screens-select-all').addEventListener('click', () => {
document.querySelectorAll('#screens-grid .screen-thumb').forEach((t) => {
if (t.dataset.name) screenSelected.add(t.dataset.name);
});
updateScreensSelectionUi();
});
el('screens-select-none').addEventListener('click', () => {
screenSelected.clear();
updateScreensSelectionUi();
});
el('screens-delete-sel').addEventListener('click', () => deleteSelectedScreenshots());
el('content-open-dp').addEventListener('click', () => window.api.openSubdir(currentDetailId, 'datapacks'));
// Mod-Dialog
el('m-search-btn').addEventListener('click', () => loadCatalog(false));
el('m-query').addEventListener('keydown', (e) => { if (e.key === 'Enter') loadCatalog(false); });
el('m-sort').addEventListener('change', () => loadCatalog(false));
el('m-category').addEventListener('change', () => loadCatalog(false));
if (el('m-source')) el('m-source').addEventListener('change', () => loadCatalog(false));
el('m-more').addEventListener('click', () => loadCatalog(true));
el('m-tab-catalog').addEventListener('click', () => switchModTab('catalog'));
el('m-tab-installed').addEventListener('click', () => switchModTab('installed'));
el('m-art').addEventListener('change', modArtGewechselt);
el('m-open-folder').addEventListener('click', () => window.api.openGameDir(currentDetailId));
// Automatische Sicherung: unaufdringlich melden, Fehler deutlich
window.api.onAutoBackup((d) => {
if (d.ok) toast(d.anlass === 'vor-dem-start' ? 'Sicherung vor dem Start erstellt.' : 'Automatische Sicherung erstellt.');
else toast('Automatische Sicherung fehlgeschlagen: ' + (d.message || 'unbekannter Grund'), true);
});
// Absturzbericht
el('crash-copy').addEventListener('click', async () => {
if (!aktuellerCrash) return;
try { await navigator.clipboard.writeText(aktuellerCrash.text || ''); toast('Bericht kopiert.'); }
catch { toast('Kopieren fehlgeschlagen.', true); }
});
el('crash-folder').addEventListener('click', async () => {
const ok = await window.api.crashOpenFolder(currentDetailId);
if (!ok) toast('Ordner mit den Berichten nicht gefunden.', true);
});
el('crash-console').addEventListener('click', () => { hide('modal-crash'); openConsole(); });
el('m-add-file').addEventListener('click', async () => {
const res = await window.api.modAddFile(currentDetailId, modArt);
if (res.canceled) return;
if (!res.ok) { toast(res.message || 'Datei konnte nicht hinzugefügt werden.', true); return; }
const n = res.added.length;
const f = res.failed.length;
if (n) toast(`${n} Datei${n === 1 ? '' : 'en'} hinzugefügt${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0 && !n);
else if (f) toast(res.failed[0].message || 'Hinzufügen fehlgeschlagen.', true);
await refreshInstalled();
});
el('m-check-updates').addEventListener('click', checkModUpdates);
el('m-apply-updates').addEventListener('click', applyModUpdates);
window.api.onModUpdateProgress((p) => setUpdateStatus(`Suche … ${p.geprueft}/${p.gesamt} (${p.titel})`));
// Einstellungen
el('s-pick-dir').addEventListener('click', async () => {
const p = await window.api.pickFolder('Instanz-Verzeichnis wählen');
if (p) el('s-instances-dir').value = p;
});
el('s-pick-java').addEventListener('click', async () => {
const p = await window.api.pickJava();
if (p) { el('s-java').value = p; await refreshJavaStatus(p); }
});
el('s-theme').addEventListener('change', (e) => applyTheme(e.target.value));
el('s-bgskin-opacity').addEventListener('input', (e) => {
el('s-bgskin-val').textContent = e.target.value;
document.documentElement.style.setProperty('--bg-skin-opacity', String(e.target.value / 100));
});
// Konto-/Pose-Wechsel sofort anwenden (ohne erst zu speichern)
const previewSkin = async () => {
await window.api.saveSettings({
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
});
await refreshSkinBackground();
};
el('s-bgskin').addEventListener('change', previewSkin);
el('s-bgskin-pose').addEventListener('change', previewSkin);
const previewSkinCycle = async () => {
el('s-bgskin').disabled = el('s-bgskin-cycle').checked;
await window.api.saveSettings({
bgSkinCycle: el('s-bgskin-cycle').checked,
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
});
await refreshSkinBackground();
};
el('s-bgskin-cycle').addEventListener('change', previewSkinCycle);
el('s-bgskin-cycle-seconds').addEventListener('change', previewSkinCycle);
el('s-scan-java').addEventListener('click', scanJava);
el('f-java-detected').addEventListener('change', (e) => {
if (e.target.value) el('f-java').value = e.target.value;
});
el('f-java-pick').addEventListener('click', async () => {
const p = await window.api.pickJava();
if (p) { el('f-java').value = p; el('f-java-detected').value = ''; }
});
el('f-extra-args-aikar').addEventListener('click', () => {
const feld = el('f-extra-args');
const vorhanden = feld.value.trim();
if (vorhanden.includes('G1NewSizePercent')) { toast('Die GC-Flags stehen schon drin.'); return; }
feld.value = vorhanden ? vorhanden + ' ' + AIKAR_FLAGS : AIKAR_FLAGS;
toast('GC-Flags eingefügt.');
});
el('s-java-detected').addEventListener('change', async (e) => {
if (e.target.value) { el('s-java').value = e.target.value; await refreshJavaStatus(e.target.value); }
});
el('s-java').addEventListener('change', (e) => refreshJavaStatus(e.target.value.trim()));
el('s-backup-global').addEventListener('click', runGlobalBackup);
el('s-restore-global').addEventListener('click', promptGlobalRestore);
el('btn-save-settings').addEventListener('click', saveSettings);
if (el('s-cf-test')) {
el('s-cf-test').addEventListener('click', async () => {
const st = el('s-cf-test-status');
const key = el('s-cf-key').value.trim();
st.textContent = 'Prüfe bei CurseForge …';
try {
const res = await window.api.curseforgeTestKey(key);
st.textContent = (res.ok ? '✓ ' : '✖ ') + (res.message || '');
if (res.proben) {
st.textContent += ' [' + res.proben.map((p) => p.name + ':' + p.status).join(', ') + ']';
}
if (res.siehtAusWieCurseForge === false && key) {
st.textContent += ' Hinweis: Erwartet wird ein Studios-Key (cfc_pat_…) oder ein klassischer Key ($2a$10$…).';
}
} catch (err) {
st.textContent = '✖ ' + (err.message || err);
}
});
}
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('upd-open').addEventListener('click', openReleasePage);
if (el('s-check-update')) {
el('s-check-update').addEventListener('click', async () => {
renderUpdateStatus({ checking: true });
const info = await window.api.updateCheck();
renderUpdateStatus(info);
if (info.available) { hide('modal-settings'); showUpdateDialog(info); }
});
}
// Microsoft-Login / Kontoverwaltung
el('btn-account').addEventListener('click', openLogin);
el('login-add').addEventListener('click', addAccount);
el('login-url').addEventListener('click', (e) => {
e.preventDefault();
if (loginVerificationUri) window.api.openExternal(loginVerificationUri);
});
el('login-copy').addEventListener('click', async () => {
try { await navigator.clipboard.writeText(el('login-usercode').textContent.trim()); toast('Code kopiert.'); }
catch { toast('Kopieren nicht möglich.', true); }
});
// Bestätigung
el('prompt-ok').addEventListener('click', () => {
const v = el('prompt-input').value;
closePrompt(v);
});
el('prompt-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
closePrompt(el('prompt-input').value);
}
});
// Abbrechen / Schließen des Prompt-Dialogs liefert null
el('modal-prompt').querySelectorAll('[data-close]').forEach((b) => {
b.addEventListener('click', (e) => {
e.stopPropagation();
closePrompt(null);
});
});
el('modal-prompt').addEventListener('mousedown', (e) => {
if (e.target === el('modal-prompt')) closePrompt(null);
});
el('confirm-ok').addEventListener('click', async () => {
hide('modal-confirm');
if (confirmAction) { const a = confirmAction; confirmAction = null; await a(); }
});
// generisches Schließen
document.querySelectorAll('[data-close]').forEach((b) =>
b.addEventListener('click', () => {
const m = b.closest('.modal');
if (!m) return;
if (m.id === 'modal-prompt') { closePrompt(null); return; }
m.classList.add('hidden');
}));
document.querySelectorAll('.modal').forEach((m) =>
m.addEventListener('mousedown', (e) => {
if (e.target !== m) return;
if (m.id === 'modal-prompt') { closePrompt(null); return; }
m.classList.add('hidden');
}));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (!el('ctx-menu').classList.contains('hidden')) { closeContextMenu(); return; }
if (!el('modal-prompt').classList.contains('hidden')) { closePrompt(null); return; }
const open = [...document.querySelectorAll('.modal:not(.hidden)')].pop();
if (open) open.classList.add('hidden');
return;
}
const strg = e.ctrlKey || e.metaKey;
const modalOffen = document.querySelector('.modal:not(.hidden)');
const inEingabe = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)
|| document.activeElement.isContentEditable;
// Strg+F: Suche fokussieren – nicht wenn gerade ein Dialog offen ist,
// der eine eigene Suche hat (Mods, Modpack-Katalog, Import …)
if (strg && e.key.toLowerCase() === 'f' && !modalOffen) {
e.preventDefault();
el('search').focus();
el('search').select();
return;
}
// Strg+N: neue Instanz anlegen
if (strg && e.key.toLowerCase() === 'n' && !modalOffen) {
e.preventDefault();
el('btn-new').click();
return;
}
// Entf: markierte Instanz(en) löschen – nicht während man irgendwo tippt
if (e.key === 'Delete' && !modalOffen && !inEingabe) {
e.preventDefault();
el('p-delete').click();
}
});
wireFileDrop();
}
// ---------- Datei-Drag&Drop aufs Fenster (.jar/.mrpack/.zip) ----------
function wireFileDrop() {
let dragDepth = 0; // dragenter/-leave feuern auch für Kindelemente
const istDatei = (e) => e.dataTransfer && [...e.dataTransfer.types].includes('Files');
window.addEventListener('dragenter', (e) => {
if (!istDatei(e)) return;
e.preventDefault();
dragDepth++;
document.body.classList.add('file-drop-active');
});
window.addEventListener('dragover', (e) => {
if (!istDatei(e)) return;
e.preventDefault();
});
window.addEventListener('dragleave', (e) => {
if (!istDatei(e)) return;
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) document.body.classList.remove('file-drop-active');
});
window.addEventListener('drop', async (e) => {
if (!istDatei(e)) return;
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);
});
}
async function handleDroppedFile(filePath) {
const ext = ('.' + filePath.split('.').pop()).toLowerCase();
if (!['.jar', '.mrpack', '.zip'].includes(ext)) {
toast('Nicht unterstützter Dateityp: ' + filePath.split(/[\\/]/).pop(), true);
return;
}
// Für .jar: läuft der Mods-Dialog gerade, zählt die dort gewählte
// Inhaltsart (Mod/Ressourcenpaket/Shader) – sonst landet es als Mod
// direkt in der aktuell ausgewählten Instanz.
const modsOpen = !el('modal-mods').classList.contains('hidden');
const targetArt = modsOpen ? modArt : 'mod';
const res = await window.api.filesDrop(filePath, currentDetailId, targetArt);
if (!res.ok) { toast(res.message || 'Import fehlgeschlagen.', true); return; }
if (res.kind === 'mod') {
toast(`Datei hinzugefügt: ${res.added[0]}`);
if (modsOpen) await refreshInstalled();
return;
}
await reload();
if (res.instance) {
toast(`Instanz übernommen: ${res.instance.name}`);
await selectInstance(res.instance.id);
} else {
toast('Import abgeschlossen.');
}
}
window.addEventListener('DOMContentLoaded', async () => {
wire();
applyIcons();
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 refreshSkinBackground();
window.api.onLaunchProgress(onLaunchEvt);
window.api.onBackupProgress((p) => applyBackupProgressEvent(p.mode, p.percent || 0, p.detail || ''));
window.api.onAuthCode(onAuthCode);
window.api.onAuthRefreshed(async () => { await refreshAccountButton(); await refreshSkinBackground(); });
window.api.onAuthHealth(showAuthHealth);
// Spiel gestartet oder beendet -> "spielt gerade" in den Kontenlisten nachziehen
window.api.onAccountsChanged(() => { refreshAllAccountLists(); });
// über eine Desktop-Verknüpfung gestartet -> Instanz auswählen und anwerfen
// (bei einer Server-Verknüpfung zusätzlich direkt mit dem Server verbinden)
window.api.onLaunchRequest(async (payload) => {
const id = payload && typeof payload === 'object' ? payload.id : payload;
const serverIp = payload && typeof payload === 'object' ? payload.serverIp : null;
await selectInstance(id);
launch(id, null, serverIp ? { quickPlayServer: serverIp } : null);
});
// Empfänger steht jetzt – dem Hauptprozess Bescheid geben, damit ein über
// eine Verknüpfung mitgegebener Startwunsch sicher zugestellt werden kann
window.api.notifyReady();
window.api.onUpdateAvailable(showUpdateDialog);
window.api.onUpdateProgress(onUpdateProgress);
window.api.onUpdateChecked(renderUpdateStatus);
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
await loadMcIcons(); // echte Minecraft-Icons bereitstellen
await applyBedrockVisibility();
await refreshAccountButton();
await reload();
// zuletzt gewählte Instanz wieder markieren (sonst die zuletzt gespielte)
if (instances.length) {
let start = null;
try {
const s = await window.api.getSettings();
if (s.lastSelectedId && instances.some((i) => i.id === s.lastSelectedId)) start = s.lastSelectedId;
} catch { /* Einstellungen nicht lesbar */ }
await selectInstance(start || instances[0].id); // instances ist nach lastPlayed sortiert
}
});