4433 lines
180 KiB
JavaScript
4433 lines
180 KiB
JavaScript
'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 += `<rect x="${offsetX + (x * scale)}" y="${offsetY + (y * scale)}" width="${scale}" height="${scale}" fill="${col}"/>`;
|
||
}
|
||
});
|
||
return rects;
|
||
}
|
||
|
||
function pixelIconSvg(key, size) {
|
||
const rects = pixelIconRects(key);
|
||
return `<svg viewBox="0 0 16 16" width="${size}" height="${size}">${rects}</svg>`;
|
||
}
|
||
|
||
const HD_ICON_THEMES = {
|
||
grass: { glyph: 'loop', bg: '#46b96c', shade: '#1f5b33', glow: '#95f08a' },
|
||
dirt: { glyph: 'cube', bg: '#a96d43', shade: '#58311a', glow: '#d9a56e' },
|
||
stone: { glyph: 'cube', bg: '#98a3b3', shade: '#495364', glow: '#dfe5ef' },
|
||
planks: { glyph: 'cube', bg: '#cf9b61', shade: '#704521', glow: '#f2cb97' },
|
||
tnt: { glyph: 'crate', bg: '#db4d45', shade: '#6d1d18', glow: '#fff2d2' },
|
||
obsidian: { glyph: 'cube', bg: '#6f5cd7', shade: '#25184d', glow: '#b8abff' },
|
||
netherrack: { glyph: 'cube', bg: '#c14f4f', shade: '#5d1717', glow: '#ff9a7b' },
|
||
crafting: { glyph: 'grid', bg: '#bf8750', shade: '#613a20', glow: '#f5d5a4' },
|
||
furnace: { glyph: 'forge', bg: '#7e8997', shade: '#38414c', glow: '#ffbd67' },
|
||
bookshelf: { glyph: 'books', bg: '#b77a4d', shade: '#5b331f', glow: '#ffd966' },
|
||
diamondblock: { glyph: 'cube', bg: '#42d0d7', shade: '#14606d', glow: '#dfffff' },
|
||
goldblock: { glyph: 'cube', bg: '#f0bf3c', shade: '#8b5a09', glow: '#fff1a1' },
|
||
emeraldblock: { glyph: 'cube', bg: '#2ec97b', shade: '#0b5a35', glow: '#b4ffd7' },
|
||
redstoneblock: { glyph: 'cube', bg: '#e14d51', shade: '#75191d', glow: '#ffb1b4' },
|
||
diamond: { glyph: 'gem', bg: '#47dce0', shade: '#126b79', glow: '#defcff' },
|
||
emerald: { glyph: 'gem', bg: '#2fd485', shade: '#0d683c', glow: '#d8ffe8' },
|
||
gold: { glyph: 'ingot', bg: '#f2c44e', shade: '#8d6208', glow: '#fff3b4' },
|
||
iron: { glyph: 'ingot', bg: '#b9c3d1', shade: '#57606d', glow: '#f5f8fc' },
|
||
redstone: { glyph: 'spark', bg: '#ea5956', shade: '#7e1717', glow: '#ffc8c6' },
|
||
coal: { glyph: 'coal', bg: '#59606b', shade: '#1f242b', glow: '#cfd5df' },
|
||
sword: { glyph: 'sword', bg: '#5d9ff4', shade: '#1f477c', glow: '#eef6ff' },
|
||
pickaxe: { glyph: 'pickaxe', bg: '#57a8de', shade: '#1f5276', glow: '#ebf8ff' },
|
||
axe: { glyph: 'axe', bg: '#6997df', shade: '#27456c', glow: '#eef4ff' },
|
||
bow: { glyph: 'bow', bg: '#b97a58', shade: '#5c3522', glow: '#ffe1cb' },
|
||
apple: { glyph: 'apple', bg: '#ea5353', shade: '#7e1717', glow: '#ffd3d3' },
|
||
goldenapple: { glyph: 'apple', bg: '#f0c44f', shade: '#8c6208', glow: '#fff1b2' },
|
||
book: { glyph: 'book', bg: '#7a5ad3', shade: '#33205f', glow: '#ece4ff' },
|
||
enchantedbook: { glyph: 'bookSpark', bg: '#6f63e7', shade: '#272266', glow: '#d9fcff' },
|
||
map: { glyph: 'map', bg: '#5aa67d', shade: '#1d563f', glow: '#f0f1c9' },
|
||
star: { glyph: 'star', bg: '#5f7ce0', shade: '#22357c', glow: '#ffffff' },
|
||
endereye: { glyph: 'eye', bg: '#3da984', shade: '#124a40', glow: '#ccfff0' },
|
||
enderpearl: { glyph: 'pearl', bg: '#4cc0c7', shade: '#155b62', glow: '#e6ffff' },
|
||
blaze: { glyph: 'flame', bg: '#ef8b38', shade: '#7c3a06', glow: '#ffe3a3' },
|
||
xp: { glyph: 'bottle', bg: '#3ea977', shade: '#15503a', glow: '#dcffe0' },
|
||
cake: { glyph: 'cake', bg: '#f08cab', shade: '#8a3453', glow: '#fff6dc' },
|
||
bucket: { glyph: 'bucket', bg: '#5b94dc', shade: '#1f4a79', glow: '#d8f1ff' },
|
||
lava: { glyph: 'bucket', bg: '#ec7134', shade: '#862d08', glow: '#ffd4ab' },
|
||
tnt_item: { glyph: 'powder', bg: '#9ba2af', shade: '#404753', glow: '#fff4ce' },
|
||
creeper: { glyph: 'creeper', bg: '#67c35b', shade: '#234f1c', glow: '#d7ffd0' },
|
||
chest: { glyph: 'chest', bg: '#c4884f', shade: '#653a1d', glow: '#f5d28f' },
|
||
potion: { glyph: 'potion', bg: '#8d63d9', shade: '#3b2466', glow: '#ffd7f5' },
|
||
nether: { glyph: 'flame', bg: '#d65a3b', shade: '#6b2111', glow: '#ffd1a0' },
|
||
end: { glyph: 'eye', bg: '#3f8d7d', shade: '#153f39', glow: '#d6fff4' },
|
||
};
|
||
|
||
const LEGACY_REAL_ICON_MAP = {
|
||
chest: 'chest',
|
||
creeper: 'creeper',
|
||
potion: 'potion',
|
||
nether: 'blaze',
|
||
end: 'endereye',
|
||
tnt_item: 'tnt_item',
|
||
};
|
||
|
||
function hdIconTheme(key) {
|
||
return HD_ICON_THEMES[key] || { glyph: 'letter', bg: '#5b6474', shade: '#252b34', glow: '#e8edf7' };
|
||
}
|
||
|
||
function hdIconGlyph(key, theme) {
|
||
switch (theme.glyph) {
|
||
case 'cube':
|
||
return `
|
||
<path d="M32 18 44 24 32 30 20 24Z"/>
|
||
<path d="M20 24v14l12 7 12-7V24"/>
|
||
<path d="M32 30v15"/>
|
||
`;
|
||
case 'loop':
|
||
return `
|
||
<path d="M22 33c0-4 3-7 7-7 5 0 7 6 11 6 3 0 6-3 6-7s-3-7-7-7c-5 0-7 6-11 6-3 0-6-3-6-7s3-7 7-7"/>
|
||
`;
|
||
case 'crate':
|
||
return `
|
||
<rect x="19" y="20" width="26" height="24" rx="5"/>
|
||
<path d="M19 28h26M32 20v24M24 24h16M24 40h16"/>
|
||
`;
|
||
case 'grid':
|
||
return `
|
||
<rect x="18" y="18" width="28" height="28" rx="7"/>
|
||
<path d="M27 18v28M37 18v28M18 27h28M18 37h28"/>
|
||
`;
|
||
case 'forge':
|
||
return `
|
||
<rect x="18" y="18" width="28" height="28" rx="7"/>
|
||
<path d="M25 27h14v9H25zM24 40h16"/>
|
||
`;
|
||
case 'books':
|
||
return `
|
||
<rect x="18" y="17" width="9" height="30" rx="3"/>
|
||
<rect x="28" y="15" width="8" height="32" rx="3"/>
|
||
<rect x="37" y="19" width="9" height="28" rx="3"/>
|
||
`;
|
||
case 'gem':
|
||
return `
|
||
<path d="M32 17 45 29 32 46 19 29Z"/>
|
||
<path d="M24 22h16M20 29h24M32 17v29"/>
|
||
`;
|
||
case 'ingot':
|
||
return `
|
||
<path d="M22 26h20l5 12H17Z"/>
|
||
<path d="M26 21h12l5 5H21Z"/>
|
||
`;
|
||
case 'spark':
|
||
return `
|
||
<path d="M32 17 35 26 45 28 37 34 39 44 32 39 25 44 27 34 19 28 29 26Z"/>
|
||
`;
|
||
case 'coal':
|
||
return `
|
||
<path d="M24 19h16l7 11-7 15H24l-7-15Z"/>
|
||
<path d="M24 19 32 30 40 19"/>
|
||
`;
|
||
case 'sword':
|
||
return `
|
||
<path d="M37 18 46 27 35 38 26 29Z"/>
|
||
<path d="M28 31 18 41M24 45l-4 4M27 42l-7 2"/>
|
||
`;
|
||
case 'pickaxe':
|
||
return `
|
||
<path d="M19 26c7-7 16-9 26-6"/>
|
||
<path d="M33 20 22 45"/>
|
||
`;
|
||
case 'axe':
|
||
return `
|
||
<path d="M25 19c8 0 15 5 17 13-8 2-14-1-18-8"/>
|
||
<path d="M31 19 22 46"/>
|
||
`;
|
||
case 'bow':
|
||
return `
|
||
<path d="M24 18c9 8 9 20 0 28"/>
|
||
<path d="M39 18v28M27 22 39 32 27 42"/>
|
||
`;
|
||
case 'apple':
|
||
return `
|
||
<path d="M32 23c7 0 13 5 13 12s-6 13-13 13-13-6-13-13 6-12 13-12Z"/>
|
||
<path d="M32 19c0-3 2-5 5-6M36 18c3 0 5 1 7 3"/>
|
||
`;
|
||
case 'book':
|
||
return `
|
||
<path d="M20 19h12c3 0 5 2 5 5v21H24c-2 0-4 1-4 2Z"/>
|
||
<path d="M44 19H32c-3 0-5 2-5 5v21h13c2 0 4 1 4 2Z"/>
|
||
`;
|
||
case 'bookSpark':
|
||
return `
|
||
<path d="M20 19h12c3 0 5 2 5 5v21H24c-2 0-4 1-4 2Z"/>
|
||
<path d="M44 19H32c-3 0-5 2-5 5v21h13c2 0 4 1 4 2Z"/>
|
||
<path d="M45 17 47 22 52 24 47 26 45 31 43 26 38 24 43 22Z"/>
|
||
`;
|
||
case 'map':
|
||
return `
|
||
<path d="M18 20 28 16 38 20 46 17v27l-8 3-10-4-10 4Z"/>
|
||
<path d="M28 16v27M38 20v27"/>
|
||
`;
|
||
case 'star':
|
||
return `
|
||
<path d="M32 17 36 26 46 27 38 34 40 44 32 39 24 44 26 34 18 27 28 26Z"/>
|
||
`;
|
||
case 'eye':
|
||
return `
|
||
<path d="M16 32c5-8 10-12 16-12s11 4 16 12c-5 8-10 12-16 12s-11-4-16-12Z"/>
|
||
<circle cx="32" cy="32" r="5"/>
|
||
`;
|
||
case 'pearl':
|
||
return `
|
||
<circle cx="32" cy="32" r="13"/>
|
||
<path d="M26 27c2-2 4-3 7-3"/>
|
||
`;
|
||
case 'flame':
|
||
return `
|
||
<path d="M34 17c5 8 9 12 9 19 0 7-4 13-11 13s-11-5-11-12c0-7 5-11 10-17 0 4 1 6 4 10 2-6 1-9-1-13Z"/>
|
||
`;
|
||
case 'bottle':
|
||
return `
|
||
<path d="M28 18h8v6l4 6v12c0 5-3 8-8 8s-8-3-8-8V30l4-6Z"/>
|
||
<path d="M25 39h14"/>
|
||
`;
|
||
case 'cake':
|
||
return `
|
||
<path d="M18 30h28v14H18Z"/>
|
||
<path d="M18 30c3-4 7-6 14-6s11 2 14 6M26 24v6M32 23v7M38 24v6"/>
|
||
`;
|
||
case 'bucket':
|
||
return `
|
||
<path d="M22 22h20l-3 22H25Z"/>
|
||
<path d="M26 22c0-4 2-7 6-7s6 3 6 7"/>
|
||
`;
|
||
case 'powder':
|
||
return `
|
||
<circle cx="24" cy="34" r="4"/>
|
||
<circle cx="33" cy="28" r="5"/>
|
||
<circle cx="41" cy="36" r="3"/>
|
||
`;
|
||
case 'creeper':
|
||
return `
|
||
<rect x="21" y="18" width="22" height="28" rx="6"/>
|
||
<rect x="25" y="24" width="4" height="5" rx="1" fill="currentColor" stroke="none"/>
|
||
<rect x="35" y="24" width="4" height="5" rx="1" fill="currentColor" stroke="none"/>
|
||
<path d="M26 40h12l-2-7h-8Z" fill="currentColor" stroke="none"/>
|
||
`;
|
||
case 'chest':
|
||
return `
|
||
<rect x="18" y="22" width="28" height="22" rx="5"/>
|
||
<path d="M18 29h28M32 29v8"/>
|
||
`;
|
||
case 'potion':
|
||
return `
|
||
<path d="M28 17h8v5l4 6v13c0 6-3 9-8 9s-8-3-8-9V28l4-6Z"/>
|
||
<path d="M25 39h14"/>
|
||
`;
|
||
case 'letter':
|
||
return `<text x="32" y="40" text-anchor="middle" font-size="22" font-weight="700" font-family="Segoe UI, sans-serif" fill="currentColor" stroke="none">${escapeHtml((key || '?').slice(0, 1).toUpperCase())}</text>`;
|
||
default:
|
||
return pixelIconSvg(key, 44);
|
||
}
|
||
}
|
||
|
||
function hdIconSvg(key, size = 44) {
|
||
const theme = hdIconTheme(key);
|
||
const gid = `g-${String(key || 'icon').replace(/[^a-z0-9_-]/gi, '-')}-${size}`;
|
||
return `
|
||
<svg viewBox="0 0 64 64" width="${size}" height="${size}" aria-hidden="true">
|
||
<defs>
|
||
<clipPath id="${gid}-clip">
|
||
<rect x="8" y="8" width="48" height="48" rx="12"/>
|
||
</clipPath>
|
||
<filter id="${gid}-shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="rgba(0,0,0,.28)"/>
|
||
</filter>
|
||
</defs>
|
||
<g filter="url(#${gid}-shadow)">
|
||
<g clip-path="url(#${gid}-clip)">${pixelIconRects('grass', 3, 8, 8)}</g>
|
||
<rect x="8" y="8" width="48" height="48" rx="12" fill="rgba(18,28,16,.10)"/>
|
||
<rect x="8" y="8" width="48" height="48" rx="12" fill="none" stroke="rgba(255,255,255,.18)" stroke-width="1.5"/>
|
||
</g>
|
||
<g fill="none" stroke="${theme.glow}" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">
|
||
${hdIconGlyph(key, theme)}
|
||
</g>
|
||
</svg>`;
|
||
}
|
||
|
||
const ICON_KEYS = Object.keys(PIX);
|
||
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)
|
||
? `<rect x="8" y="8" width="48" height="48" rx="15" fill="${theme.bg}" opacity=".16"/>`
|
||
: `<circle cx="32" cy="32" r="22" fill="${theme.bg}" opacity=".14"/>`;
|
||
return `
|
||
<svg class="mc-item-icon" viewBox="0 0 64 64" width="${size}" height="${size}" aria-hidden="true">
|
||
${plate}
|
||
<image href="${src}" x="10" y="10" width="44" height="44" preserveAspectRatio="xMidYMid meet"/>
|
||
</svg>`;
|
||
}
|
||
|
||
// erzeugt die Würfel-Darstellungen einmalig im Voraus
|
||
async function buildIsoCubes() {
|
||
const jobs = Object.values(mcIcons)
|
||
.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 = '<div class="mods-empty">Suche läuft …</div>';
|
||
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 = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
|
||
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 = '<div class="mods-empty">Keine Modpacks gefunden.</div>';
|
||
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 =
|
||
`<div class="mod-icon">${h.iconUrl ? `<img src="${escapeHtml(h.iconUrl)}" alt="" style="width:32px;height:32px;border-radius:4px;object-fit:cover" />` : '📦'}</div>
|
||
<div class="mod-info">
|
||
<div class="mod-name">${escapeHtml(h.title || h.slug || h.projectId)}</div>
|
||
<div class="bd-meta">${escapeHtml(meta)}${cats ? ' · ' + escapeHtml(cats) : ''}</div>
|
||
<div class="bd-meta" style="opacity:.85">${escapeHtml((h.description || '').slice(0, 120))}${(h.description || '').length > 120 ? '…' : ''}</div>
|
||
</div>
|
||
<div class="mod-action"></div>`;
|
||
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 = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
|
||
}
|
||
}
|
||
|
||
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 = '<div class="mods-empty">Keine Profile gefunden.</div>'; 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 =
|
||
`<input type="checkbox" data-idx="${idx}" checked />
|
||
<div class="mod-info">
|
||
<div class="mmc-name">${escapeHtml(inst.name)}</div>
|
||
<div class="mmc-meta">${escapeHtml(meta)}</div>
|
||
</div>`;
|
||
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 = '<div class="mods-empty">Keine Instanzen gefunden.</div>'; 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 =
|
||
`<input type="checkbox" data-idx="${idx}" checked />
|
||
<div class="mod-info">
|
||
<div class="mmc-name">${escapeHtml(inst.name)}</div>
|
||
<div class="mmc-meta">${escapeHtml(meta)}</div>
|
||
</div>`;
|
||
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 = '<div class="mods-empty">Keine Instanzen gefunden.</div>'; 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 =
|
||
`<input type="checkbox" data-idx="${idx}" checked />
|
||
<div class="mod-info">
|
||
<div class="mmc-name">${escapeHtml(inst.name)}</div>
|
||
<div class="mmc-meta">${escapeHtml(meta)}</div>
|
||
</div>`;
|
||
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 = '<div class="mods-empty">Suche läuft …</div>';
|
||
const res = await window.api.technicPlatformSearch(query);
|
||
if (!res.ok) {
|
||
st.textContent = 'Fehler: ' + (res.message || 'Suche fehlgeschlagen.');
|
||
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
|
||
return;
|
||
}
|
||
const hits = res.hits || [];
|
||
st.textContent = hits.length ? `${hits.length} Treffer` : 'Keine Treffer.';
|
||
box.innerHTML = '';
|
||
if (!hits.length) { box.innerHTML = '<div class="mods-empty">Keine Modpacks gefunden.</div>'; return; }
|
||
for (const h of hits) {
|
||
const item = document.createElement('div');
|
||
item.className = 'mod-item';
|
||
item.innerHTML =
|
||
`<div class="mod-icon">${h.iconUrl ? `<img src="${escapeHtml(h.iconUrl)}" alt="" style="width:32px;height:32px;border-radius:4px;object-fit:cover" />` : '📦'}</div>
|
||
<div class="mod-info"><div class="mod-name">${escapeHtml(h.name)}</div></div>
|
||
<div class="mod-action"></div>`;
|
||
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 = '<div class="mods-empty">Keine Modpacks gefunden.</div>'; 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 =
|
||
`<input type="checkbox" data-idx="${idx}" checked />
|
||
<div class="mod-info">
|
||
<div class="mmc-name">${escapeHtml(inst.name)}</div>
|
||
<div class="mmc-meta">${escapeHtml(meta)}</div>
|
||
</div>`;
|
||
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 = '<div class="mods-empty">Lade …</div>';
|
||
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 = `<div class="mods-empty">${leer[kind] || 'Nichts vorhanden.'}</div>`;
|
||
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 =
|
||
`<div class="mod-icon">${kind === 'worlds' ? '🌍' : kind === 'screenshots' ? '🖼' : '📦'}</div>
|
||
<div class="mod-info">
|
||
<div class="mod-name">${escapeHtml(name)}</div>
|
||
<div class="bd-meta">${escapeHtml(meta)}</div>
|
||
</div>
|
||
<div class="mod-action"></div>`;
|
||
|
||
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: '<path d="M12 5v14M5 12h14"/>',
|
||
import: '<path d="M12 3v12m0 0l-4-4m4 4l4-4M4 20h16"/>',
|
||
package: '<path d="M21 8l-9-5-9 5 9 5 9-5zM3 8v8l9 5 9-5V8M12 13v8"/>',
|
||
folder: '<path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>',
|
||
terminal: '<path d="M4 5h16v14H4zM7 9l3 3-3 3M13 16h4"/>',
|
||
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/>',
|
||
settings: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/>',
|
||
play: '<path d="M7 5l12 7-12 7z"/>',
|
||
blocks: '<path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/>',
|
||
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 010 18M12 3a15 15 0 000 18"/>',
|
||
image: '<path d="M4 5h16v14H4z"/><path d="M8.5 11a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM4 17l5-5 4 4 3-3 4 4"/>',
|
||
edit: '<path d="M4 20h4L18 10l-4-4L4 16v4zM14 6l4 4"/>',
|
||
copy: '<path d="M9 9h10v10H9zM5 15V5h10"/>',
|
||
export: '<path d="M12 21V9m0 0l-4 4m4-4l4 4M4 4h16"/>',
|
||
trash: '<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>',
|
||
};
|
||
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 `<svg viewBox="0 0 24 24" ${attrs}>${p}</svg>`;
|
||
}
|
||
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 = `<img src="${iconData}" alt="" />`;
|
||
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
|
||
? `<img class="card-icon-img" src="${inst.iconData}" alt="" />`
|
||
: `<span class="card-icon">${iconOf(inst.icon, 60)}</span>`;
|
||
// Tooltip: Name, Version/Loader und – falls vorhanden – die Notizen
|
||
const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
|
||
? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : '';
|
||
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}
|
||
<div class="card-name">${escapeHtml(inst.name)}</div>`;
|
||
|
||
// 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 = `<span class="chev">${collapsedGroups.has(g) ? '▸' : '▾'}</span> ${escapeHtml(g || 'Ohne Gruppe')} <span class="group-count">(${list.length})</span>`;
|
||
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) => `<option value="${escapeHtml(g)}"></option>`).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 = '<option value="">neueste (empfohlen)</option>';
|
||
|
||
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 = '<option value="">– keine gewählt –</option>';
|
||
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 = `<img src="${dataUri}" alt="" />`;
|
||
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 = '<option value="">– gefundene Java-Installationen –</option>';
|
||
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 ? `<div class="crash-zeile"><div class="crash-titel">${t}</div><div class="crash-wert">${escapeHtml(w)}</div></div>` : '');
|
||
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 = '<div class="mods-empty">Lade …</div>';
|
||
const res = await window.api.serversList(currentDetailId);
|
||
box.innerHTML = '';
|
||
|
||
if (!res.ok) { box.innerHTML = `<div class="mods-empty">${escapeHtml(res.message || 'Liste nicht lesbar.')}</div>`; return; }
|
||
if (!res.server.length) {
|
||
box.innerHTML = '<div class="mods-empty">Noch keine Server eingetragen.</div>';
|
||
return;
|
||
}
|
||
|
||
for (const s of res.server) {
|
||
const item = document.createElement('div');
|
||
item.className = 'mod-item';
|
||
item.innerHTML =
|
||
`<div class="mod-icon">🖥</div>
|
||
<div class="mod-info">
|
||
<div class="mod-name">${escapeHtml(s.name || s.ip)}</div>
|
||
<div class="mod-meta">${escapeHtml(s.ip)}</div>
|
||
<div class="mod-meta srv-status hint">Wird geprüft …</div>
|
||
</div>
|
||
<div class="mod-action"></div>`;
|
||
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 = '<div class="mods-empty">Es gibt keine weitere Instanz.</div>';
|
||
}
|
||
for (const i of andere) {
|
||
const zeile = document.createElement('label');
|
||
zeile.className = 'checkbox-field';
|
||
zeile.style.padding = '6px 2px';
|
||
zeile.innerHTML = `<input type="checkbox" value="${escapeHtml(i.id)}" /> <span>${escapeHtml(i.name)}</span>`;
|
||
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 <img> 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
|
||
? `<div class="crash-zeile"><div class="crash-titel">${titel}</div><div class="crash-wert">${escapeHtml(wert)}</div></div>`
|
||
: '';
|
||
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 += `<div class="crash-zeile"><div class="crash-titel">Aufrufverlauf</div>`
|
||
+ `<div class="crash-wert"><div class="crash-verlauf">${escapeHtml(res.verlauf.join('\n'))}</div></div></div>`;
|
||
}
|
||
el('crash-kern').innerHTML = html || '<div class="hint">Der Bericht enthält keine der üblichen Kennzeichen.</div>';
|
||
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 = '<div class="screens-empty">Lade …</div>';
|
||
screenSelected = new Set();
|
||
updateScreensSelectionUi();
|
||
show('modal-screens');
|
||
|
||
const shots = await window.api.screenshots(currentDetailId);
|
||
grid.innerHTML = '';
|
||
if (!shots.length) {
|
||
grid.innerHTML = '<div class="screens-empty">Keine Screenshots vorhanden.</div>';
|
||
updateScreensSelectionUi();
|
||
return;
|
||
}
|
||
for (const s of shots) {
|
||
const thumb = document.createElement('div');
|
||
thumb.className = 'screen-thumb';
|
||
thumb.dataset.name = s.name;
|
||
thumb.innerHTML =
|
||
`<input type="checkbox" class="screen-check" title="Auswählen" />
|
||
<img alt="" /><div class="screen-name">${escapeHtml(s.name)}</div>
|
||
<button class="screen-del" title="Löschen">🗑</button>`;
|
||
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 = '<div class="mods-empty">Nichts vorhanden.</div>'; return; }
|
||
for (const it of items) {
|
||
const item = document.createElement('div');
|
||
item.className = 'mod-item';
|
||
item.innerHTML =
|
||
`<div class="mod-icon">${emoji}</div>
|
||
<div class="mod-info"><div class="mod-name">${escapeHtml(it.name)}</div></div>
|
||
<div class="mod-action"></div>`;
|
||
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 = `<div class="mods-empty">${!inst.minecraft.version
|
||
? 'Ohne Minecraft-Version geht hier nichts.'
|
||
: 'Für Mods braucht diese Instanz einen Mod-Loader.'}</div>`;
|
||
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 = '<div class="mods-empty">Noch keine Mods.</div>'; 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('<span class="mod-new">Update</span>');
|
||
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
|
||
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
|
||
// OptiFine lässt sich nicht über Modrinth/CurseForge installieren (kein API-Zugriff auf
|
||
// deren Downloads) – von Hand hinzugefügt taucht es sonst wie ein beliebiger Mod auf.
|
||
const istOptiFine = modArt === 'mod' && /^optifine[_-]/i.test(m.filename || '');
|
||
if (istOptiFine) kennzeichen.push('<span class="mod-off" style="opacity:.8">OptiFine</span>');
|
||
|
||
item.innerHTML =
|
||
`<div class="mod-icon">${aus ? '🚫' : istOptiFine ? '⚡' : '🧩'}</div>
|
||
<div class="mod-info">
|
||
<div class="mod-name">${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}</div>
|
||
<div class="mod-meta">${up
|
||
? escapeHtml(up.alt.versionNumber) + ' → ' + escapeHtml(up.neu.versionNumber)
|
||
: escapeHtml(m.versionNumber || '') + ' · ' + escapeHtml(m.filename)}</div>
|
||
</div>
|
||
<div class="mod-action"></div>`;
|
||
|
||
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 = '<div class="mods-empty">Lade …</div>'; 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 = `<div class="mods-empty">${escapeHtml(res.error || 'Suche nicht möglich.')}</div>`;
|
||
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 = '<div class="mods-empty">Nichts gefunden.</div>';
|
||
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 = `<div class="mods-empty">Fehler: ${escapeHtml(msg)}</div>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
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 =
|
||
`<div class="mod-name">${escapeHtml(mod.title)}</div>
|
||
<div class="mod-desc">${escapeHtml(mod.description || '')}</div>
|
||
<div class="mod-meta">von ${escapeHtml(mod.author || 'unbekannt')} · ${Number(mod.downloads).toLocaleString('de-DE')} Downloads${mod.source === 'curseforge' ? ' · CurseForge' : ''}</div>`;
|
||
|
||
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 = '<option value="">Aus</option><option value="active">Aktives Konto</option>';
|
||
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 = '<option value="">– gefundene Java-Installationen –</option>';
|
||
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
|
||
? `<a href="#" class="info-meta-link" data-url="${escapeHtml(url)}">${escapeHtml(value)}</a>`
|
||
: action
|
||
? `<a href="#" class="info-meta-link" data-action="${escapeHtml(action)}">${escapeHtml(value)}</a>`
|
||
: escapeHtml(value || '–');
|
||
return `<div class="settings-kv-row"><span class="settings-kv-label">${escapeHtml(label)}</span><span class="settings-kv-value">${renderedValue}</span></div>`;
|
||
}).join('');
|
||
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
|
||
link.addEventListener('click', 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 = '<option value="">– gefundene Java-Installationen –</option>';
|
||
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 = '<div class="account-empty">Noch kein Konto angemeldet.</div>';
|
||
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 =
|
||
`<div class="account-name">${escapeHtml(a.name)}</div>
|
||
<div class="account-tag">${merkmale.length ? '● ' + merkmale.join(' · ') : ''}</div>`;
|
||
|
||
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
|
||
}
|
||
});
|