'use strict'; /* renderer.js – UI-Logik für AeroMC */ // ---------- Instanz-Icons: eigene Pixel-Grafiken im Minecraft-Stil ---------- // Jedes Icon ist ein 16x16-Raster; jedes Zeichen verweist auf eine Farbe der Palette. const PIX = { grass: { p: { a: '#7cbd56', b: '#63a343', c: '#8b6b4a', d: '#6f5238' }, r: [ 'aaaaaaaaaaaaaaaa', 'abaabaaabaabaaab', 'aaaaaaaaaaaaaaaa', 'babaabaabaaabaab', 'cccccccccccccccc', 'cdcccdcccccdccdc', 'ccccccccdcccccdc', 'cccdccccccccdccc', 'dcccccdcccccdccc', 'ccccdccccdccccdc', 'cdccccccccccccdc', 'ccccccdcccccdccc', 'ccdcccccccdccccc', 'cccccccdccccccdc', 'cdccccccccccdccc', 'cccccccccccccccc', ] }, creeper: { p: { a: '#7cbd56', b: '#5f9c42', k: '#0d0f0d' }, r: [ 'abbaabbaabbaabba', 'baabbaabbaabbaab', 'abbaabbaabbaabba', 'aakkaaaaaaaakkaa', 'aakkaaaaaaaakkaa', 'aaaaaakkkkaaaaaa', 'aaaaakkkkkkaaaaa', 'aaaaakkkkkkaaaaa', 'aaaaakkaakkaaaaa', 'aaaaakkaakkaaaaa', 'abbaabbaabbaabba', 'baabbaabbaabbaab', 'abbaabbaabbaabba', 'baabbaabbaabbaab', 'abbaabbaabbaabba', 'baabbaabbaabbaab', ] }, diamond: { p: { a: '#5ce4e0', b: '#3ec6c9', c: '#2aa3ab', w: '#c9fbff', t: null }, r: [ 'tttttttttttttttt', 'tttttttttttttttt', 'tttttaaaaaattttt', 'ttttawwwaaaatttt', 'tttawwbbbaaaattt', 'ttawwbbbbbaaaatt', 'tawwbbbbbbbaaaat', 'abbbbbbbbbbbccca', 'abbbbbbbbbbbccca', 'tabbbbbbbbbcccat', 'ttabbbbbbbcccatt', 'tttabbbbbcccattt', 'ttttabbbcccatttt', 'tttttabcccattttt', 'ttttttaccatttttt', 'tttttttaattttttt', ] }, tnt: { p: { r: '#c0392b', w: '#f2f2f2', k: '#2b2b2b', d: '#8e2a20' }, r: [ 'rrrrrrrrrrrrrrrr', 'rdrrrdrrrrdrrrdr', 'wwwwwwwwwwwwwwww', 'wkkwkkwkkwkkwkkw', 'wkwkwkwkwkwkwkwk', 'wwwwwwwwwwwwwwww', 'rrrrrrrrrrrrrrrr', 'rdrrrrrdrrrrrdrr', 'rrrrdrrrrrdrrrrr', 'rrdrrrrrdrrrrrdr', 'rrrrrdrrrrrdrrrr', 'rdrrrrrrdrrrrrrr', 'rrrrdrrrrrrrdrrr', 'rrdrrrrdrrrrrrdr', 'rrrrrrrrrrrrrrrr', 'rrrrrrrrrrrrrrrr', ] }, chest: { p: { a: '#a97142', b: '#8a5a33', c: '#6b4423', k: '#3a2716', y: '#e0c060' }, r: [ 'cccccccccccccccc', 'cabababababababc', 'cbababababababac', 'cabababababababc', 'ckkkkkkkkkkkkkkc', 'cabababababababc', 'cababyyyyyybabac', 'cababykkkkybabac', 'cababyyyyyybabac', 'cabababababababc', 'cabababababababc', 'cabababababababc', 'cabababababababc', 'cabababababababc', 'cabababababababc', 'cccccccccccccccc', ] }, sword: { p: { s: '#d8dde6', l: '#f4f7fb', d: '#9aa3b0', h: '#8a5a33', g: '#e0c060', t: null }, r: [ 'tttttttttttttsst', 'ttttttttttttslls', 'tttttttttttslldt', 'ttttttttttslldtt', 'tttttttttslldttt', 'ttttttttslldtttt', 'tttttttslldttttt', 'ttttttslldtttttt', 'tttttslldttttttt', 'ttttslldtttttttt', 'tttgllgdtttttttt', 'tthhghhttttttttt', 'ttthhhtttttttttt', 'tttthhtttttttttt', 'ttttthtttttttttt', 'tttttttttttttttt', ] }, pickaxe: { p: { s: '#c9ced6', l: '#eef2f7', d: '#8e959f', h: '#8a5a33', b: '#6b4423', t: null }, r: [ 'tttssssttssssttt', 'ttslllsddslllstt', 'tsllddsttsddllst', 'tsldttttttttdlst', 'ttdttttsstttttdt', 'tttttthhtttttttt', 'ttttttbhtttttttt', 'ttttttthbttttttt', 'tttttttbhttttttt', 'ttttttttbhtttttt', 'ttttttttthbttttt', 'ttttttttttbhtttt', 'tttttttttttbhttt', 'ttttttttttttbhtt', 'tttttttttttttbht', 'tttttttttttttttt', ] }, potion: { p: { g: '#cfd8e3', l: '#eef3fa', v: '#c0392b', d: '#8e2a20', k: '#5a4632', t: null }, r: [ 'ttttttkkkktttttt', 'tttttkllkktttttt', 'tttttkllkttttttt', 'tttttkllkttttttt', 'ttttkglllgkttttt', 'tttkgllllllgkttt', 'ttkglvvvvvvlgktt', 'tkglvvvvvvvvlgkt', 'tkglvvvvvvvvlgkt', 'tkglvvvvvvvvlgkt', 'tkgdvvvvvvvvdgkt', 'tkgdvvvvvvvvdgkt', 'ttkgdvvvvvvdgktt', 'tttkgdvvvvdgkttt', 'ttttkkgggggkkttt', 'tttttkkkkkkkkttt', ] }, nether: { p: { a: '#e07b39', b: '#c9542a', c: '#8f2f18', y: '#f7c948', t: null }, r: [ 'tttttttyyttttttt', 'ttttttyaaytttttt', 'tttttyaaaayttttt', 'ttttyaaaaaaytttt', 'tttyaabbaaaayttt', 'ttyaabbbbaaayttt', 'tyaabbbbbbaaaytt', 'yaabbbcccbbbaayt', 'yabbbcccccbbbayt', 'yabbcccccccbbayt', 'yabbcccccccbbayt', 'tyabcccccccbaytt', 'ttyabcccccbayttt', 'tttyabcccbaytttt', 'ttttyaabbayttttt', 'tttttyyyyytttttt', ] }, end: { p: { a: '#2f6f66', b: '#3f8f83', c: '#1f4a45', w: '#bff5ea', t: null }, r: [ 'tttttbbbbbbttttt', 'tttbbaaaaaabbttt', 'ttbaaaawwaaaabtt', 'tbaaawwwwwwaaabt', 'baaawwbbbbwwaaab', 'baawwbbaabbwwaab', 'baawbbaaaabbwaab', 'bawbbaaaaaabbwab', 'bawbbaaaaaabbwab', 'baawbbaaaabbwaab', 'baawwbbaabbwwaab', 'baaawwbbbbwwaaab', 'tbaaawwwwwwaaabt', 'ttbaaaawwaaaabtt', 'tttbbaaaaaabbttt', 'tttttbbbbbbttttt', ] }, redstone: { p: { a: '#c0392b', b: '#8e2a20', c: '#5c1a13', g: '#3a3a3a', t: null }, r: [ 'tttttttggttttttt', 'ttttttgaagtttttt', 'tttttgaaaagttttt', 'ttttgaabbaagtttt', 'tttgaabbbbaagttt', 'ttgaabbbbbbaagtt', 'tgaabbbccbbbaagt', 'gaabbbcccccbbaag', 'gaabbbcccccbbaag', 'tgaabbbccbbbaagt', 'ttgaabbbbbbaagtt', 'tttgaabbbbaagttt', 'ttttgaabbaagtttt', 'tttttgaaaagttttt', 'ttttttgaagtttttt', 'tttttttggttttttt', ] }, gold: { p: { a: '#f7d774', b: '#e0b53c', c: '#b8891f', t: null }, r: [ 'tttttttttttttttt', 'tttttttttttttttt', 'tttaaaaaaaaaattt', 'ttaaaaaaaaaaaatt', 'taaabbbbbbbbaaat', 'taabbbbbbbbbbaat', 'taabbbbbbbbbbaat', 'tabbbbbbbbbbbbat', 'tabbbbbbbbbbbbat', 'tabbbcccccbbbbat', 'taccccccccccccat', 'taccccccccccccat', 'ttcccccccccccctt', 'tttccccccccccttt', 'tttttttttttttttt', 'tttttttttttttttt', ] }, book: { p: { a: '#8a5a33', b: '#6b4423', w: '#f0e6d2', y: '#e0c060', t: null }, r: [ 'tbbbbbbbbbbbbbbt', 'bawwwwwwwwwwwwab', 'bawwwwwwwwwwwwab', 'bawwbbbwwbbbwwab', 'bawwwwwwwwwwwwab', 'bawwbbbbwbbbbwab', 'bawwwwwwwwwwwwab', 'bawwbbbwwbbbwwab', 'bawwwwwwwwwwwwab', 'bawwbbbbwbbbbwab', 'bawwwwwwwwwwwwab', 'bawwwwyywwwwwwab', 'bawwwwyywwwwwwab', 'bawwwwwwwwwwwwab', 'bawwwwwwwwwwwwab', 'tbbbbbbbbbbbbbbt', ] }, map: { p: { a: '#e8dcc0', b: '#cdbb95', c: '#8a5a33', g: '#7cbd56', u: '#4a90d9' }, r: [ 'cccccccccccccccc', 'caaaaaaaaaaaaaac', 'cabbaaaaaaaaaabc', 'caaauuuaaggaaaac', 'caaauuuaagggaaac', 'caaaauaaaggaaaac', 'caaaaaaaaaaaaaac', 'caagggaaaaauuaac', 'caaggggaaaauuaac', 'caagggaaaaauuaac', 'caaaaaaaaaaaaaac', 'caaaaaabbaaaaaac', 'caaaaabbbbaaaaac', 'caaaaaabbaaaaaac', 'caaaaaaaaaaaaaac', 'cccccccccccccccc', ] }, star: { p: { a: '#ffffff', b: '#dfe6f2', c: '#9fb0cc', t: null }, r: [ 'tttttttaattttttt', 'tttttttaattttttt', 'ttttttbaabtttttt', 'ttttttbaabtttttt', 'tttttbbaabbttttt', 'ttattbbaabbttatt', 'tabaabbaabbaabat', 'taaaaaaaaaaaaaat', 'taaaaaaaaaaaaaat', 'tabaabbaabbaabat', 'ttcttbbaabbttctt', 'tttttbbaabbttttt', 'ttttttbaabtttttt', 'ttttttbaabtttttt', 'tttttttaattttttt', 'tttttttaattttttt', ] }, }; // baut aus dem Raster ein SVG function pixelIconRects(key, scale = 1, offsetX = 0, offsetY = 0) { const def = PIX[key] || PIX.grass; let rects = ''; def.r.forEach((row, y) => { for (let x = 0; x < 16 && x < row.length; x++) { const col = def.p[row[x]]; if (!col || col === '#00000000') continue; rects += ``; } }); return rects; } function pixelIconSvg(key, size) { const rects = pixelIconRects(key); return `${rects}`; } const HD_ICON_THEMES = { grass: { glyph: 'loop', bg: '#46b96c', shade: '#1f5b33', glow: '#95f08a' }, dirt: { glyph: 'cube', bg: '#a96d43', shade: '#58311a', glow: '#d9a56e' }, stone: { glyph: 'cube', bg: '#98a3b3', shade: '#495364', glow: '#dfe5ef' }, planks: { glyph: 'cube', bg: '#cf9b61', shade: '#704521', glow: '#f2cb97' }, tnt: { glyph: 'crate', bg: '#db4d45', shade: '#6d1d18', glow: '#fff2d2' }, obsidian: { glyph: 'cube', bg: '#6f5cd7', shade: '#25184d', glow: '#b8abff' }, netherrack: { glyph: 'cube', bg: '#c14f4f', shade: '#5d1717', glow: '#ff9a7b' }, crafting: { glyph: 'grid', bg: '#bf8750', shade: '#613a20', glow: '#f5d5a4' }, furnace: { glyph: 'forge', bg: '#7e8997', shade: '#38414c', glow: '#ffbd67' }, bookshelf: { glyph: 'books', bg: '#b77a4d', shade: '#5b331f', glow: '#ffd966' }, diamondblock: { glyph: 'cube', bg: '#42d0d7', shade: '#14606d', glow: '#dfffff' }, goldblock: { glyph: 'cube', bg: '#f0bf3c', shade: '#8b5a09', glow: '#fff1a1' }, emeraldblock: { glyph: 'cube', bg: '#2ec97b', shade: '#0b5a35', glow: '#b4ffd7' }, redstoneblock: { glyph: 'cube', bg: '#e14d51', shade: '#75191d', glow: '#ffb1b4' }, diamond: { glyph: 'gem', bg: '#47dce0', shade: '#126b79', glow: '#defcff' }, emerald: { glyph: 'gem', bg: '#2fd485', shade: '#0d683c', glow: '#d8ffe8' }, gold: { glyph: 'ingot', bg: '#f2c44e', shade: '#8d6208', glow: '#fff3b4' }, iron: { glyph: 'ingot', bg: '#b9c3d1', shade: '#57606d', glow: '#f5f8fc' }, redstone: { glyph: 'spark', bg: '#ea5956', shade: '#7e1717', glow: '#ffc8c6' }, coal: { glyph: 'coal', bg: '#59606b', shade: '#1f242b', glow: '#cfd5df' }, sword: { glyph: 'sword', bg: '#5d9ff4', shade: '#1f477c', glow: '#eef6ff' }, pickaxe: { glyph: 'pickaxe', bg: '#57a8de', shade: '#1f5276', glow: '#ebf8ff' }, axe: { glyph: 'axe', bg: '#6997df', shade: '#27456c', glow: '#eef4ff' }, bow: { glyph: 'bow', bg: '#b97a58', shade: '#5c3522', glow: '#ffe1cb' }, apple: { glyph: 'apple', bg: '#ea5353', shade: '#7e1717', glow: '#ffd3d3' }, goldenapple: { glyph: 'apple', bg: '#f0c44f', shade: '#8c6208', glow: '#fff1b2' }, book: { glyph: 'book', bg: '#7a5ad3', shade: '#33205f', glow: '#ece4ff' }, enchantedbook: { glyph: 'bookSpark', bg: '#6f63e7', shade: '#272266', glow: '#d9fcff' }, map: { glyph: 'map', bg: '#5aa67d', shade: '#1d563f', glow: '#f0f1c9' }, star: { glyph: 'star', bg: '#5f7ce0', shade: '#22357c', glow: '#ffffff' }, endereye: { glyph: 'eye', bg: '#3da984', shade: '#124a40', glow: '#ccfff0' }, enderpearl: { glyph: 'pearl', bg: '#4cc0c7', shade: '#155b62', glow: '#e6ffff' }, blaze: { glyph: 'flame', bg: '#ef8b38', shade: '#7c3a06', glow: '#ffe3a3' }, xp: { glyph: 'bottle', bg: '#3ea977', shade: '#15503a', glow: '#dcffe0' }, cake: { glyph: 'cake', bg: '#f08cab', shade: '#8a3453', glow: '#fff6dc' }, bucket: { glyph: 'bucket', bg: '#5b94dc', shade: '#1f4a79', glow: '#d8f1ff' }, lava: { glyph: 'bucket', bg: '#ec7134', shade: '#862d08', glow: '#ffd4ab' }, tnt_item: { glyph: 'powder', bg: '#9ba2af', shade: '#404753', glow: '#fff4ce' }, creeper: { glyph: 'creeper', bg: '#67c35b', shade: '#234f1c', glow: '#d7ffd0' }, chest: { glyph: 'chest', bg: '#c4884f', shade: '#653a1d', glow: '#f5d28f' }, potion: { glyph: 'potion', bg: '#8d63d9', shade: '#3b2466', glow: '#ffd7f5' }, nether: { glyph: 'flame', bg: '#d65a3b', shade: '#6b2111', glow: '#ffd1a0' }, end: { glyph: 'eye', bg: '#3f8d7d', shade: '#153f39', glow: '#d6fff4' }, }; const LEGACY_REAL_ICON_MAP = { chest: 'chest', creeper: 'creeper', potion: 'potion', nether: 'blaze', end: 'endereye', tnt_item: 'tnt_item', }; function hdIconTheme(key) { return HD_ICON_THEMES[key] || { glyph: 'letter', bg: '#5b6474', shade: '#252b34', glow: '#e8edf7' }; } function hdIconGlyph(key, theme) { switch (theme.glyph) { case 'cube': return ` `; case 'loop': return ` `; case 'crate': return ` `; case 'grid': return ` `; case 'forge': return ` `; case 'books': return ` `; case 'gem': return ` `; case 'ingot': return ` `; case 'spark': return ` `; case 'coal': return ` `; case 'sword': return ` `; case 'pickaxe': return ` `; case 'axe': return ` `; case 'bow': return ` `; case 'apple': return ` `; case 'book': return ` `; case 'bookSpark': return ` `; case 'map': return ` `; case 'star': return ` `; case 'eye': return ` `; case 'pearl': return ` `; case 'flame': return ` `; case 'bottle': return ` `; case 'cake': return ` `; case 'bucket': return ` `; case 'powder': return ` `; case 'creeper': return ` `; case 'chest': return ` `; case 'potion': return ` `; case 'letter': return `${escapeHtml((key || '?').slice(0, 1).toUpperCase())}`; default: return pixelIconSvg(key, 44); } } function hdIconSvg(key, size = 44) { const theme = hdIconTheme(key); const gid = `g-${String(key || 'icon').replace(/[^a-z0-9_-]/gi, '-')}-${size}`; return ` `; } const ICON_KEYS = Object.keys(PIX); const ICON_LABELS = { grass: 'Grasblock', creeper: 'Creeper', diamond: 'Diamant', tnt: 'TNT', chest: 'Truhe', sword: 'Schwert', pickaxe: 'Spitzhacke', potion: 'Trank', nether: 'Feuer', end: 'Endauge', redstone: 'Redstone', gold: 'Goldbarren', book: 'Buch', map: 'Karte', star: 'Netherstern', }; // echte Minecraft-Icons (aus den Spieldateien extrahiert), key -> dataUri let mcIcons = {}; async function loadMcIcons() { try { let list = await window.api.iconsList(); const needRefresh = !list.length || !['potion', 'chest', 'creeper', 'paper', 'slimeball', 'cookie'].every((key) => list.some((i) => i.key === key)); if (needRefresh) { const res = await window.api.iconsExtract(); if (res && res.ok && Array.isArray(res.icons) && res.icons.length) list = res.icons; } mcIcons = Object.fromEntries(list.map((i) => [i.key, i])); await buildIsoCubes(); // räumliche Block-Symbole vorbereiten } catch { mcIcons = {}; } } function resolveRealIconKey(key) { if (mcIcons[key]) return key; const mapped = LEGACY_REAL_ICON_MAP[key]; if (mapped && mcIcons[mapped]) return mapped; return ''; } function realItemIconSvg(key, size = 44) { const realKey = resolveRealIconKey(key); if (!realKey) return ''; const real = mcIcons[realKey]; if (!real) return ''; const theme = hdIconTheme(realKey); const src = iso3dEnabled && BLOCK_KEYS.has(realKey) && isoCache[realKey] ? isoCache[realKey] : real.dataUri; const plate = BLOCK_KEYS.has(realKey) ? `` : ``; return ` `; } // erzeugt die Würfel-Darstellungen einmalig im Voraus async function buildIsoCubes() { const jobs = Object.values(mcIcons) .filter((i) => BLOCK_KEYS.has(i.key)) .map((i) => new Promise((resolve) => { const img = new Image(); img.onload = () => { isoSources[i.key] = img; resolve(); }; img.onerror = () => resolve(); img.src = i.dataUri; })); await Promise.all(jobs); for (const key of Object.keys(isoSources)) renderIsoCube(key); } // Block-Texturen, die als räumlicher Würfel dargestellt werden const BLOCK_KEYS = new Set(['grass', 'dirt', 'stone', 'planks', 'tnt', 'obsidian', 'netherrack', 'crafting', 'furnace', 'bookshelf', 'diamondblock', 'goldblock', 'emeraldblock', 'redstoneblock']); let iso3dEnabled = true; const isoCache = {}; // key -> fertiges Würfelbild const isoSources = {}; // key -> geladenes Textur-Bild // zeichnet aus einer 16x16-Textur einen isometrischen Würfel (Canvas -> data:-URI) function renderIsoCube(key) { const img = isoSources[key]; if (!img) return null; const S = 96, c = document.createElement('canvas'); c.width = S; c.height = S; const g = c.getContext('2d'); g.imageSmoothingEnabled = true; if ('imageSmoothingQuality' in g) g.imageSmoothingQuality = 'high'; const w = 26, h = 15, cx = S / 2, top = 8; const face = (m, bright) => { g.save(); g.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); g.drawImage(img, 0, 0, 16, 16); g.restore(); if (bright !== 1) { // Fläche abdunkeln/aufhellen g.save(); g.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); g.globalCompositeOperation = bright < 1 ? 'source-atop' : 'lighter'; g.fillStyle = bright < 1 ? `rgba(0,0,0,${(1 - bright).toFixed(2)})` : 'rgba(255,255,255,.12)'; g.fillRect(0, 0, 16, 16); g.restore(); } }; // Oberseite (Raute), linke und rechte Seitenfläche face([w / 16, h / 16, -w / 16, h / 16, cx, top], 1); face([w / 16, h / 16, 0, 30 / 16, cx - w, top + h], 0.72); face([w / 16, -h / 16, 0, 30 / 16, cx, top + h * 2], 0.55); const out = c.toDataURL('image/png'); isoCache[key] = out; return out; } // liefert das Icon-Markup als skalierbares HD-SVG; eigene Bilder der Instanz bleiben separat erhalten function iconOf(key, size = 44) { const real = realItemIconSvg(key, size); if (real) return real; if (PIX[key]) return pixelIconSvg(key, size); return hdIconSvg(key, size); } const LOADER_LABEL = { vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge', neoforge: 'NeoForge', quilt: 'Quilt', optifine: 'OptiFine', }; // Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren // bei größeren Modpacks spürbar GC-bedingte Ruckler. Wirkt erst ab genug RAM // sinnvoll, schadet aber auch bei wenig RAM nicht. const AIKAR_FLAGS = '-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 ' + '-XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch ' + '-XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M ' + '-XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 ' + '-XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 ' + '-XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem ' + '-XX:MaxTenuringThreshold=1'; // ---------- Zustand ---------- let instances = []; let currentDetailId = null; let selectedInstanceIds = new Set(); let editingId = null; let confirmAction = null; let mcVersionData = null; // gecachte Mojang-Versionsliste let currentModsInstalled = new Set(); // projectIds im offenen Mod-Dialog let pendingImagePath = null; // im Dialog gewähltes eigenes Bild (noch nicht gespeichert) let pendingClearImage = false; // eigenes Bild soll entfernt werden let loginVerificationUri = null; // aktueller Device-Code-Link const collapsedGroups = new Set(); // eingeklappte Gruppen let consoleBuffer = ''; // gesammelte Log-Ausgabe (roh, für Kopieren) let consolePending = ''; // noch nicht abgeschlossene Zeile // ---------- DOM-Helfer ---------- const el = (id) => document.getElementById(id); function show(id) { el(id).classList.remove('hidden'); } function hide(id) { el(id).classList.add('hidden'); } function toast(msg, isError = false) { const t = el('toast'); t.textContent = msg; t.classList.toggle('err', isError); t.classList.remove('hidden'); clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.add('hidden'), 2800); } function fmtDate(iso) { if (!iso) return '–'; try { return new Date(iso).toLocaleString('de-DE'); } catch { return iso; } } function status(text) { el('status-left').textContent = text; } function fmtDuration(s) { s = s || 0; if (s < 60) return s + ' Sek.'; const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); return h ? `${h} Std. ${m} Min.` : `${m} Min.`; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); } function numOrNull(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : null; } function applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark'); } // ---------- Instanzen aus MultiMC übernehmen ---------- let mmcFound = []; let cfFound = []; let mcFound = []; let technicFound = []; let importKind = 'zip'; async function ensureDefaultMcScan() { if (importKind !== 'mc') return; if (el('mc-path').value.trim()) return; if (mcFound.length) return; await scanMc(); } function setImportKind(kind) { importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].includes(kind) ? kind : 'zip'; ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic'].forEach((entryKind) => { el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind); el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind); }); el('import-zip-do').classList.toggle('hidden', importKind !== 'zip'); el('import-modpack-do').classList.toggle('hidden', importKind !== 'modpack'); el('mc-do').classList.toggle('hidden', importKind !== 'mc'); el('cf-do').classList.toggle('hidden', importKind !== 'cf'); el('mmc-do').classList.toggle('hidden', importKind !== 'mmc'); el('technic-do').classList.toggle('hidden', importKind !== 'technic'); el('mc-progress').classList.toggle('hidden', importKind !== 'mc' || !el('mc-progress').textContent); el('cf-progress').classList.toggle('hidden', importKind !== 'cf' || !el('cf-progress').textContent); el('mmc-progress').classList.toggle('hidden', importKind !== 'mmc' || !el('mmc-progress').textContent); el('technic-progress').classList.toggle('hidden', importKind !== 'technic' || !el('technic-progress').textContent); } function openImportDialog(kind = 'zip') { mcFound = []; el('mc-result').classList.add('hidden'); el('mc-do').disabled = true; el('mc-progress').textContent = ''; el('mc-progress').classList.add('hidden'); el('mc-status').textContent = 'Leer lassen nutzt automatisch den Standardpfad unter Windows.'; el('cf-result').classList.add('hidden'); el('cf-do').disabled = true; el('cf-progress').textContent = ''; el('cf-progress').classList.add('hidden'); el('cf-status').textContent = 'Standardpfad unter Windows: C:\\Users\\DeinName\\curseforge\\minecraft\\Instances'; el('mmc-result').classList.add('hidden'); el('mmc-do').disabled = true; el('mmc-progress').textContent = ''; el('mmc-progress').classList.add('hidden'); el('mmc-status').textContent = 'Gib den Ordner an, in dem MultiMC.exe oder PrismLauncher.exe liegt.'; setImportKind(kind); show('modal-import'); if (importKind === 'mc') { el('mc-path').focus(); ensureDefaultMcScan(); } if (importKind === 'cf') el('cf-path').focus(); if (importKind === 'mmc') el('mmc-path').focus(); } async function runZipImport() { el('import-zip-do').disabled = true; try { const res = await window.api.importInstance(); if (res.canceled) return; if (res.ok) { await reload(); hide('modal-import'); toast('Importiert: ' + res.instance.name); return; } toast(res.message || 'Import fehlgeschlagen.', true); } finally { el('import-zip-do').disabled = false; } } async function runModpackImport() { el('import-modpack-do').disabled = true; status('Importiere Modpack …'); try { const res = await window.api.importModpack(); status('Bereit'); el('status-right').textContent = ''; if (res.canceled) return; if (res.ok) { await reload(); hide('modal-import'); toast(`Modpack importiert: ${res.instance.name} (${res.fileCount} Dateien)`); // CurseForge gibt nicht jede Datei heraus – ehrlich benennen statt verschweigen if (res.fehlend) toast(`${res.fehlend} Datei(en) konnten nicht geladen werden und fehlen in der Instanz.`, true); return; } toast(res.message || 'Modpack-Import fehlgeschlagen.', true); if (res.needsKey) { hide('modal-import'); openSettings('pane-network'); } } finally { status('Bereit'); el('status-right').textContent = ''; el('import-modpack-do').disabled = false; } } function fmtDownloads(n) { n = Number(n) || 0; if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'; if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k'; return String(n); } async function searchModpacks() { const query = el('mp-query').value.trim(); const gameVersion = el('mp-mc').value.trim(); const sort = el('mp-sort').value || 'relevance'; const source = (el('mp-source') && el('mp-source').value) || 'modrinth'; const box = el('mp-results'); const st = el('mp-status'); st.textContent = 'Suche …'; box.innerHTML = '
Suche läuft …
'; try { const res = await window.api.modpackSearch({ query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack', source, }); if (res.error) { st.textContent = 'Fehler: ' + res.error; box.innerHTML = '
Suche fehlgeschlagen.
'; if (res.needKey) { const wrap = document.createElement('div'); wrap.className = 'mods-empty'; const btn = document.createElement('button'); btn.className = 'btn btn-mini'; btn.textContent = 'Zu den Netzwerk-Einstellungen'; btn.addEventListener('click', () => { hide('modal-import'); openSettings('pane-network'); }); wrap.appendChild(btn); box.appendChild(wrap); } return; } const hits = res.hits || []; st.textContent = hits.length ? (res.total + ' Treffer' + (gameVersion ? ' für ' + gameVersion : '')) : 'Keine Treffer.'; box.innerHTML = ''; if (!hits.length) { box.innerHTML = '
Keine Modpacks gefunden.
'; return; } for (const h of hits) { const item = document.createElement('div'); item.className = 'mod-item'; const meta = [ h.author ? ('von ' + h.author) : '', h.downloads != null ? (fmtDownloads(h.downloads) + ' Downloads') : '', ].filter(Boolean).join(' · '); const cats = (h.displayCategories || h.categories || []).slice(0, 4).join(', '); item.innerHTML = `
${h.iconUrl ? `` : '📦'}
${escapeHtml(h.title || h.slug || h.projectId)}
${escapeHtml(meta)}${cats ? ' · ' + escapeHtml(cats) : ''}
${escapeHtml((h.description || '').slice(0, 120))}${(h.description || '').length > 120 ? '…' : ''}
`; const btn = document.createElement('button'); btn.className = 'btn btn-primary btn-mini'; btn.textContent = 'Installieren'; btn.addEventListener('click', () => installModpackFromSearch(h, btn)); item.querySelector('.mod-action').appendChild(btn); box.appendChild(item); } } catch (err) { st.textContent = 'Fehler: ' + (err.message || err); box.innerHTML = '
Suche fehlgeschlagen.
'; } } async function installModpackFromSearch(hit, btn) { if (!hit || !hit.projectId) return; const gameVersion = el('mp-mc').value.trim(); const prev = btn ? btn.textContent : ''; if (btn) { btn.disabled = true; btn.textContent = '…'; } status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …'); try { const res = hit.source === 'curseforge' ? await window.api.modpackInstallFromCurseForge({ cfId: hit.cfId, projectId: hit.projectId, title: hit.title || hit.slug, gameVersion, }) : await window.api.modpackInstallFromModrinth({ projectId: hit.projectId, title: hit.title || hit.slug, gameVersion, }); status('Bereit'); el('status-right').textContent = ''; if (res.ok) { await reload(); hide('modal-import'); toast(`Modpack installiert: ${res.instance.name} (${res.fileCount || 0} Dateien)`); if (currentDetailId !== res.instance.id) { try { await selectInstance(res.instance.id); } catch { /* egal */ } } return; } toast(res.message || 'Installation fehlgeschlagen.', true); } catch (err) { status('Bereit'); el('status-right').textContent = ''; toast('Installation fehlgeschlagen: ' + (err.message || err), true); } finally { if (btn) { btn.disabled = false; btn.textContent = prev || 'Installieren'; } } } async function scanMc() { const folder = el('mc-path').value.trim(); el('mc-status').textContent = 'Suche Launcher-Profile …'; const res = await window.api.mcScan(folder); if (!res.ok) { el('mc-status').textContent = '✖ Dort wurde kein launcher_profiles.json gefunden.'; el('mc-result').classList.add('hidden'); el('mc-do').disabled = true; return; } mcFound = res.instances; if (!folder && res.rootDir) el('mc-path').value = res.rootDir; el('mc-status').textContent = `${res.count} Profil${res.count === 1 ? '' : 'e'} gefunden.`; el('mc-result').classList.remove('hidden'); el('mc-all').checked = true; renderMcList(); } function renderMcList() { const box = el('mc-list'); box.innerHTML = ''; if (!mcFound.length) { box.innerHTML = '
Keine Profile gefunden.
'; return; } mcFound.forEach((inst, idx) => { const row = document.createElement('label'); row.className = 'mmc-item'; const loaderTxt = inst.loader === 'vanilla' ? 'Vanilla' : (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : ''); const meta = [inst.version || 'ohne Version', loaderTxt, inst.usesDefaultGameDir ? 'Standard-Spielordner' : 'eigener Spielordner'] .filter(Boolean).join(' · '); row.innerHTML = `
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`; box.appendChild(row); }); updateMcButton(); } function selectedMc() { return [...document.querySelectorAll('#mc-list input[type=checkbox]')] .filter((c) => c.checked) .map((c) => mcFound[Number(c.dataset.idx)]); } function updateMcButton() { const n = selectedMc().length; el('mc-do').disabled = n === 0; el('mc-do').textContent = n ? `${n} übernehmen` : 'Übernehmen'; } async function runMcImport() { const entries = selectedMc(); if (!entries.length) return; const copy = el('mc-copy').checked; el('mc-do').disabled = true; el('mc-progress').textContent = 'Übernehme …'; el('mc-progress').classList.remove('hidden'); const res = await window.api.mcImport(entries, copy); await reload(); hide('modal-import'); const n = res.imported.length; const f = res.failed.length; toast(`${n} Profil${n === 1 ? '' : 'e'} aus dem Minecraft Launcher übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0); if (instances.length) await selectInstance(instances[0].id); } async function scanCf() { const folder = el('cf-path').value.trim(); if (!folder) { el('cf-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; } el('cf-status').textContent = 'Suche Instanzen …'; const res = await window.api.cfScan(folder); if (!res.ok) { el('cf-status').textContent = '✖ Dort wurde kein CurseForge-Instances-Ordner gefunden.'; el('cf-result').classList.add('hidden'); el('cf-do').disabled = true; return; } cfFound = res.instances; el('cf-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden.`; el('cf-result').classList.remove('hidden'); el('cf-all').checked = true; renderCfList(); } function renderCfList() { const box = el('cf-list'); box.innerHTML = ''; if (!cfFound.length) { box.innerHTML = '
Keine Instanzen gefunden.
'; return; } cfFound.forEach((inst, idx) => { const row = document.createElement('label'); row.className = 'mmc-item'; const loaderTxt = inst.loader === 'vanilla' ? 'Vanilla' : (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : ''); const meta = [inst.version || 'ohne Version', loaderTxt] .filter(Boolean).join(' · '); row.innerHTML = `
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`; box.appendChild(row); }); updateCfButton(); } function selectedCf() { return [...document.querySelectorAll('#cf-list input[type=checkbox]')] .filter((c) => c.checked) .map((c) => cfFound[Number(c.dataset.idx)]); } function updateCfButton() { const n = selectedCf().length; el('cf-do').disabled = n === 0; el('cf-do').textContent = n ? `${n} übernehmen` : 'Übernehmen'; } async function runCfImport() { const entries = selectedCf(); if (!entries.length) return; const copy = el('cf-copy').checked; el('cf-do').disabled = true; el('cf-progress').textContent = 'Übernehme …'; el('cf-progress').classList.remove('hidden'); const res = await window.api.cfImport(entries, copy); await reload(); hide('modal-import'); const n = res.imported.length; const f = res.failed.length; toast(`${n} CurseForge-Instanz${n === 1 ? '' : 'en'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0); if (instances.length) await selectInstance(instances[0].id); } async function scanMmc() { const folder = el('mmc-path').value.trim(); if (!folder) { el('mmc-status').textContent = 'Bitte zuerst einen Ordner angeben.'; return; } el('mmc-status').textContent = 'Suche Instanzen …'; const res = await window.api.mmcScan(folder); if (!res.ok) { el('mmc-status').textContent = '✖ Dort wurde kein instances-Ordner gefunden.'; el('mmc-result').classList.add('hidden'); el('mmc-do').disabled = true; return; } mmcFound = res.instances; el('mmc-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden.`; el('mmc-result').classList.remove('hidden'); el('mmc-all').checked = true; renderMmcList(); } function renderMmcList() { const box = el('mmc-list'); box.innerHTML = ''; if (!mmcFound.length) { box.innerHTML = '
Keine Instanzen gefunden.
'; return; } mmcFound.forEach((inst, idx) => { const row = document.createElement('label'); row.className = 'mmc-item'; const loaderTxt = inst.loader === 'vanilla' ? 'Vanilla' : (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : ''); const meta = [inst.version || 'ohne Version', loaderTxt, inst.group ? 'Gruppe: ' + inst.group : ''] .filter(Boolean).join(' · '); row.innerHTML = `
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`; box.appendChild(row); }); updateMmcButton(); } function selectedMmc() { return [...document.querySelectorAll('#mmc-list input[type=checkbox]')] .filter((c) => c.checked) .map((c) => mmcFound[Number(c.dataset.idx)]); } function updateMmcButton() { const n = selectedMmc().length; el('mmc-do').disabled = n === 0; el('mmc-do').textContent = n ? `${n} übernehmen` : 'Übernehmen'; } async function runMmcImport() { const entries = selectedMmc(); if (!entries.length) return; const copy = el('mmc-copy').checked; el('mmc-do').disabled = true; el('mmc-progress').textContent = 'Übernehme …'; el('mmc-progress').classList.remove('hidden'); const res = await window.api.mmcImport(entries, copy); await reload(); hide('modal-import'); const n = res.imported.length; const f = res.failed.length; toast(`${n} Instanz${n === 1 ? '' : 'en'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0); if (instances.length) await selectInstance(instances[0].id); } // ---------- Technic-Plattform: Modpack-Katalog ---------- async function searchTechnicPlatform() { const query = el('tp-query').value.trim(); const box = el('tp-results'); const st = el('tp-status'); st.textContent = 'Suche …'; box.innerHTML = '
Suche läuft …
'; const res = await window.api.technicPlatformSearch(query); if (!res.ok) { st.textContent = 'Fehler: ' + (res.message || 'Suche fehlgeschlagen.'); box.innerHTML = '
Suche fehlgeschlagen.
'; return; } const hits = res.hits || []; st.textContent = hits.length ? `${hits.length} Treffer` : 'Keine Treffer.'; box.innerHTML = ''; if (!hits.length) { box.innerHTML = '
Keine Modpacks gefunden.
'; return; } for (const h of hits) { const item = document.createElement('div'); item.className = 'mod-item'; item.innerHTML = `
${h.iconUrl ? `` : '📦'}
${escapeHtml(h.name)}
`; const btn = document.createElement('button'); btn.className = 'btn btn-primary btn-mini'; btn.textContent = 'Installieren'; btn.addEventListener('click', () => installTechnicFromSearch(h, btn)); item.querySelector('.mod-action').appendChild(btn); box.appendChild(item); } } async function installTechnicFromSearch(hit, btn) { if (!hit || !hit.slug) return; const prev = btn ? btn.textContent : ''; if (btn) { btn.disabled = true; btn.textContent = '…'; } const st = el('tp-status'); st.textContent = `Installiere „${hit.name}" …`; try { const res = await window.api.technicPlatformInstall(hit.slug); if (res.ok) { await reload(); hide('modal-import'); toast(`Modpack installiert: ${res.instance.name}`); await selectInstance(res.instance.id); } else { st.textContent = '✖ ' + (res.message || 'Installation fehlgeschlagen.'); toast(res.message || 'Installation fehlgeschlagen.', true); } } finally { if (btn) { btn.disabled = false; btn.textContent = prev; } } } async function scanTechnic() { const folder = el('technic-path').value.trim(); el('technic-status').textContent = 'Suche Modpacks …'; const res = await window.api.technicScan(folder); if (!res.ok) { el('technic-status').textContent = '✖ Dort wurde kein modpacks-Ordner gefunden.'; el('technic-result').classList.add('hidden'); el('technic-do').disabled = true; return; } technicFound = res.instances; if (!folder && res.instancesDir) el('technic-path').value = res.instancesDir; el('technic-status').textContent = `${res.count} Modpack${res.count === 1 ? '' : 's'} gefunden.`; el('technic-result').classList.remove('hidden'); el('technic-all').checked = true; renderTechnicList(); } function renderTechnicList() { const box = el('technic-list'); box.innerHTML = ''; if (!technicFound.length) { box.innerHTML = '
Keine Modpacks gefunden.
'; return; } technicFound.forEach((inst, idx) => { const row = document.createElement('label'); row.className = 'mmc-item'; const loaderTxt = inst.loader === 'vanilla' ? 'Vanilla' : (LOADER_LABEL[inst.loader] || inst.loader) + (inst.loaderVersion ? ' ' + inst.loaderVersion : ''); const meta = [inst.version || 'Version unbekannt', loaderTxt] .filter(Boolean).join(' · '); row.innerHTML = `
${escapeHtml(inst.name)}
${escapeHtml(meta)}
`; box.appendChild(row); }); updateTechnicButton(); } function selectedTechnic() { return [...document.querySelectorAll('#technic-list input[type=checkbox]')] .filter((c) => c.checked) .map((c) => technicFound[Number(c.dataset.idx)]); } function updateTechnicButton() { const n = selectedTechnic().length; el('technic-do').disabled = n === 0; el('technic-do').textContent = n ? `${n} übernehmen` : 'Übernehmen'; } async function runTechnicImport() { const entries = selectedTechnic(); if (!entries.length) return; const copy = el('technic-copy').checked; el('technic-do').disabled = true; el('technic-progress').textContent = 'Übernehme …'; el('technic-progress').classList.remove('hidden'); const res = await window.api.technicImport(entries, copy); await reload(); hide('modal-import'); const n = res.imported.length; const f = res.failed.length; toast(`${n} Technic-Modpack${n === 1 ? '' : 's'} übernommen${f ? ` · ${f} fehlgeschlagen` : ''}.`, f > 0); if (instances.length) await selectInstance(instances[0].id); } // ---------- Bedrock (Windows-Store-Ausgabe) ---------- let bedrockKind = 'worlds'; // Bedrock-Knopf nur zeigen, wenn aktiviert UND tatsächlich installiert async function applyBedrockVisibility() { const btn = el('btn-bedrock'); let show = false; try { const s = await window.api.getSettings(); if (s.bedrockEnabled !== false) { const info = await window.api.bedrockDetect(); show = !!info.installed; const hint = el('s-bedrock-hint'); if (hint) { hint.textContent = info.installed ? 'Bedrock erkannt (Version ' + info.version + ').' : 'Bedrock ist auf diesem PC nicht installiert – der Bereich bleibt ausgeblendet.'; } } } catch { /* im Zweifel ausblenden */ } btn.classList.toggle('hidden', !show); } async function openBedrock() { show('modal-bedrock'); el('bd-sub').textContent = 'Wird geprüft …'; const info = await window.api.bedrockDetect(); if (!info.installed) { el('bd-sub').textContent = 'nicht installiert'; el('bd-missing').classList.remove('hidden'); el('bd-content').classList.add('hidden'); return; } el('bd-missing').classList.add('hidden'); el('bd-content').classList.remove('hidden'); el('bd-sub').textContent = 'Version ' + info.version; switchBedrockTab('worlds'); } function switchBedrockTab(kind) { bedrockKind = kind; document.querySelectorAll('.btab').forEach((t) => t.classList.toggle('on', t.dataset.kind === kind)); renderBedrockList(); } function fmtBytes(n) { if (!n) return ''; if (n < 1024) return n + ' B'; if (n < 1048576) return (n / 1024).toFixed(0) + ' KB'; return (n / 1048576).toFixed(1) + ' MB'; } async function renderBedrockList() { const box = el('bd-list'); box.innerHTML = '
Lade …
'; const kind = bedrockKind; const items = kind === 'worlds' ? await window.api.bedrockWorlds() : await window.api.bedrockList(kind); box.innerHTML = ''; if (!items.length) { const leer = { worlds: 'Noch keine Welten vorhanden.', resource_packs: 'Keine Ressourcenpakete installiert.', behavior_packs: 'Keine Verhaltenspakete installiert.', skin_packs: 'Keine Skin-Pakete installiert.', screenshots: 'Noch keine Bildschirmfotos.', }; box.innerHTML = `
${leer[kind] || 'Nichts vorhanden.'}
`; return; } for (const it of items) { const name = it.name || it.id; const item = document.createElement('div'); item.className = 'mod-item'; const meta = [fmtBytes(it.bytes), it.modified ? fmtDate(it.modified) : ''].filter(Boolean).join(' · '); item.innerHTML = `
${kind === 'worlds' ? '🌍' : kind === 'screenshots' ? '🖼' : '📦'}
${escapeHtml(name)}
${escapeHtml(meta)}
`; const actions = item.querySelector('.mod-action'); if (kind === 'worlds') { const open = document.createElement('button'); open.className = 'btn btn-mini'; open.textContent = 'Ordner'; open.addEventListener('click', () => window.api.bedrockOpenWorld(it.dir)); actions.appendChild(open); } const del = document.createElement('button'); del.className = 'btn btn-danger btn-mini'; del.textContent = 'Löschen'; del.addEventListener('click', () => askConfirm( 'Löschen', `„${name}" wird unwiderruflich gelöscht. Fortfahren?`, 'Endgültig löschen', async () => { await window.api.bedrockRemove(kind, kind === 'worlds' ? it.id : it.name); await renderBedrockList(); toast('Gelöscht: ' + name); })); actions.appendChild(del); box.appendChild(item); } } // ---------- Kontextmenü (Rechtsklick auf eine Instanz) ---------- function openContextMenu(x, y) { const menu = el('ctx-menu'); menu.classList.remove('hidden'); // erst anzeigen, dann Größe messen und am Fensterrand ausrichten const r = menu.getBoundingClientRect(); const px = Math.min(x, window.innerWidth - r.width - 6); const py = Math.min(y, window.innerHeight - r.height - 6); menu.style.left = Math.max(6, px) + 'px'; menu.style.top = Math.max(6, py) + 'px'; } function closeContextMenu() { el('ctx-menu').classList.add('hidden'); } function formatVersionLabel(version) { return String(version || '') .replace(/^v/i, '') .replace(/-beta$/i, ' Beta') .trim() || '–'; } // ---------- Automatische Updates ---------- let pendingUpdate = null; const CHANNEL_LABEL = { portable: 'Portable', setup: 'Setup' }; function showUpdateDialog(info) { pendingUpdate = info; const channel = CHANNEL_LABEL[info.installKind] || null; el('upd-current').textContent = formatVersionLabel(info.currentVersion); el('upd-new').textContent = formatVersionLabel(info.latestVersion); el('upd-channel').textContent = channel ? `Update-Kanal: ${channel}` : 'Update-Kanal: unbekannt – automatisches Update nicht möglich'; el('upd-notes').textContent = (info.notes || '').trim() || 'Keine Änderungshinweise hinterlegt.'; el('upd-progress').classList.add('hidden'); el('upd-bar-fill').style.width = '0%'; // Ohne passendes Paket wird bewusst nichts installiert – ein Portable-Launcher // darf nie einen Installer bekommen und umgekehrt. Statt eines toten Knopfes // führen wir dann zum Release, damit der Nutzer nicht in der Sackgasse steht. const hatPaket = !!info.asset; el('upd-install').classList.toggle('hidden', !hatPaket); el('upd-install').disabled = !hatPaket; el('upd-install').textContent = 'Installieren'; el('upd-open').classList.toggle('hidden', hatPaket || !info.url); el('upd-hint').textContent = hatPaket ? 'Der Launcher lädt das Update herunter und startet die Installation automatisch.' : (channel ? `Für den Kanal ${channel} liegt in diesem Release keine passende Datei bereit. Bitte im Release nachsehen und von Hand aktualisieren.` : 'Die Paketart dieses Launchers lässt sich nicht sicher bestimmen. Bitte das Update von Hand aus dem Release laden.'); show('modal-update'); } function openReleasePage() { if (pendingUpdate && pendingUpdate.url) window.api.openExternal(pendingUpdate.url); } async function startUpdate() { if (!pendingUpdate || !pendingUpdate.asset) return; const btn = el('upd-install'); btn.disabled = true; btn.textContent = 'Wird installiert …'; el('upd-progress').classList.remove('hidden'); el('upd-status').textContent = 'Wird heruntergeladen …'; const res = await window.api.updateInstall(pendingUpdate.asset); if (res.ok) { el('upd-status').textContent = res.kind === 'portable' ? 'Launcher schließt sich, wird ersetzt und startet automatisch neu.' : 'Installation wird gestartet – der Launcher schließt sich.'; } else { el('upd-status').textContent = '✖ ' + (res.message || 'Update fehlgeschlagen.'); btn.disabled = false; btn.textContent = 'Erneut versuchen'; } } function onUpdateProgress(p) { if (p.phase === 'download') { el('upd-bar-fill').style.width = p.percent + '%'; const mb = (n) => (n / 1048576).toFixed(1); el('upd-status').textContent = `Wird heruntergeladen … ${p.percent}% (${mb(p.loaded)} / ${mb(p.total)} MB)`; } else if (p.phase === 'ready') { el('upd-bar-fill').style.width = '100%'; el('upd-status').textContent = 'Download fertig – Installation startet …'; } } // Aktualisiert die Statuszeile im Info-Bereich – genutzt sowohl vom manuellen // "Nach Updates suchen"-Knopf als auch vom automatischen Check beim Start. function renderUpdateStatus(info) { const st = el('s-update-status'); if (!st) return; st.classList.remove('status-ok', 'status-bad'); if (info.checking) { st.textContent = 'Suche nach Updates …'; } else if (info.error) { st.classList.add('status-bad'); st.textContent = '✖ ' + info.error; } else if (info.noReleases) { st.textContent = 'Noch keine Veröffentlichung vorhanden.'; } else if (info.available) { st.textContent = `Version ${formatVersionLabel(info.latestVersion)} verfügbar.`; } else { st.classList.add('status-ok'); st.textContent = `✓ Du hast bereits die neueste Version (${formatVersionLabel(info.currentVersion)}).`; } } // ---------- Zustand der Anmelde-Dienste in der Statusleiste ---------- function showAuthHealth(h) { const box = el('status-auth'); if (!h || h.ok) { box.classList.add('hidden'); box.textContent = ''; return; } const list = (h.down || []).join(', '); box.textContent = (h.down && h.down.length === AUTH_SERVICE_COUNT) ? 'Anmelde-Server nicht erreichbar – Login/Start derzeit nicht möglich' : 'Anmelde-Dienst gestört: ' + list; box.title = 'Zuletzt geprüft: ' + (h.checked ? new Date(h.checked).toLocaleTimeString('de-DE') : '–') + ' · Klicken zum erneut prüfen'; box.classList.remove('hidden'); } const AUTH_SERVICE_COUNT = 3; // ---------- Skin-Hintergrund (wie MultiMCs Katze, nur mit eigenem Skin) ---------- // uuidOverride: wird vom Konten-Wechsler (siehe unten) genutzt, um gezielt // einen bestimmten Skin zu zeigen, ohne die eigentliche Einstellung anzufassen async function applySkinBackground(uuidOverride) { const s = await window.api.getSettings(); const root = document.documentElement; let uuid = ''; if (uuidOverride) { uuid = uuidOverride; } else if (s.bgSkin === 'active') { const acc = await window.api.authStatus(); uuid = acc && acc.profile ? acc.profile.id : ''; } else if (s.bgSkin) { uuid = s.bgSkin; } if (!uuid) { root.style.setProperty('--bg-skin', 'none'); return; } root.style.setProperty('--bg-skin-opacity', String((s.bgSkinOpacity ?? 55) / 100)); // Jede Pose hat auch eine Entsprechung beim 3D-Dienst (Kamerawinkel/Ansicht), // damit die Auswahl immer sichtbar wirkt – auch wenn der Pose-Dienst offline ist. // 1) Skin lokal in 3D rendern – unabhängig von Render-Diensten const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking'); if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; } // 2) Rückfall: externe Render-Dienste for (const url of [ `https://nmsr.nickac.dev/fullbody/${uuid}?yaw=25&pitch=8`, `https://visage.surgeplay.com/full/512/${uuid}`, `https://crafthead.net/armor/body/${uuid}?size=512`, ]) { const ok = await new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = url; }); if (ok) { root.style.setProperty('--bg-skin', `url("${url}")`); return; } } root.style.setProperty('--bg-skin', 'none'); } // ---------- Skin-Hintergrund: automatischer Kontenwechsel ---------- let skinCycleTimer = null; let skinCycleIndex = 0; function stopSkinCycle() { if (skinCycleTimer) { clearInterval(skinCycleTimer); skinCycleTimer = null; } } async function skinCycleTick() { const accs = await window.api.authAccounts(); if (accs.length < 2) { stopSkinCycle(); await applySkinBackground(); return; } skinCycleIndex = (skinCycleIndex + 1) % accs.length; // am Ende wieder von vorn await applySkinBackground(accs[skinCycleIndex].id); } /* * Einstiegspunkt für alles, was den Skin-Hintergrund neu anzeigen soll. * Ersetzt die frühere direkte applySkinBackground()-Aufrufe: kümmert sich * zusätzlich darum, den Kontenwechsel-Timer je nach Einstellung zu * starten/stoppen, statt dass jede Aufrufstelle das selbst tun müsste. */ async function refreshSkinBackground() { stopSkinCycle(); const s = await window.api.getSettings(); if (s.bgSkinCycle) { const accs = await window.api.authAccounts(); if (accs.length >= 2) { skinCycleIndex = 0; await applySkinBackground(accs[0].id); const seconds = Math.max(5, Number(s.bgSkinCycleSeconds) || 30); skinCycleTimer = setInterval(skinCycleTick, seconds * 1000); return; } // weniger als 2 Konten angemeldet -> normal weiter wie ohne Wechsel } await applySkinBackground(); } // Pose-Vorlagen für den lokalen 3D-Renderer (eigene Nachbildungen bekannter Motive) const SKIN_POSES = { walking: { label: 'Laufend', fov: 36, zoom: 0.62, apply: (p) => { p.rotation.set(0.05, -0.62, 0); p.skin.leftArm.rotation.set(-0.95, 0, 0.06); p.skin.rightArm.rotation.set(0.85, 0, -0.06); p.skin.leftLeg.rotation.set(0.8, 0, 0); p.skin.rightLeg.rotation.set(-0.8, 0, 0); } }, standing: { label: 'Stehend', fov: 36, zoom: 0.6, apply: (p) => { p.rotation.set(0, -0.35, 0); p.skin.leftArm.rotation.set(0, 0, 0.08); p.skin.rightArm.rotation.set(0, 0, -0.08); p.skin.leftLeg.rotation.set(0, 0, 0.02); p.skin.rightLeg.rotation.set(0, 0, -0.02); } }, legends: { label: 'Heldenhaft', fov: 34, zoom: 0.58, apply: (p) => { p.rotation.set(-0.10, -0.5, 0); p.skin.leftArm.rotation.set(-0.25, 0, 0.35); p.skin.rightArm.rotation.set(-0.15, 0, -0.42); p.skin.leftLeg.rotation.set(0.12, 0, 0.16); p.skin.rightLeg.rotation.set(-0.12, 0, -0.16); } }, bedwars: { label: 'Kampfhaltung', fov: 36, zoom: 0.6, apply: (p) => { p.rotation.set(0.06, -0.85, 0); p.skin.rightArm.rotation.set(-1.45, 0, -0.18); // Arm vorgestreckt p.skin.leftArm.rotation.set(0.45, 0, 0.30); p.skin.leftLeg.rotation.set(0.45, 0, 0.10); p.skin.rightLeg.rotation.set(-0.35, 0, -0.10); } }, hoverboard: { label: 'Schwebend', fov: 38, zoom: 0.6, apply: (p) => { p.rotation.set(0.28, -0.7, 0.12); // vorgebeugt, gekippt p.skin.leftArm.rotation.set(-0.35, 0, 1.05); // Arme zum Ausbalancieren p.skin.rightArm.rotation.set(-0.35, 0, -1.05); p.skin.leftLeg.rotation.set(0.5, 0, 0.18); p.skin.rightLeg.rotation.set(0.15, 0, -0.18); } }, sitting: { label: 'Sitzend', fov: 36, zoom: 0.62, apply: (p) => { p.rotation.set(0.05, -0.55, 0); p.skin.leftArm.rotation.set(-0.55, 0, 0.18); p.skin.rightArm.rotation.set(-0.55, 0, -0.18); p.skin.leftLeg.rotation.set(-1.5, 0, 0.12); // Beine angewinkelt p.skin.rightLeg.rotation.set(-1.5, 0, -0.12); } }, spooky: { label: 'Unheimlich', fov: 32, zoom: 0.6, apply: (p) => { p.rotation.set(-0.06, -0.45, 0); // gedreht + leichte Untersicht p.skin.head.rotation.set(0.22, -0.2, 0.28); // Kopf schief gelegt p.skin.leftArm.rotation.set(-1.35, 0, 0.12); // Arme nach vorn ausgestreckt p.skin.rightArm.rotation.set(-1.25, 0, -0.16); p.skin.leftLeg.rotation.set(0.18, 0, 0.06); p.skin.rightLeg.rotation.set(-0.12, 0, -0.06); } }, }; // Rendert den Skin lokal mit WebGL in der gewählten Pose /* * Rendert eine Skin-Textur als Spielfigur und gibt ein Bild zurück. * quelle: Adresse ODER data:-URI – so lässt sich auch eine Datei anzeigen, * die noch gar nicht hochgeladen ist. * modell: 'default' (breite Arme) oder 'slim' (Alex) */ async function renderSkin3D(quelle, poseKey, opts = {}) { if (typeof skinview3d === 'undefined') return null; const canvas = el('skin-canvas'); const breite = opts.breite || 420; const hoehe = opts.hoehe || 700; try { const viewer = new skinview3d.SkinViewer({ canvas, width: breite, height: hoehe, skin: quelle, model: opts.modell === 'slim' ? 'slim' : 'default', preserveDrawingBuffer: true, }); // Auf das Laden der Skin-Textur warten await new Promise((resolve, reject) => { const t = setTimeout(reject, 8000); const check = () => { if (viewer.playerObject && viewer.playerObject.skin && viewer.playerObject.skin.map) { clearTimeout(t); resolve(); } else setTimeout(check, 60); }; check(); }); const pose = SKIN_POSES[poseKey] || SKIN_POSES.walking; viewer.fov = pose.fov; viewer.zoom = pose.zoom; viewer.autoRotate = false; viewer.animation = null; pose.apply(viewer.playerObject); viewer.render(); const data = canvas.toDataURL('image/png'); viewer.dispose(); return data && data.length > 2000 ? data : null; } catch { return null; } } // ---------- Icon-System (SVG statt Emoji) ---------- const ICONS_SVG = { plus: '', import: '', package: '', folder: '', terminal: '', user: '', settings: '', play: '', blocks: '', globe: '', image: '', edit: '', copy: '', export: '', trash: '', }; function svgIcon(name) { const p = ICONS_SVG[name]; if (!p) return ''; const filled = name === 'play'; const attrs = filled ? 'fill="currentColor" stroke="none"' : 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"'; return `${p}`; } function applyIcons(root = document) { root.querySelectorAll('[data-ic]').forEach((b) => { if (b.querySelector('svg')) return; b.insertAdjacentHTML('afterbegin', svgIcon(b.dataset.ic)); }); } // ---------- Instanz-Kacheln ---------- const SINGLE_SELECTION_BUTTON_IDS = [ 'p-play', 'p-offline', 'p-edit', 'p-notes', 'p-mods', 'p-worlds', 'p-screens', 'p-logs', 'p-servers', 'p-modpack', 'p-mcfolder', 'p-configfolder', 'p-instfolder', 'p-shortcut', 'p-export', 'p-copy', ]; function orderedSelectedInstanceIds() { return instances.map((inst) => inst.id).filter((id) => selectedInstanceIds.has(id)); } function syncCardSelectionUi() { document.querySelectorAll('.card').forEach((card) => { card.classList.toggle('sel', selectedInstanceIds.has(card.dataset.id)); }); } function setSingleSelectionActionsDisabled(disabled) { SINGLE_SELECTION_BUTTON_IDS.forEach((id) => { el(id).disabled = disabled; }); const groupLine = el('p-groupline'); groupLine.style.pointerEvents = disabled ? 'none' : ''; groupLine.style.color = disabled ? 'var(--dim)' : ''; } async function refreshSelectionPanel() { const selectedIds = orderedSelectedInstanceIds(); const count = selectedIds.length; const panelC = el('p-content'); const panelE = el('p-empty'); syncCardSelectionUi(); if (!count) { currentDetailId = null; setSingleSelectionActionsDisabled(false); el('p-delete').disabled = false; el('p-delete').textContent = 'Löschen'; panelC.classList.add('hidden'); panelE.classList.remove('hidden'); el('status-left').textContent = 'Bereit'; el('status-play').textContent = 'Keine Instanz ausgewählt'; el('status-total').textContent = ''; return; } if (count > 1) { currentDetailId = null; setSingleSelectionActionsDisabled(true); el('p-delete').disabled = false; el('p-delete').textContent = `${count} Instanzen löschen`; panelE.classList.add('hidden'); panelC.classList.remove('hidden'); el('p-icon').innerHTML = iconOf('chest', 60); el('p-name').textContent = `${count} Instanzen ausgewählt`; el('p-groupline').textContent = 'Mehrfachauswahl aktiv'; el('status-left').textContent = `${count} Instanzen ausgewählt`; el('status-play').textContent = 'Strg+Klick wählt weitere Instanzen aus oder ab'; el('status-total').textContent = 'Sammellöschen verfügbar'; return; } const id = selectedIds[0]; currentDetailId = id; setSingleSelectionActionsDisabled(false); el('p-delete').disabled = false; el('p-delete').textContent = 'Löschen'; window.api.saveSettings({ lastSelectedId: id || '' }); const inst = id ? await window.api.getInstance(id) : null; if (!inst) { selectedInstanceIds.delete(id); panelC.classList.add('hidden'); panelE.classList.remove('hidden'); el('status-left').textContent = 'Bereit'; el('status-play').textContent = 'Keine Instanz ausgewählt'; el('status-total').textContent = ''; return; } panelE.classList.add('hidden'); panelC.classList.remove('hidden'); // "Modpack aktualisieren" nur bei Instanzen, die aus einem Modrinth-Paket stammen el('p-modpack').classList.toggle('hidden', !(inst.modpack && inst.modpack.projectId)); const iconData = inst.customImage ? await window.api.instanceIcon(id) : null; if (iconData) el('p-icon').innerHTML = ``; else { el('p-icon').innerHTML = iconOf(inst.icon, 60); } el('p-name').textContent = inst.name; el('p-groupline').textContent = inst.group ? `Gruppe: ${inst.group}` : 'Ohne Gruppe'; const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla' ? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : ''; el('status-left').textContent = inst.name + ' (' + (inst.minecraft.version || 'ohne Version') + loaderTxt + ')'; el('status-play').textContent = 'Zuletzt gespielt: ' + (inst.lastPlayed ? fmtDate(inst.lastPlayed) : 'nie'); el('status-total').textContent = 'Spielzeit: ' + fmtDuration(inst.totalPlaySeconds); } async function applySelection(ids) { const valid = new Set(instances.map((inst) => inst.id)); selectedInstanceIds = new Set((ids || []).filter((id) => valid.has(id))); await refreshSelectionPanel(); } async function toggleInstanceSelection(id) { const next = new Set(selectedInstanceIds); if (next.has(id)) next.delete(id); else next.add(id); await applySelection([...next]); } function buildCard(inst) { const card = document.createElement('div'); card.className = 'card' + (selectedInstanceIds.has(inst.id) ? ' sel' : ''); card.dataset.id = inst.id; const iconHtml = inst.iconData ? `` : `${iconOf(inst.icon, 60)}`; // Tooltip: Name, Version/Loader und – falls vorhanden – die Notizen const loaderTxt = inst.minecraft.loader && inst.minecraft.loader !== 'vanilla' ? ' · ' + (LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader) : ''; let tip = inst.name + '\n' + (inst.minecraft.version || 'ohne Version') + loaderTxt; if (inst.notes && inst.notes.trim()) tip += '\n\n' + inst.notes.trim(); card.title = tip; card.innerHTML = ` ${iconHtml}
${escapeHtml(inst.name)}
`; // einfacher Klick = auswählen (MultiMC), Doppelklick = starten card.addEventListener('click', (e) => { if (e.ctrlKey || e.metaKey) toggleInstanceSelection(inst.id); else selectInstance(inst.id); }); card.addEventListener('dblclick', () => launch(inst.id)); // Rechtsklick: Instanz auswählen und Kontextmenü öffnen card.addEventListener('contextmenu', async (e) => { e.preventDefault(); if (!selectedInstanceIds.has(inst.id)) await selectInstance(inst.id); openContextMenu(e.clientX, e.clientY); // sofort anzeigen }); // Drag & Drop in Gruppen card.draggable = true; card.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', inst.id); card.classList.add('dragging'); }); card.addEventListener('dragend', () => card.classList.remove('dragging')); return card; } function render() { const q = el('search').value.trim().toLowerCase(); const grid = el('grid'); grid.innerHTML = ''; const filtered = instances.filter((i) => !q || (i.name + ' ' + i.minecraft.version + ' ' + i.minecraft.loader + ' ' + (i.group || '')).toLowerCase().includes(q)); el('empty').classList.toggle('hidden', instances.length !== 0); grid.classList.toggle('hidden', instances.length === 0); // nach Gruppen bündeln const groups = new Map(); for (const inst of filtered) { const g = (inst.group && inst.group.trim()) || ''; if (!groups.has(g)) groups.set(g, []); groups.get(g).push(inst); } // benannte Gruppen alphabetisch, "ohne Gruppe" zuletzt const names = [...groups.keys()].sort((a, b) => (a === '' ? 1 : b === '' ? -1 : a.localeCompare(b))); const showHeaders = names.some((n) => n !== ''); for (const g of names) { const list = groups.get(g); const section = document.createElement('div'); section.className = 'group-section' + (collapsedGroups.has(g) ? ' collapsed' : ''); if (showHeaders) { const header = document.createElement('div'); header.className = 'group-header' + (collapsedGroups.has(g) ? ' collapsed' : ''); header.innerHTML = `${collapsedGroups.has(g) ? '▸' : '▾'} ${escapeHtml(g || 'Ohne Gruppe')} (${list.length})`; header.addEventListener('click', () => { if (collapsedGroups.has(g)) collapsedGroups.delete(g); else collapsedGroups.add(g); render(); }); section.appendChild(header); } const gridEl = document.createElement('div'); gridEl.className = 'group-grid'; for (const inst of list) gridEl.appendChild(buildCard(inst)); section.appendChild(gridEl); // Drop-Ziel: Instanz per Drag&Drop in diese Gruppe verschieben section.addEventListener('dragover', (e) => { e.preventDefault(); section.classList.add('drop-hover'); }); section.addEventListener('dragleave', (e) => { if (!section.contains(e.relatedTarget)) section.classList.remove('drop-hover'); }); section.addEventListener('drop', async (e) => { e.preventDefault(); section.classList.remove('drop-hover'); const dropId = e.dataTransfer.getData('text/plain'); const dragged = instances.find((i) => i.id === dropId); if (dragged && (dragged.group || '') !== g) { await window.api.updateInstance(dropId, { group: g }); await reload(); toast('Verschoben nach: ' + (g || 'Ohne Gruppe')); } }); grid.appendChild(section); } status(`${instances.length} Instanz${instances.length === 1 ? '' : 'en'}`); } function fillGroupDatalist() { const groups = [...new Set(instances.map((i) => i.group).filter(Boolean))].sort(); el('group-list').innerHTML = groups.map((g) => ``).join(''); } async function reload() { instances = await window.api.listInstances(); const valid = new Set(instances.map((inst) => inst.id)); selectedInstanceIds = new Set([...selectedInstanceIds].filter((id) => valid.has(id))); render(); await refreshSelectionPanel(); } // ---------- Minecraft-Versionen ---------- async function loadVersions() { if (mcVersionData) return mcVersionData; try { mcVersionData = await window.api.mcVersions(); } catch (e) { mcVersionData = { offline: true, versions: [], latest: {} }; } return mcVersionData; } function fillVersionSelect(selectedId) { const type = el('f-version-type').value; const sel = el('f-version'); const hint = el('f-version-hint'); sel.innerHTML = ''; const data = mcVersionData || { versions: [], latest: {} }; let list = data.versions || []; if (type !== 'all') list = list.filter((v) => v.type === type); // gewählte Version muss vorhanden sein (auch wenn Filter sie ausblendet) if (selectedId && !list.some((v) => v.id === selectedId)) { const found = (data.versions || []).find((v) => v.id === selectedId); list = [found || { id: selectedId, type: 'unknown' }, ...list]; } if (!list.length) { const o = document.createElement('option'); o.value = selectedId || ''; o.textContent = selectedId || '(keine Versionsliste – offline?)'; sel.appendChild(o); } else { for (const v of list.slice(0, 1200)) { const o = document.createElement('option'); o.value = v.id; o.textContent = v.id; if (v.id === selectedId) o.selected = true; sel.appendChild(o); } } if (data.offline) hint.textContent = '⚠ Offline – Versionsliste evtl. veraltet.'; else if (data.latest) hint.textContent = `Neuestes Release: ${data.latest.release || '?'} · Snapshot: ${data.latest.snapshot || '?'}`; } // Loader-Versionen (Fabric/Quilt) passend zur MC-Version laden async function updateLoaderVersions(preselect) { const loader = el('f-loader').value; const mc = el('f-version').value; const sel = el('f-loader-version'); const hint = el('f-loader-hint'); sel.innerHTML = ''; 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 = ''; sel.disabled = false; if (el('f-optifine-status')) el('f-optifine-status').textContent = ''; if (el('f-optifine-confirm')) el('f-optifine-confirm').classList.add('hidden'); if (!editingId) { hint.textContent = 'Erst speichern, dann lässt sich OptiFine im Bearbeiten-Dialog einrichten.'; if (el('f-optifine-install')) el('f-optifine-install').disabled = true; return; } if (el('f-optifine-install')) el('f-optifine-install').disabled = false; if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; } hint.textContent = 'Suche bereits eingerichtete OptiFine-Fassungen …'; try { const found = await window.api.optifineFind(mc); for (const v of found) { const o = document.createElement('option'); o.value = v; o.textContent = v; if (v === preselect) o.selected = true; sel.appendChild(o); } hint.textContent = found.length ? `${found.length} eingerichtete OptiFine-Fassung(en) für ${mc}.` : 'Noch keine OptiFine-Installation für diese Version – über den Knopf unten einrichten.'; } catch { hint.textContent = 'Suche fehlgeschlagen.'; } return; } sel.disabled = false; if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; } hint.textContent = 'Lade Loader-Versionen …'; try { const versions = await window.api.loaderVersions(loader, mc); for (const v of versions.slice(0, 80)) { const o = document.createElement('option'); o.value = v.version; o.textContent = v.version + (v.stable ? '' : ' (beta)'); if (v.version === preselect) o.selected = true; sel.appendChild(o); } hint.textContent = versions.length ? `${versions.length} Version(en) verfügbar` : 'Keine Loader-Version für diese MC-Version.'; } catch { hint.textContent = 'Loader-Versionen konnten nicht geladen werden.'; } } // ---------- Instanz-Dialog ---------- // alle wählbaren Icons: echte Minecraft-Texturen zuerst, dann die eigenen function availableIcons() { const real = Object.values(mcIcons).map((i) => ({ key: i.key, label: i.label, real: true })); if (real.length) return real; const own = ICON_KEYS .filter((k) => !mcIcons[k]) .map((k) => ({ key: k, label: ICON_LABELS[k] || k, real: false })); return real.concat(own); } function fillIconSelect(selected) { const sel = el('f-icon'); sel.innerHTML = ''; for (const it of availableIcons()) { const o = document.createElement('option'); o.value = it.key; o.textContent = it.label; if (it.key === selected) o.selected = true; sel.appendChild(o); } renderIconChoices(selected); } // klickbare Item-Vorschau unter der Auswahl function renderIconChoices(selected) { const box = el('f-icon-grid'); if (!box) return; box.innerHTML = ''; for (const it of availableIcons()) { const b = document.createElement('button'); b.type = 'button'; b.className = 'icon-choice' + (it.key === selected ? ' on' : ''); b.title = it.label; b.innerHTML = iconOf(it.key, 24); b.addEventListener('click', () => { el('f-icon').value = it.key; renderIconChoices(it.key); }); box.appendChild(b); } } // zeigt/versteckt die Bildvorschau im Dialog function setIconPreview(dataUri) { const box = el('f-icon-preview'); const clr = el('f-clear-image'); if (dataUri) { box.innerHTML = ``; box.classList.remove('hidden'); clr.classList.remove('hidden'); } else { box.innerHTML = ''; box.classList.add('hidden'); clr.classList.add('hidden'); } } function resetIconControls() { pendingImagePath = null; pendingClearImage = false; setIconPreview(null); } async function openCreate() { editingId = null; el('instance-modal-title').textContent = 'Neue Instanz'; el('f-name').value = ''; el('f-group').value = ''; fillGroupDatalist(); el('f-loader').value = 'vanilla'; el('f-loader-version').value = ''; el('f-notes').value = ''; fillIconSelect('grass'); resetIconControls(); el('f-advanced').classList.add('hidden'); show('modal-instance'); el('f-name').focus(); el('f-version-hint').textContent = 'Lade Versionsliste …'; await loadVersions(); el('f-version-type').value = 'release'; fillVersionSelect(mcVersionData.latest && mcVersionData.latest.release); updateLoaderVersions(); } async function openEdit(inst) { editingId = inst.id; el('instance-modal-title').textContent = 'Instanz bearbeiten'; el('f-name').value = inst.name; el('f-group').value = inst.group || ''; fillGroupDatalist(); el('f-loader').value = inst.minecraft.loader || 'vanilla'; el('f-loader-version').value = inst.minecraft.loaderVersion || ''; el('f-notes').value = inst.notes || ''; fillIconSelect(inst.icon); resetIconControls(); if (inst.customImage) { const data = await window.api.instanceIcon(inst.id); if (data) setIconPreview(data); } el('f-min-mem').value = inst.java.minMemMb || ''; el('f-max-mem').value = inst.java.maxMemMb || ''; zeigeSpeicherHinweis(); fuelleInstanzJava(inst); el('f-extra-args').value = inst.java.extraArgs || ''; el('f-prelaunch').value = inst.preLaunchCommand || ''; el('f-postexit').value = inst.postExitCommand || ''; el('f-advanced').classList.remove('hidden'); show('modal-instance'); el('f-name').focus(); el('f-version-hint').textContent = 'Lade Versionsliste …'; await loadVersions(); el('f-version-type').value = inst.minecraft.version ? 'all' : 'release'; fillVersionSelect(inst.minecraft.version || (mcVersionData.latest && mcVersionData.latest.release)); await updateLoaderVersions(inst.minecraft.loaderVersion); } async function saveInstance() { const name = el('f-name').value.trim(); if (!name) { toast('Bitte einen Namen angeben.', true); el('f-name').focus(); return; } const payload = { name, group: el('f-group').value.trim(), icon: el('f-icon').value, notes: el('f-notes').value.trim(), minecraft: { version: el('f-version').value.trim(), loader: el('f-loader').value, loaderVersion: el('f-loader-version').value.trim(), }, }; // Speicherangaben prüfen, bevor gespeichert wird const minMem = numOrNull(el('f-min-mem').value); const maxMem = numOrNull(el('f-max-mem').value); if (minMem || maxMem) { const pruefung = await window.api.checkMemory(minMem, maxMem); if (!pruefung.ok) { toast(pruefung.message, true); return; } if (pruefung.warnung) toast(pruefung.warnung, false); } try { let targetId = editingId; if (editingId) { payload.java = { path: el('f-java').value.trim(), minMemMb: minMem, maxMemMb: maxMem, extraArgs: el('f-extra-args').value.trim(), }; payload.preLaunchCommand = el('f-prelaunch').value.trim(); payload.postExitCommand = el('f-postexit').value.trim(); await window.api.updateInstance(editingId, payload); } else { const created = await window.api.createInstance(payload); targetId = created.id; } // eigenes Bild anwenden bzw. entfernen if (pendingImagePath) await window.api.setInstanceImage(targetId, pendingImagePath); else if (pendingClearImage && editingId) await window.api.clearInstanceImage(targetId); toast(editingId ? 'Instanz gespeichert.' : 'Instanz angelegt.'); hide('modal-instance'); await reload(); await selectInstance(targetId); } catch (e) { toast('Fehler: ' + e.message, true); } } // ---------- Auswahl + rechte Aktionsleiste (MultiMC-Stil) ---------- async function selectInstance(id) { await applySelection(id ? [id] : []); } let launching = false; async function launch(id, accountId, launchOpts) { if (launching) return; launching = true; status('Vorbereiten …'); el('status-right').textContent = ''; appendConsole('\n=== Start: ' + new Date().toLocaleTimeString('de-DE') + ' ===\n'); try { const res = await window.api.launchInstance(id, accountId, launchOpts || null); if (res.ok) { status('Läuft (PID ' + res.pid + ')'); toast('Minecraft gestartet.'); const s = await window.api.getSettings(); if (s.showConsoleOnLaunch) openConsole(); // nur wenn gewünscht } else if (res.needAuth) { status('Bereit'); el('status-right').textContent = ''; toast(res.message, false); } else { status('Bereit'); el('status-right').textContent = ''; toast(res.message || 'Start fehlgeschlagen.', true); } } catch (e) { status('Bereit'); toast('Fehler: ' + e.message, true); } finally { launching = false; } } // ---------- Java je Instanz ---------- async function fuelleInstanzJava(inst) { const feld = el('f-java'); if (!feld) return; feld.value = (inst && inst.java && inst.java.path) || ''; const sel = el('f-java-detected'); sel.innerHTML = ''; 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 ? `
${t}
${escapeHtml(w)}
` : ''); el('mp-kern').innerHTML = zeile('Installiert', res.aktuelleNummer) + zeile('Neu', res.neueNummer) + zeile('Minecraft', res.spielVersion) + (res.hinweise ? zeile('Änderungen', res.hinweise.slice(0, 400)) : ''); el('mp-kern').classList.remove('hidden'); el('mp-do').classList.remove('hidden'); } async function doModpackUpdate() { if (!modpackNeueVersion) return; const btn = el('mp-do'); btn.disabled = true; el('mp-status').textContent = 'Wird aktualisiert – das kann einige Minuten dauern …'; const res = await window.api.modpackUpdate(currentDetailId, modpackNeueVersion); btn.disabled = false; if (!res.ok) { el('mp-status').textContent = '✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.'); return; } el('mp-status').textContent = `✔ Auf ${res.neueNummer} aktualisiert: ${res.eingespielt} Dateien eingespielt` + (res.entfernt ? `, ${res.entfernt} entfernt` : '') + '.'; el('mp-do').classList.add('hidden'); toast('Modpack aktualisiert.'); await reload(); await selectInstance(currentDetailId); } // ---------- Serverliste ---------- let zuUebertragenderServer = null; async function openServers() { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); el('servers-title').textContent = 'Server – ' + (inst ? inst.name : ''); el('srv-name').value = ''; el('srv-ip').value = ''; show('modal-servers'); await renderServers(); } async function renderServers() { const box = el('srv-list'); box.innerHTML = '
Lade …
'; const res = await window.api.serversList(currentDetailId); box.innerHTML = ''; if (!res.ok) { box.innerHTML = `
${escapeHtml(res.message || 'Liste nicht lesbar.')}
`; return; } if (!res.server.length) { box.innerHTML = '
Noch keine Server eingetragen.
'; return; } for (const s of res.server) { const item = document.createElement('div'); item.className = 'mod-item'; item.innerHTML = `
🖥
${escapeHtml(s.name || s.ip)}
${escapeHtml(s.ip)}
Wird geprüft …
`; const aktionen = item.querySelector('.mod-action'); pingServerRow(s.ip, item.querySelector('.srv-status')); const starten = document.createElement('button'); starten.className = 'btn btn-primary btn-mini'; starten.textContent = 'Starten'; starten.title = 'Instanz starten und direkt diesem Server beitreten (Quick Play, ab MC 1.20)'; starten.addEventListener('click', () => { hide('modal-servers'); launch(currentDetailId, null, { quickPlayServer: s.ip }); }); aktionen.appendChild(starten); const favorit = document.createElement('button'); favorit.className = 'btn btn-mini'; favorit.textContent = '★ Verknüpfung'; favorit.title = 'Desktop-Verknüpfung erstellen, die Minecraft startet und direkt mit diesem Server verbindet (Quick Play, ab MC 1.20)'; favorit.addEventListener('click', async () => { const inst = await window.api.getInstance(currentDetailId); const vorschlag = `${inst ? inst.name : 'Instanz'} - ${s.name || s.ip}`; const eingabe = await askPrompt({ title: 'Server-Verknüpfung erstellen', text: 'Name der Desktop-Verknüpfung (ohne .lnk):', label: 'Name', value: vorschlag, okLabel: 'Erstellen', }); if (eingabe == null) return; // abgebrochen const res = await window.api.serversShortcut(currentDetailId, s.ip, s.name, eingabe.trim()); if (res.ok) toast('Verknüpfung auf dem Desktop erstellt: ' + (s.name || s.ip)); else toast(res.message || 'Verknüpfung fehlgeschlagen.', true); }); aktionen.appendChild(favorit); const umbenennen = document.createElement('button'); umbenennen.className = 'btn btn-mini'; umbenennen.textContent = 'Umbenennen'; umbenennen.title = 'Anzeigenamen ändern'; umbenennen.addEventListener('click', async () => { const aktuell = s.name || s.ip; const eingabe = await askPrompt({ title: 'Server umbenennen', text: 'Adresse: ' + s.ip, label: 'Name', value: aktuell, okLabel: 'Speichern', }); if (eingabe == null) return; // abgebrochen const res = await window.api.serversRename(currentDetailId, s.ip, eingabe); if (!res.ok) { toast(res.message || 'Umbenennen fehlgeschlagen.', true); return; } if (!res.unveraendert) toast('Umbenannt: ' + eingabe.trim()); await renderServers(); }); aktionen.appendChild(umbenennen); const kopieren = document.createElement('button'); kopieren.className = 'btn btn-mini'; kopieren.textContent = 'Übertragen…'; kopieren.title = 'Diesen Server in andere Instanzen eintragen'; kopieren.addEventListener('click', () => openServerCopy(s)); aktionen.appendChild(kopieren); const weg = document.createElement('button'); weg.className = 'btn btn-danger btn-mini'; weg.textContent = 'Entfernen'; weg.addEventListener('click', async () => { const r = await window.api.serversRemove(currentDetailId, s.ip); if (r.ok) toast('Entfernt: ' + (s.name || s.ip)); else toast(r.message || 'Entfernen fehlgeschlagen.', true); await renderServers(); }); aktionen.appendChild(weg); box.appendChild(item); } } // Fragt Live-Status (MOTD/Spieler/Version) ab und trägt ihn in die Zeile ein. // Läuft unabhängig für jede Zeile, ohne die Liste selbst zu blockieren. async function pingServerRow(ip, statusEl) { if (!statusEl) return; const res = await window.api.serversPing(ip); // Zeile kann inzwischen aus dem DOM entfernt worden sein (Liste neu aufgebaut) if (!statusEl.isConnected) return; if (!res.ok) { statusEl.textContent = '⚠ ' + (res.message || 'nicht erreichbar'); statusEl.classList.add('status-bad'); return; } const spieler = res.max != null ? `${res.online ?? '?'}/${res.max} Spieler` : ''; statusEl.textContent = [res.motd, spieler, res.version, res.latencyMs + ' ms'] .filter(Boolean).join(' · '); statusEl.classList.add('status-ok'); } async function addServer() { const eintrag = { name: el('srv-name').value.trim(), ip: el('srv-ip').value.trim() }; const res = await window.api.serversAdd(currentDetailId, eintrag); if (!res.ok) { toast(res.message || 'Hinzufügen fehlgeschlagen.', true); return; } el('srv-name').value = ''; el('srv-ip').value = ''; toast('Server hinzugefügt.'); await renderServers(); } async function openServerCopy(server) { zuUebertragenderServer = server; el('srv-copy-sub').textContent = `„${server.name || server.ip}" (${server.ip}) in diese Instanzen eintragen:`; const box = el('srv-copy-list'); box.innerHTML = ''; const alle = await window.api.listInstances(); const andere = alle.filter((i) => i.id !== currentDetailId); if (!andere.length) { box.innerHTML = '
Es gibt keine weitere Instanz.
'; } for (const i of andere) { const zeile = document.createElement('label'); zeile.className = 'checkbox-field'; zeile.style.padding = '6px 2px'; zeile.innerHTML = ` ${escapeHtml(i.name)}`; box.appendChild(zeile); } show('modal-srv-copy'); } async function doServerCopy() { if (!zuUebertragenderServer) return; const ids = [...el('srv-copy-list').querySelectorAll('input:checked')].map((c) => c.value); if (!ids.length) { toast('Keine Instanz ausgewählt.', true); return; } const res = await window.api.serversCopyTo(ids, zuUebertragenderServer); hide('modal-srv-copy'); if (res.erledigt.length) toast(`Eingetragen in: ${res.erledigt.join(', ')}`); for (const f of res.fehler) toast(`${f.name || f.id}: ${f.message}`, true); } // ---------- Skin ändern ---------- let gewaehlterSkin = null; /* * Bild nur zeigen, wenn es wirklich geladen wurde – ein ohne Quelle * stellt sonst das Platzhalter-Symbol des Browsers dar. */ function zeigeSkinBild(bildId, leerId, quelle, leerText) { const bild = el(bildId); const leer = el(leerId); leer.textContent = leerText; if (!quelle) { bild.classList.add('hidden'); bild.removeAttribute('src'); leer.classList.remove('hidden'); return; } bild.onload = () => { bild.classList.remove('hidden'); leer.classList.add('hidden'); }; bild.onerror = () => { bild.classList.add('hidden'); bild.removeAttribute('src'); leer.textContent = 'Vorschau nicht ladbar'; leer.classList.remove('hidden'); }; bild.src = quelle; } // Textur einer Seite merken, damit bei einem Wechsel der Armform beide // Figuren neu gerendert werden können let skinTexturAktuell = ''; let skinTexturNeu = ''; // Skin-Textur als Figur rendern; klappt das nicht, wird die Textur gezeigt async function zeigeSkinFigur(bildId, leerId, textur, leerText) { if (!textur) { zeigeSkinBild(bildId, leerId, '', leerText); return; } el(leerId).textContent = 'wird gerendert …'; const modell = el('skin-variante').value === 'slim' ? 'slim' : 'default'; const figur = await renderSkin3D(textur, 'standing', { modell, breite: 260, hoehe: 420 }); zeigeSkinBild(bildId, leerId, figur || textur, leerText); } async function openSkin() { gewaehlterSkin = null; skinTexturAktuell = ''; skinTexturNeu = ''; el('skin-pfad').value = ''; el('skin-apply').disabled = true; el('skin-status').textContent = ''; zeigeSkinBild('skin-neu', 'skin-neu-leer', '', 'noch keine Datei gewählt'); zeigeSkinBild('skin-aktuell', 'skin-aktuell-leer', '', 'wird geladen …'); el('skin-konto').textContent = 'Profil wird geladen …'; show('modal-skin'); const p = await window.api.skinProfile(); if (!p.ok) { el('skin-konto').textContent = p.message || 'Profil konnte nicht geladen werden.'; zeigeSkinBild('skin-aktuell', 'skin-aktuell-leer', '', 'kein Profil geladen'); return; } el('skin-konto').textContent = `Angemeldet als ${p.name}. Die Änderung gilt für dieses Konto.`; // Armform zuerst setzen – davon hängt ab, wie die Figur gerendert wird if (p.variante) el('skin-variante').value = p.variante === 'slim' ? 'slim' : 'classic'; skinTexturAktuell = p.skinUrl || ''; await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, 'Standard-Skin (keine eigene Datei hinterlegt)'); } // Armform gewechselt -> beide Figuren neu zeichnen async function skinVarianteGewechselt() { if (skinTexturAktuell) await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, ''); if (skinTexturNeu) await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, ''); } async function waehleSkinDatei() { const res = await window.api.skinChoose(); if (res.canceled) return; if (!res.ok) { el('skin-status').textContent = '✖ ' + res.message; return; } gewaehlterSkin = res.pfad; el('skin-pfad').value = res.pfad; skinTexturNeu = res.dataUri || ''; await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, 'Vorschau nicht verfügbar'); el('skin-status').textContent = `${res.breite}×${res.hoehe} Pixel – sieht gut aus.`; el('skin-apply').disabled = false; } async function setzeSkin() { if (!gewaehlterSkin) return; const btn = el('skin-apply'); btn.disabled = true; el('skin-status').textContent = 'Wird hochgeladen …'; const res = await window.api.skinApply(gewaehlterSkin, el('skin-variante').value); if (res.ok) { el('skin-status').textContent = '✔ Skin gesetzt. Im Spiel kann es einen Moment dauern.'; toast('Skin geändert.'); await openSkin(); // Vorschau mit dem neuen Stand auffrischen await refreshSkinBackground(); // auch der Hintergrund zeigt jetzt den neuen Skin } else { el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.'); btn.disabled = false; } } async function setzeSkinZurueck() { const btn = el('skin-reset'); btn.disabled = true; el('skin-status').textContent = 'Wird zurückgesetzt …'; const res = await window.api.skinReset(); if (res.ok) { toast('Skin auf Standard zurückgesetzt.'); await openSkin(); await refreshSkinBackground(); } else { el('skin-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.'); } btn.disabled = false; } // ---------- Logdateien ---------- let aktuellesLog = ''; async function openLogs() { if (!currentDetailId) return; const inst = await window.api.getInstance(currentDetailId); el('logs-title').textContent = 'Logdateien – ' + (inst ? inst.name : ''); const auswahl = el('logs-select'); auswahl.innerHTML = ''; el('logs-text').textContent = 'Lade …'; el('logs-info').textContent = ''; aktuellesLog = ''; show('modal-logs'); const dateien = await window.api.logsList(currentDetailId); if (!dateien.length) { el('logs-text').textContent = 'Für diese Instanz gibt es noch keine Logdateien. ' + 'Sie entstehen beim ersten Spielstart.'; return; } for (const d of dateien) { const o = document.createElement('option'); o.value = d.name; const kb = Math.max(1, Math.round(d.groesse / 1024)); o.textContent = `${d.name} · ${kb} KB · ${new Date(d.geaendert).toLocaleString('de-DE')}` + (d.gepackt ? ' · gepackt' : ''); auswahl.appendChild(o); } await ladeLog(dateien[0].name); } async function ladeLog(name) { el('logs-text').textContent = 'Lade …'; const res = await window.api.logsRead(currentDetailId, name); if (!res.ok) { el('logs-text').textContent = 'Datei konnte nicht gelesen werden.'; el('logs-info').textContent = ''; aktuellesLog = ''; return; } aktuellesLog = res.text; el('logs-text').textContent = res.text || '(leer)'; el('logs-info').textContent = res.gekuerzt ? `${res.zeilenGesamt} Zeilen – die letzten 5000 werden gezeigt` : `${res.zeilenGesamt} Zeilen`; // ans Ende springen: dort steht, was zuletzt passierte const feld = el('logs-text'); feld.scrollTop = feld.scrollHeight; } // ---------- Absturzberichte ---------- let letzterStartMs = 0; let aktuellerCrash = null; async function zeigeAbsturzbericht(instanzId) { const id = instanzId || currentDetailId; if (!id) return; // Minecraft schreibt den Bericht erst kurz nach dem Ende await new Promise((r) => setTimeout(r, 900)); let res; try { res = await window.api.crashLatest(id, letzterStartMs); } catch { return; } if (!res || !res.ok) return; // kein Bericht -> Konsole muss reichen aktuellerCrash = res; const inst = await window.api.getInstance(id); el('crash-sub').textContent = `${inst ? inst.name + ' · ' : ''}${res.datei} · ` + new Date(res.zeitpunkt).toLocaleString('de-DE'); const zeile = (titel, wert) => wert ? `
${titel}
${escapeHtml(wert)}
` : ''; let html = zeile('Diagnose', res.diagnose) + zeile('Empfohlene Lösung', res.empfehlung) + zeile('Beschreibung', res.beschreibung) + zeile('Fehler', res.fehler) + zeile('Verdächtige Mods', res.verdaechtigeMods); if (res.verlauf && res.verlauf.length) { html += `
Aufrufverlauf
` + `
${escapeHtml(res.verlauf.join('\n'))}
`; } el('crash-kern').innerHTML = html || '
Der Bericht enthält keine der üblichen Kennzeichen.
'; el('crash-voll').textContent = res.text || ''; show('modal-crash'); } // Fortschrittsmeldungen der Download-/Start-Pipeline function onLaunchEvt(evt) { const labels = { version: 'Version', client: 'Client', libraries: 'Libraries', assets: 'Assets', loader: 'Loader', java: 'Java', launch: 'Start', modpack: 'Modpack', backup: 'Sicherung' }; if (evt.phase === 'log') { appendConsole(evt.line); return; } if (evt.phase === 'exit') { status('Minecraft beendet (Code ' + evt.code + ')'); el('status-right').textContent = ''; appendConsole('\n[Prozess beendet, Code ' + evt.code + ']\n'); // War vorher "Stopping!" im Log (regulärer Client-Shutdown), zählt ein // nachträglicher harter Exit-Code nicht als Absturz während des Spiels – // z. B. wenn Minecrafts interner Shutdown-Watchdog wegen hängender // Mod-Threads (z. B. JourneyMap WebMap) die JVM nach Ablauf einer // Wartezeit zwangsweise beendet. Der Bericht bleibt trotzdem gespeichert. const crashed = evt.code !== 0 && evt.code !== null && !evt.cleanShutdown; if (evt.cleanShutdown && evt.code !== 0 && evt.code !== null) { appendConsole('[Hinweis] Der Client wurde regulär beendet (Stopping!), aber danach vom ' + 'internen Shutdown-Watchdog hart terminiert (z. B. durch einen Mod, dessen Threads ' + 'nicht rechtzeitig beenden – etwa JourneyMap WebMap). Kein Absturz während des Spiels.\n'); } // Absturzbericht suchen, bevor über die Konsole entschieden wird – // ein Bericht sagt mehr als der Java-Auswurf if (crashed) zeigeAbsturzbericht(evt.id); window.api.getSettings().then((s) => { if (crashed && s.showConsoleOnCrash !== false) { openConsole(); return; } if (s.autoCloseConsole) hide('modal-console'); }).catch(() => {}); return; } // Startzeitpunkt merken: nur Berichte ab hier gehören zu diesem Lauf if (evt.phase === 'launch') letzterStartMs = Date.now(); const label = labels[evt.phase] || evt.phase; if (evt.total) { status(`${label} … ${evt.current}/${evt.total}`); el('status-right').textContent = Math.round((evt.current / evt.total) * 100) + '%'; } else { status(evt.detail || label + ' …'); el('status-right').textContent = ''; appendConsole('[' + label + '] ' + (evt.detail || '') + '\n'); } } // ---------- Konsole ---------- function classifyLine(l) { if (/\/(ERROR|FATAL|SEVERE)\]/.test(l) || /Exception(:| in )|^\s+at\s|Caused by:/.test(l)) return 'log-error'; if (/\/WARN(ING)?\]/.test(l) || /\bWARN\b/.test(l)) return 'log-warn'; if (/\[Pre-Launch\]|\[Post-Exit\]|=== Start:|\[Prozess beendet/.test(l)) return 'log-info'; return ''; } function appendLineEl(out, line) { const div = document.createElement('div'); div.className = 'log-line ' + classifyLine(line); div.textContent = line; out.appendChild(div); } let consoleMaxChars = 300000; // aus den Einstellungen abgeleitet (Zeilen x ~80 Zeichen) function appendConsole(text) { consoleBuffer += text; if (consoleBuffer.length > consoleMaxChars) consoleBuffer = consoleBuffer.slice(-Math.round(consoleMaxChars * 0.75)); consolePending += text; const lines = consolePending.split('\n'); consolePending = lines.pop(); const out = el('console-out'); if (!el('modal-console').classList.contains('hidden')) { for (const line of lines) appendLineEl(out, line); out.scrollTop = out.scrollHeight; } } function openConsole() { const out = el('console-out'); out.innerHTML = ''; const lines = (consoleBuffer || '(noch keine Ausgabe)').split('\n'); for (const line of lines) appendLineEl(out, line); consolePending = ''; show('modal-console'); out.scrollTop = out.scrollHeight; } // ---------- Welten & Ressourcenpakete ---------- async function openContent() { const inst = await window.api.getInstance(currentDetailId); el('content-title').textContent = 'Welten, Ressourcen- & Datenpakete – ' + inst.name; await renderContentLists(); show('modal-content'); } async function renderContentLists() { renderContentList('content-worlds', await window.api.worlds(currentDetailId), 'saves', '🌍'); renderContentList('content-rp', await window.api.resourcepacks(currentDetailId), 'resourcepacks', '🎨'); renderContentList('content-dp', await window.api.datapacks(currentDetailId), 'datapacks', '📜'); } // ---------- Screenshots ---------- let screenSelected = new Set(); function updateScreensSelectionUi() { const n = screenSelected.size; const btn = el('screens-delete-sel'); if (btn) { btn.disabled = n === 0; btn.textContent = n ? (n + ' löschen') : 'Auswahl löschen'; } const hint = el('screens-sel-hint'); if (hint) hint.textContent = n ? (n + ' ausgewählt – Doppelklick öffnet das Bild') : 'Klick auf Vorschaubild zum Auswählen · Doppelklick öffnet'; document.querySelectorAll('#screens-grid .screen-thumb').forEach((t) => { const name = t.dataset.name; const on = name && screenSelected.has(name); t.classList.toggle('selected', !!on); const cb = t.querySelector('.screen-check'); if (cb) cb.checked = !!on; }); } async function openScreens() { const inst = await window.api.getInstance(currentDetailId); el('screens-title').textContent = 'Screenshots – ' + inst.name; const grid = el('screens-grid'); grid.innerHTML = '
Lade …
'; screenSelected = new Set(); updateScreensSelectionUi(); show('modal-screens'); const shots = await window.api.screenshots(currentDetailId); grid.innerHTML = ''; if (!shots.length) { grid.innerHTML = '
Keine Screenshots vorhanden.
'; updateScreensSelectionUi(); return; } for (const s of shots) { const thumb = document.createElement('div'); thumb.className = 'screen-thumb'; thumb.dataset.name = s.name; thumb.innerHTML = `
${escapeHtml(s.name)}
`; const img = thumb.querySelector('img'); window.api.screenshotData(currentDetailId, s.name).then((d) => { if (d) img.src = d; }); const toggle = () => { if (screenSelected.has(s.name)) screenSelected.delete(s.name); else screenSelected.add(s.name); updateScreensSelectionUi(); }; thumb.querySelector('.screen-check').addEventListener('click', (e) => { e.stopPropagation(); toggle(); }); thumb.addEventListener('click', (e) => { if (e.target.closest('.screen-del') || e.target.closest('.screen-check')) return; toggle(); }); img.addEventListener('dblclick', (e) => { e.stopPropagation(); window.api.openScreenshot(currentDetailId, s.name); }); thumb.querySelector('.screen-del').addEventListener('click', (e) => { e.stopPropagation(); askConfirm('Screenshot löschen', `„${s.name}" löschen?`, '🗑 Löschen', async () => { await window.api.deleteContent(currentDetailId, 'screenshots', s.name); openScreens(); }); }); grid.appendChild(thumb); } updateScreensSelectionUi(); } async function deleteSelectedScreenshots() { const names = [...screenSelected]; if (!names.length || !currentDetailId) return; const label = names.length === 1 ? `„${names[0]}" löschen?` : `${names.length} Screenshots unwiderruflich löschen?`; askConfirm('Screenshots löschen', label, '🗑 Löschen', async () => { for (const name of names) { await window.api.deleteContent(currentDetailId, 'screenshots', name); } toast(names.length === 1 ? 'Screenshot gelöscht.' : names.length + ' Screenshots gelöscht.'); openScreens(); }); } function renderContentList(boxId, items, sub, emoji) { const box = el(boxId); box.innerHTML = ''; if (!items.length) { box.innerHTML = '
Nichts vorhanden.
'; return; } for (const it of items) { const item = document.createElement('div'); item.className = 'mod-item'; item.innerHTML = `
${emoji}
${escapeHtml(it.name)}
`; const aktionen = item.querySelector('.mod-action'); if (sub === 'saves') { const sichern = document.createElement('button'); sichern.className = 'btn btn-mini'; sichern.textContent = 'Sichern'; sichern.title = 'Nur diese Welt jetzt als ZIP sichern (Dokumente\\AeroMC Launcher\\Welten-Sicherungen)'; sichern.addEventListener('click', async () => { sichern.disabled = true; const orig = sichern.textContent; sichern.textContent = 'Sichere …'; const res = await window.api.backupWorld(currentDetailId, it.name); sichern.disabled = false; sichern.textContent = orig; if (res.ok) toast(`„${it.name}" gesichert.`); else toast(res.message || 'Sicherung fehlgeschlagen.', true); }); aktionen.appendChild(sichern); } const rm = document.createElement('button'); rm.className = 'btn btn-danger btn-mini'; rm.textContent = 'Löschen'; rm.addEventListener('click', () => askConfirm( 'Löschen', `„${it.name}" wird unwiderruflich gelöscht. Fortfahren?`, '🗑 Löschen', async () => { await window.api.deleteContent(currentDetailId, sub, it.name); await renderContentLists(); toast('Gelöscht: ' + it.name); }, )); aktionen.appendChild(rm); box.appendChild(item); } } // ---------- Mod-Browser ---------- // Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader let modArt = 'mod'; const ART_LABEL = { mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader', datapack: 'Datenpakete', }; const ART_SUCHE = { mod: 'Mods durchsuchen … (z. B. Sodium, JEI, Create)', resourcepack: 'Ressourcenpakete durchsuchen … (z. B. Faithful, Bare Bones)', shader: 'Shader durchsuchen … (z. B. Complementary, BSL)', datapack: 'Datenpakete durchsuchen … (z. B. Terralith, Vanilla Tweaks)', }; // Nur Mods brauchen einen Loader – Ressourcenpakete, Shader und Datenpakete laufen auch in Vanilla const ART_BRAUCHT_LOADER = { mod: true, resourcepack: false, shader: false, datapack: false, }; // CurseForge hat für Datenpakete keinen verlässlich zugeordneten API-Klassencode in // diesem Launcher – dort deshalb nur Modrinth als Quelle anbieten, statt falsche // Treffer (z. B. Mods) unter "Datenpakete" anzuzeigen. const ART_NUR_MODRINTH = { datapack: true }; async function openMods() { const inst = await window.api.getInstance(currentDetailId); if (!inst) return; modArt = 'mod'; el('m-art').value = 'mod'; await zeigeModAnsicht(inst); show('modal-mods'); } // Wechsel der Inhaltsart: alles neu aufbauen async function modArtGewechselt() { modArt = el('m-art').value || 'mod'; const inst = await window.api.getInstance(currentDetailId); if (inst) await zeigeModAnsicht(inst); } async function zeigeModAnsicht(inst) { el('m-title').textContent = ART_LABEL[modArt] + ' – ' + inst.name; const loaderTxt = LOADER_LABEL[inst.minecraft.loader] || inst.minecraft.loader; el('m-sub').textContent = (inst.minecraft.version || 'keine Version') + ' · ' + loaderTxt; el('m-query').placeholder = ART_SUCHE[modArt]; const warn = el('m-warn'); const brauchtLoader = ART_BRAUCHT_LOADER[modArt]; const blocked = !inst.minecraft.version || (brauchtLoader && inst.minecraft.loader === 'vanilla'); if (blocked) { warn.classList.remove('hidden'); warn.textContent = !inst.minecraft.version ? '⚠ Diese Instanz hat keine Minecraft-Version. Bitte zuerst im Bearbeiten-Dialog eine Version wählen.' : '⚠ Vanilla-Instanzen unterstützen keine Mods. Wähle beim Bearbeiten einen Mod-Loader (Fabric/Forge/NeoForge/Quilt). Ressourcenpakete und Shader gehen auch ohne.'; } else { warn.classList.add('hidden'); } ['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; }); // Kategorien gelten nur für Mods el('m-category').disabled = blocked || modArt !== 'mod'; // Manche Quellen decken nicht jede Inhaltsart ab (z. B. CurseForge <-> Datenpakete) const cfOption = el('m-source').querySelector('option[value="curseforge"]'); if (cfOption) cfOption.disabled = !!ART_NUR_MODRINTH[modArt]; if (ART_NUR_MODRINTH[modArt] && el('m-source').value === 'curseforge') el('m-source').value = 'modrinth'; el('m-query').value = ''; el('m-more').classList.add('hidden'); // Ergebnisse der vorigen Ansicht nicht mitschleppen modUpdates = new Map(); setUpdateStatus(''); el('m-apply-updates').classList.add('hidden'); el('m-check-updates').disabled = blocked; switchModTab('catalog'); await refreshInstalled(); if (blocked) { const box = el('m-results'); box.innerHTML = `
${!inst.minecraft.version ? 'Ohne Minecraft-Version geht hier nichts.' : 'Für Mods braucht diese Instanz einen Mod-Loader.'}
`; const b = document.createElement('button'); b.className = 'btn btn-primary'; b.style.margin = '0 auto'; b.style.display = 'block'; b.textContent = inst.minecraft.version ? 'Mod-Loader hinzufügen' : 'Version wählen'; b.addEventListener('click', async () => { hide('modal-mods'); const i = await window.api.getInstance(currentDetailId); if (i) openEdit(i); }); box.appendChild(b); } else { await loadCatalog(false); // beliebte Einträge sofort anzeigen } } function switchModTab(which) { const cat = which === 'catalog'; el('m-tab-catalog').classList.toggle('on', cat); el('m-tab-installed').classList.toggle('on', !cat); el('m-pane-catalog').classList.toggle('hidden', !cat); el('m-pane-installed').classList.toggle('hidden', cat); } // gefundene Aktualisierungen: projectId -> { title, alt, neu } let modUpdates = new Map(); async function refreshInstalled() { const list = await window.api.modList(currentDetailId, modArt); currentModsInstalled = new Set(list.map((m) => m.projectId)); el('m-installed-count').textContent = String(list.length); const box = el('m-installed'); box.innerHTML = ''; if (!list.length) { box.innerHTML = '
Noch keine Mods.
'; return; } for (const m of list) { const up = m.projectId ? modUpdates.get(m.projectId) : null; const aus = m.enabled === false; const item = document.createElement('div'); item.className = 'mod-item' + (aus ? ' mod-aus' : ''); const kennzeichen = []; if (up) kennzeichen.push('Update'); if (aus) kennzeichen.push('aus'); if (m.fehlt) kennzeichen.push('Datei fehlt'); // OptiFine lässt sich nicht über Modrinth/CurseForge installieren (kein API-Zugriff auf // deren Downloads) – von Hand hinzugefügt taucht es sonst wie ein beliebiger Mod auf. const istOptiFine = modArt === 'mod' && /^optifine[_-]/i.test(m.filename || ''); if (istOptiFine) kennzeichen.push('OptiFine'); item.innerHTML = `
${aus ? '🚫' : istOptiFine ? '⚡' : '🧩'}
${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}
${up ? escapeHtml(up.alt.versionNumber) + ' → ' + escapeHtml(up.neu.versionNumber) : escapeHtml(m.versionNumber || '') + ' · ' + escapeHtml(m.filename)}
`; const aktionen = item.querySelector('.mod-action'); // Ab-/Anschalten: die Datei bleibt liegen, Minecraft übergeht sie nur if (!m.fehlt) { const schalter = document.createElement('button'); schalter.className = 'btn btn-mini'; schalter.textContent = aus ? 'Einschalten' : 'Ausschalten'; schalter.title = aus ? 'Datei wieder auf .jar umbenennen' : 'Datei auf .jar.disabled umbenennen – Minecraft lädt sie dann nicht'; schalter.addEventListener('click', async () => { schalter.disabled = true; const res = await window.api.modSetEnabled(currentDetailId, m.filename, aus, modArt); if (res.ok) toast((aus ? 'Eingeschaltet: ' : 'Ausgeschaltet: ') + m.title); else toast(res.message || 'Umschalten fehlgeschlagen.', true); await refreshInstalled(); }); aktionen.appendChild(schalter); } const btn = document.createElement('button'); btn.className = 'btn btn-danger btn-mini'; btn.textContent = 'Entfernen'; btn.addEventListener('click', async () => { const tatsaechlichEntfernen = async () => { await window.api.modRemove(currentDetailId, m.filename, modArt); toast('Entfernt: ' + m.title); await refreshInstalled(); }; const abhaenger = await window.api.modDependents(currentDetailId, m.filename, modArt); if (abhaenger.length) { const namen = abhaenger.map((n) => `„${n}"`).join(', '); askConfirm( 'Wird noch gebraucht', `${namen} ${abhaenger.length === 1 ? 'benötigt' : 'benötigen'} „${m.title}" als Pflicht-Abhängigkeit. ` + 'Wird sie trotzdem entfernt, funktionieren diese Mods vermutlich nicht mehr richtig. Trotzdem entfernen?', 'Trotzdem entfernen', tatsaechlichEntfernen, ); } else { await tatsaechlichEntfernen(); } }); aktionen.appendChild(btn); box.appendChild(item); } } // ---------- Mod-Aktualisierung ---------- function setUpdateStatus(text) { el('m-update-status').textContent = text || ''; } async function checkModUpdates() { const btn = el('m-check-updates'); btn.disabled = true; el('m-apply-updates').classList.add('hidden'); setUpdateStatus('Suche …'); try { const res = await window.api.modCheckUpdates(currentDetailId, modArt); if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Suche fehlgeschlagen.')); return; } modUpdates = new Map((res.treffer || []).map((t) => [t.projectId, t])); await refreshInstalled(); const teile = []; if (!res.treffer.length) teile.push('Alles aktuell'); else teile.push(res.treffer.length + (res.treffer.length === 1 ? ' Update verfügbar' : ' Updates verfügbar')); teile.push(res.geprueft + ' geprüft'); // von Hand abgelegte Dateien lassen sich nicht zuordnen if (res.uebersprungen) teile.push(res.uebersprungen + ' ohne Quelle übersprungen'); setUpdateStatus(teile.join(' · ')); el('m-apply-updates').classList.toggle('hidden', !res.treffer.length); } finally { btn.disabled = false; } } async function applyModUpdates() { if (!modUpdates.size) return; const btn = el('m-apply-updates'); btn.disabled = true; setUpdateStatus('Wird aktualisiert …'); try { const res = await window.api.modApplyUpdates(currentDetailId, [...modUpdates.keys()], modArt); if (!res.ok) { setUpdateStatus('✖ ' + (res.message || 'Aktualisierung fehlgeschlagen.')); return; } modUpdates = new Map(); await refreshInstalled(); const n = res.erneuert.length; const wort = n === 1 ? 'Eintrag' : 'Einträge'; setUpdateStatus(`${n} ${wort} aktualisiert` + (res.fehler.length ? ' · ' + res.fehler.length + ' fehlgeschlagen' : '')); if (n) toast(`${n} ${wort} aktualisiert.`); for (const f of res.fehler) toast(f.title + ': ' + f.message, true); btn.classList.add('hidden'); } finally { btn.disabled = false; } } let modOffset = 0; async function loadCatalog(append) { const inst = await window.api.getInstance(currentDetailId); if (!inst || !inst.minecraft.version) return; // Loader nur für Mods verlangen if (ART_BRAUCHT_LOADER[modArt] && inst.minecraft.loader === 'vanilla') return; const box = el('m-results'); if (!append) { modOffset = 0; box.innerHTML = '
Lade …
'; el('m-more').classList.add('hidden'); } const source = (el('m-source') && el('m-source').value) || 'modrinth'; if (el('m-category')) el('m-category').disabled = source === 'curseforge' || modArt !== 'mod'; try { const res = await window.api.modSearch({ query: el('m-query').value.trim(), gameVersion: inst.minecraft.version, loader: inst.minecraft.loader, category: (source === 'modrinth' && modArt === 'mod') ? el('m-category').value : '', sort: el('m-sort').value, limit: 30, offset: modOffset, art: modArt, source, }); if (!append) box.innerHTML = ''; if (res && (res.error || res.needKey)) { if (!append) { box.innerHTML = `
${escapeHtml(res.error || 'Suche nicht möglich.')}
`; if (res.needKey || /API-Key|Weiterleitung reicht/i.test(res.error || '')) { const wrap = document.createElement('div'); wrap.className = 'mods-empty'; const btn = document.createElement('button'); btn.className = 'btn btn-mini'; btn.textContent = 'Zu den Netzwerk-Einstellungen'; btn.addEventListener('click', () => { hide('modal-mods'); openSettings('pane-network'); }); wrap.appendChild(btn); box.appendChild(wrap); } } el('m-more').classList.add('hidden'); return; } const hits = (res && res.hits) || []; if (!hits.length && !append) { box.innerHTML = '
Nichts gefunden.
'; el('m-more').classList.add('hidden'); return; } for (const mod of hits) box.appendChild(buildResultItem(mod)); modOffset += hits.length; el('m-more').classList.toggle('hidden', hits.length < 30 || modOffset >= (res.total || 0)); } catch (e) { if (!append) { const msg = (e && e.message) ? e.message : String(e); box.innerHTML = `
Fehler: ${escapeHtml(msg)}
`; } } } function buildResultItem(mod) { const item = document.createElement('div'); item.className = 'mod-item'; const icon = document.createElement(mod.iconUrl ? 'img' : 'div'); icon.className = 'mod-icon'; if (mod.iconUrl) { icon.src = mod.iconUrl; icon.addEventListener('error', () => { const d = document.createElement('div'); d.className = 'mod-icon'; d.textContent = '🧩'; icon.replaceWith(d); }); } else { icon.textContent = '🧩'; } const info = document.createElement('div'); info.className = 'mod-info'; info.innerHTML = `
${escapeHtml(mod.title)}
${escapeHtml(mod.description || '')}
von ${escapeHtml(mod.author || 'unbekannt')} · ${Number(mod.downloads).toLocaleString('de-DE')} Downloads${mod.source === 'curseforge' ? ' · CurseForge' : ''}
`; const action = document.createElement('div'); action.className = 'mod-action'; const btn = document.createElement('button'); btn.className = 'btn btn-primary btn-mini'; const already = currentModsInstalled.has(mod.projectId); if (already) { btn.textContent = '✓ Installiert'; btn.disabled = true; } else { btn.textContent = 'Installieren'; btn.addEventListener('click', (e) => { e.stopPropagation(); installMod(mod, btn, item); }); // ganze Zeile klickbar: ein Klick installiert Mod + alle Abhängigkeiten item.classList.add('clickable'); item.title = 'Klicken zum Installieren (inkl. Abhängigkeiten)'; item.addEventListener('click', () => { if (!btn.disabled) installMod(mod, btn, item); }); } action.appendChild(btn); item.appendChild(icon); item.appendChild(info); item.appendChild(action); return item; } async function installMod(mod, btn, item) { btn.disabled = true; btn.textContent = 'Lädt …'; if (item) { item.classList.add('installing'); item.classList.remove('clickable'); } const res = await window.api.modInstall(currentDetailId, { projectId: mod.projectId, title: mod.title }, modArt); if (item) item.classList.remove('installing'); if (res.ok) { const dep = res.depCount ? ` (+${res.depCount} Abhängigkeit${res.depCount > 1 ? 'en' : ''})` : ''; toast('Installiert: ' + mod.title + dep); btn.textContent = '✓ Installiert'; currentModsInstalled.add(mod.projectId); await refreshInstalled(); } else { toast(res.message || 'Installation fehlgeschlagen.', true); btn.disabled = false; btn.textContent = 'Installieren'; if (item) item.classList.add('clickable'); } } // ---------- Einstellungen ---------- function setSettingsPane(paneId) { document.querySelectorAll('.snav').forEach((button) => button.classList.toggle('on', button.dataset.pane === paneId)); document.querySelectorAll('.spane').forEach((pane) => pane.classList.toggle('hidden', pane.id !== paneId)); } // pane: optionaler Bereich, der direkt geöffnet werden soll (z. B. 'pane-network') async function openSettings(pane) { setSettingsPane(pane || 'pane-general'); const s = await window.api.getSettings(); el('s-theme').value = s.theme || 'dark'; el('s-global-pre').value = s.globalPreLaunch || ''; el('s-global-post').value = s.globalPostExit || ''; el('s-wrapper').value = s.wrapperCommand || ''; // Skin-Hintergrund: Auswahl aus allen angemeldeten Konten const accs = await window.api.authAccounts(); const sel = el('s-bgskin'); sel.innerHTML = ''; 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 = ''; await refreshJavaStatus(s.javaPath); const info = await window.api.appInfo(); el('s-appinfo').innerHTML = [ ['Name', info.name || 'AeroMC'], ['Version', formatVersionLabel(info.version)], ['Autor', info.author || 'Nicht hinterlegt'], ['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''], ['Launcher-Webseite', info.launcherWebsite || 'Nicht hinterlegt', info.launcherWebsite || ''], ['Lizenz', 'Nur privater, nicht-kommerzieller Gebrauch – Details anzeigen', null, 'license'], ].map(([label, value, url, action]) => { const renderedValue = url ? `${escapeHtml(value)}` : action ? `${escapeHtml(value)}` : escapeHtml(value || '–'); return `
${escapeHtml(label)}${renderedValue}
`; }).join(''); for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) { link.addEventListener('click', async (evt) => { evt.preventDefault(); const url = link.dataset.url; if (url) { window.api.openExternal(url); return; } if (link.dataset.action === 'license') { show('modal-license'); el('license-text').textContent = await window.api.appLicense(); } }); } show('modal-settings'); await renderAccounts('s-accounts'); // Konten direkt in den Einstellungen zeigen applyBedrockVisibility(); // aktualisiert auch den Hinweistext } async function refreshJavaStatus(javaPath) { const box = el('s-java-status'); box.className = 'hint'; box.textContent = 'Prüfe Java …'; const r = await window.api.detectJava(javaPath); if (r.ok) { box.classList.add('status-ok'); // grün, wenn Java passt box.textContent = '✔ ' + r.version; } else { box.classList.add('status-bad'); box.textContent = '✖ Java nicht gefunden (' + (r.error || 'unbekannt') + ')'; } } async function scanJava() { const btn = el('s-scan-java'); btn.disabled = true; btn.textContent = 'Suche …'; try { const jvms = await window.api.scanJava(); const sel = el('s-java-detected'); sel.innerHTML = ''; 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 = ''; 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 = ` `; 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 } });