Upload via GUI (123 Dateien)

This commit is contained in:
2026-10-03 07:09:09 +00:00
parent 10ee45cbc2
commit e5ccf0b4cc
7 changed files with 2446 additions and 787 deletions
+146 -8
View File
@@ -537,13 +537,23 @@ function escapeHtml(s) {
}
function numOrNull(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : null; }
// Designs: 'dark'/'light' = Klassisch (MultiMC-Look), 'aero' = wie die Webseite,
// 'lila' = Aero in dunklem Lila, 'minecraft' = Block-Optik. Alle außer Klassisch
// bauen auf den dunklen Regeln auf (data-theme="dark") und überschreiben sie über data-design.
const DESIGNS = ['dark', 'light', 'aero', 'lila', 'minecraft'];
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark');
const t = DESIGNS.includes(theme) ? theme : 'dark';
const root = document.documentElement;
root.setAttribute('data-theme', t === 'light' ? 'light' : 'dark');
root.setAttribute('data-design', ['aero', 'lila', 'minecraft'].includes(t) ? t : 'klassisch');
if (!POPOUT && window.api.setTitleBarDesign) window.api.setTitleBarDesign(t);
document.querySelectorAll('.design-wahl').forEach((b) => b.classList.toggle('on', b.dataset.design === t));
}
// ---------- Instanzen aus MultiMC übernehmen ----------
let mmcFound = [];
let cfFound = [];
let mrFound = [];
let mcFound = [];
let technicFound = [];
let importKind = 'zip';
@@ -568,8 +578,8 @@ async function ensureDefaultMcScan() {
}
function setImportKind(kind) {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mmc', 'technic', 'code'].forEach((entryKind) => {
importKind = ['zip', 'modpack', 'mc', 'cf', 'mr', 'mmc', 'technic', 'code'].includes(kind) ? kind : 'zip';
['zip', 'modpack', 'mc', 'cf', 'mr', 'mmc', 'technic', 'code'].forEach((entryKind) => {
el(`import-tab-${entryKind}`).classList.toggle('on', entryKind === importKind);
el(`import-pane-${entryKind}`).classList.toggle('hidden', entryKind !== importKind);
});
@@ -577,12 +587,14 @@ function setImportKind(kind) {
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('mr-do').classList.toggle('hidden', importKind !== 'mr');
el('mmc-do').classList.toggle('hidden', importKind !== 'mmc');
el('technic-do').classList.toggle('hidden', importKind !== 'technic');
el('code-do').classList.toggle('hidden', importKind !== 'code');
el('code-progress').classList.toggle('hidden', importKind !== 'code' || !el('code-progress').textContent);
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('mr-progress').classList.toggle('hidden', importKind !== 'mr' || !el('mr-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);
}
@@ -601,6 +613,12 @@ function openImportDialog(kind = 'zip', vorauswahl = null) {
el('cf-progress').textContent = '';
el('cf-progress').classList.add('hidden');
el('cf-status').textContent = 'Standardpfad unter Windows: C:\\Users\\DeinName\\curseforge\\minecraft\\Instances';
mrFound = [];
el('mr-result').classList.add('hidden');
el('mr-do').disabled = true;
el('mr-progress').textContent = '';
el('mr-progress').classList.add('hidden');
el('mr-status').textContent = 'Leer lassen nutzt automatisch den Standardort der Modrinth App.';
el('mmc-result').classList.add('hidden');
el('mmc-do').disabled = true;
el('mmc-progress').textContent = '';
@@ -613,6 +631,8 @@ function openImportDialog(kind = 'zip', vorauswahl = null) {
ensureDefaultMcScan();
}
if (importKind === 'cf') el('cf-path').focus();
// Modrinth App: Standardort gleich durchsuchen, wie beim Minecraft Launcher
if (importKind === 'mr' && !(Array.isArray(vorauswahl) && vorauswahl.length)) scanMr();
if (importKind === 'mmc') el('mmc-path').focus();
if (Array.isArray(vorauswahl) && vorauswahl.length) uebernehmeSetupVorauswahl(vorauswahl);
}
@@ -622,7 +642,7 @@ function openImportDialog(kind = 'zip', vorauswahl = null) {
* eintragen und durchsuchen, damit man beim ersten Start nur noch auswählt.
*/
function uebernehmeSetupVorauswahl(quellen) {
const scanner = { mc: scanMc, cf: scanCf, mmc: scanMmc, technic: scanTechnic };
const scanner = { mc: scanMc, cf: scanCf, mr: scanMr, mmc: scanMmc, technic: scanTechnic };
importWarteschlange = quellen.map((q) => q && q.kind).filter((k) => scanner[k]);
for (const q of quellen) {
if (!q || !scanner[q.kind]) continue;
@@ -923,6 +943,82 @@ async function runCfImport() {
if (instances.length) await selectInstance(instances[0].id);
}
async function scanMr() {
const folder = el('mr-path').value.trim();
el('mr-status').textContent = 'Suche Instanzen der Modrinth App …';
const res = await window.api.mrScan(folder);
if (!res.ok) {
el('mr-status').textContent = folder
? '✖ Dort wurden keine Daten der Modrinth App gefunden. Gemeint ist der Ordner mit „app.db“ und „profiles“.'
: '✖ Die Modrinth App wurde am Standardort nicht gefunden. Ist sie installiert? Sonst den Datenordner wählen.';
el('mr-result').classList.add('hidden');
el('mr-do').disabled = true;
return;
}
mrFound = res.instances;
el('mr-status').textContent = `${res.count} Instanz${res.count === 1 ? '' : 'en'} gefunden (${res.basis}).`
+ (res.hinweis ? ' ⚠ ' + res.hinweis : '');
el('mr-result').classList.remove('hidden');
el('mr-all').checked = true;
renderMrList();
}
function renderMrList() {
const box = el('mr-list');
box.innerHTML = '';
if (!mrFound.length) { box.innerHTML = '<div class="mods-empty">Keine Instanzen gefunden.</div>'; return; }
mrFound.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 : '',
inst.modpack ? 'Modrinth-Modpack' : '', inst.playSeconds ? fmtDuration(inst.playSeconds) + ' gespielt' : '',
inst.installiert === false ? 'nicht fertig installiert' : '']
.filter(Boolean).join(' · ');
row.innerHTML =
`<input type="checkbox" data-idx="${idx}" checked />
<div class="mod-info">
<div class="mmc-name">${escapeHtml(inst.name)}</div>
<div class="mmc-meta">${escapeHtml(meta)}</div>
</div>`;
box.appendChild(row);
});
updateMrButton();
}
function selectedMr() {
return [...document.querySelectorAll('#mr-list input[type=checkbox]')]
.filter((c) => c.checked)
.map((c) => mrFound[Number(c.dataset.idx)]);
}
function updateMrButton() {
const n = selectedMr().length;
el('mr-do').disabled = n === 0;
el('mr-do').textContent = n ? `${n} übernehmen` : 'Übernehmen';
}
async function runMrImport() {
const entries = selectedMr();
if (!entries.length) return;
const copy = el('mr-copy').checked;
el('mr-do').disabled = true;
el('mr-progress').textContent = 'Übernehme …';
el('mr-progress').classList.remove('hidden');
const res = await window.api.mrImport(entries, copy);
await reload();
importAbgeschlossen('mr');
const n = res.imported.length;
const f = res.failed.length;
const ohneLink = res.imported.filter((i) => i.modpack === false).length;
toast(`${n} Instanz${n === 1 ? '' : 'en'} aus der Modrinth App übernommen${f ? ` · ${f} fehlgeschlagen` : ''}`
+ (ohneLink ? ` · ${ohneLink}× Modpack-Verknüpfung nicht möglich (offline?)` : '') + '.', 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; }
@@ -1464,7 +1560,11 @@ async function applySkinBackground(uuidOverride) {
// 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');
// Armform: fest gewählt oder (Standard) die des Kontos – sonst sieht ein
// schmaler Alex-Skin mit breiten Armen falsch aus (grauer Streifen am Arm)
let modell = s.bgSkinModel === 'slim' || s.bgSkinModel === 'classic' ? s.bgSkinModel : '';
if (!modell) { try { modell = await window.api.skinModelFor(uuid); } catch { modell = 'classic'; } }
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking', { modell });
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
// 2) Rückfall: externe Render-Dienste
@@ -1630,7 +1730,7 @@ const ICONS_SVG = {
terminal: '<path d="M4 5h16v14H4zM7 9l3 3-3 3M13 16h4"/>',
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z"/>',
users: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>',
settings: '<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M7 17l-2 2"/>',
settings: '<path d="M19.1 13.1 21.5 14.5 20.5 16.9 17.8 16.3 16.3 17.8 16.9 20.5 14.5 21.5 13.1 19.1 10.9 19.1 9.5 21.5 7.1 20.5 7.7 17.8 6.2 16.3 3.5 16.9 2.5 14.5 4.9 13.1 4.9 10.9 2.5 9.5 3.5 7.1 6.2 7.7 7.7 6.2 7.1 3.5 9.5 2.5 10.9 4.9 13.1 4.9 14.5 2.5 16.9 3.5 16.3 6.2 17.8 7.7 20.5 7.1 21.5 9.5 19.1 10.9Z"/><circle cx="12" cy="12" r="3"/>', // Zahnrad (8 Zähne)
play: '<path d="M7 5l12 7-12 7z"/>',
blocks: '<path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/>',
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 010 18M12 3a15 15 0 000 18"/>',
@@ -4790,7 +4890,8 @@ async function openSettings(pane) {
window.api.updateLastCheck().then(renderUpdateStatus).catch(() => {});
const s = await window.api.getSettings();
el('s-theme').value = s.theme || 'dark';
el('s-theme').value = DESIGNS.includes(s.theme) ? s.theme : 'dark';
applyTheme(el('s-theme').value);
el('s-global-pre').value = s.globalPreLaunch || '';
el('s-global-post').value = s.globalPostExit || '';
el('s-wrapper').value = s.wrapperCommand || '';
@@ -4806,6 +4907,7 @@ async function openSettings(pane) {
}
sel.value = s.bgSkin || '';
el('s-bgskin-pose').value = s.bgSkinPose || 'walking';
el('s-bgskin-model').value = ['classic', 'slim'].includes(s.bgSkinModel) ? s.bgSkinModel : 'auto';
el('s-bgskin-cycle').checked = !!s.bgSkinCycle;
el('s-bgskin-cycle-seconds').value = s.bgSkinCycleSeconds || 30;
sel.disabled = !!s.bgSkinCycle;
@@ -4939,6 +5041,7 @@ async function saveSettings() {
globalPostExit: el('s-global-post').value.trim(),
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
bgSkinModel: el('s-bgskin-model').value,
bgSkinCycle: el('s-bgskin-cycle').checked,
bgSkinCycleSeconds: Math.max(5, numOrNull(el('s-bgskin-cycle-seconds').value) || 30),
sortBy: el('s-sortby').value,
@@ -5471,6 +5574,9 @@ function wire() {
});
document.addEventListener('scroll', closeContextMenu, true);
window.addEventListener('blur', closeContextMenu);
// Titelleiste wie bei Windows abblenden, solange das Fenster nicht aktiv ist
window.addEventListener('blur', () => document.body.classList.add('fenster-inaktiv'));
window.addEventListener('focus', () => document.body.classList.remove('fenster-inaktiv'));
// Warnung anklicken -> sofort erneut prüfen
el('status-auth').addEventListener('click', async () => {
@@ -5530,6 +5636,18 @@ function wire() {
});
el('cf-list').addEventListener('change', updateCfButton);
el('cf-do').addEventListener('click', runCfImport);
el('mr-pick').addEventListener('click', async () => {
const p = await window.api.mrPickFolder();
if (p) { el('mr-path').value = p; await scanMr(); }
});
el('mr-scan').addEventListener('click', scanMr);
el('mr-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') scanMr(); });
el('mr-all').addEventListener('change', (e) => {
document.querySelectorAll('#mr-list input[type=checkbox]').forEach((c) => { c.checked = e.target.checked; });
updateMrButton();
});
el('mr-list').addEventListener('change', updateMrButton);
el('mr-do').addEventListener('click', runMrImport);
el('mmc-pick').addEventListener('click', async () => {
const p = await window.api.mmcPickFolder();
if (p) { el('mmc-path').value = p; await scanMmc(); }
@@ -5567,6 +5685,10 @@ function wire() {
el('cf-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('cf-progress').classList.remove('hidden');
});
window.api.onMrProgress((p) => {
el('mr-progress').textContent = p.detail ? `${p.detail} …` : `Übernehme … ${p.current}/${p.total}`;
el('mr-progress').classList.remove('hidden');
});
window.api.onMmcProgress((p) => {
el('mmc-progress').textContent = `Übernehme … ${p.current}/${p.total}`;
el('mmc-progress').classList.remove('hidden');
@@ -5804,6 +5926,10 @@ function wire() {
el('skin-pick').addEventListener('click', waehleSkinDatei);
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
el('skin-apply').addEventListener('click', setzeSkin);
// gewählte Datei (sonst der aktuelle Skin des Kontos) im Editor öffnen
el('skin-editor').addEventListener('click', () => window.openSkinEditor(gewaehlterSkin
? { pfad: gewaehlterSkin, variante: el('skin-variante').value }
: { quelle: 'aktuell' }));
el('skin-reset').addEventListener('click', setzeSkinZurueck);
el('logs-select').addEventListener('change', (e) => ladeLog(e.target.value));
el('logs-folder').addEventListener('click', async () => {
@@ -5986,6 +6112,11 @@ function wire() {
if (p) { el('s-java').value = p; await refreshJavaStatus(p); }
});
el('s-theme').addEventListener('change', (e) => applyTheme(e.target.value));
// Design-Kacheln setzen die (versteckte) Auswahl und zeigen es sofort an
document.querySelectorAll('.design-wahl').forEach((b) => b.addEventListener('click', () => {
el('s-theme').value = b.dataset.design;
applyTheme(b.dataset.design);
}));
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));
@@ -5995,11 +6126,13 @@ function wire() {
await window.api.saveSettings({
bgSkin: el('s-bgskin').value,
bgSkinPose: el('s-bgskin-pose').value,
bgSkinModel: el('s-bgskin-model').value,
});
await refreshSkinBackground();
};
el('s-bgskin').addEventListener('change', previewSkin);
el('s-bgskin-pose').addEventListener('change', previewSkin);
el('s-bgskin-model').addEventListener('change', previewSkin);
const previewSkinCycle = async () => {
el('s-bgskin').disabled = el('s-bgskin-cycle').checked;
await window.api.saveSettings({
@@ -6240,6 +6373,7 @@ const POPOUT_OPENERS = {
screens: () => openScreens(),
servers: () => openServers(),
skin: () => openSkin(),
skineditor: (p) => window.openSkinEditor(p),
bedrock: () => openBedrock(),
login: (p) => openLogin({ addAccount: p.addAccount === true || p.addAccount === 'true' }),
import: (p) => {
@@ -6398,7 +6532,11 @@ window.addEventListener('DOMContentLoaded', async () => {
// Ein Skin-Wechsel oder eine neue/aktualisierte Instanz aus einem anderen
// Fenster (Skin-/Import-Popup) betrifft ebenfalls nur die Anzeige im
// Hauptfenster (Skin-Hintergrund, Seitenleiste) – Popups zeigen beides nicht an
window.api.onSkinChanged(() => { if (!POPOUT) refreshSkinBackground(); });
window.api.onSkinChanged((info) => {
if (!POPOUT) refreshSkinBackground();
// im Editor gesetzt -> offenes Skin-Fenster zeigt sonst noch den alten Stand
if (POPOUT === 'skin' && info.quelle === 'editor' && !el('modal-skin').classList.contains('hidden')) openSkin();
});
window.api.onInstancesChanged(() => { if (!POPOUT) reload(); });
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
await loadMcIcons(); // echte Minecraft-Icons bereitstellen