${up
@@ -3123,17 +3192,24 @@ async function openSettings(pane) {
['Autor', info.author || 'Nicht hinterlegt'],
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
['Launcher-Webseite', info.launcherWebsite || 'Nicht hinterlegt', info.launcherWebsite || ''],
- ].map(([label, value, url]) => {
+ ['Lizenz', 'Nur privater, nicht-kommerzieller Gebrauch – Details anzeigen', null, 'license'],
+ ].map(([label, value, url, action]) => {
const renderedValue = url
? `
${escapeHtml(value)}`
- : 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', (evt) => {
+ link.addEventListener('click', async (evt) => {
evt.preventDefault();
const url = link.dataset.url;
- if (url) window.api.openExternal(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');
@@ -3859,6 +3935,47 @@ function wire() {
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;
diff --git a/src/services.js b/src/services.js
index 43ba32e..b08e399 100644
--- a/src/services.js
+++ b/src/services.js
@@ -236,6 +236,7 @@ const INHALTSARTEN = {
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
+ datapack: { projectType: 'datapack', ordner: 'datapacks', label: 'Datenpakete', loaderNoetig: false },
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
};
@@ -515,7 +516,9 @@ function forgeCdnUrl(fileId, fileName) {
}
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
-const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552 };
+// classId je Inhaltsart bei gameId 432 (Minecraft). modpack=4471 ist die auf
+// CurseForge selbst verwendete Kategorie-ID (siehe curseforge.com/minecraft/modpacks).
+const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552, modpack: 4471 };
function cfLoaderType(loader) {
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
@@ -598,7 +601,12 @@ async function curseforgeSearch(zugang, {
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
};
}
- const classId = CF_CLASS[art] || CF_CLASS.mod;
+ if (!CF_CLASS[art]) {
+ // Kein zugeordneter CurseForge-Klassencode für diese Inhaltsart -> lieber gar
+ // keine Treffer als falsch einsortierte (z. B. Mods unter "Datenpakete").
+ return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' };
+ }
+ const classId = CF_CLASS[art];
const url = new URL(CURSEFORGE + '/mods/search');
url.searchParams.set('gameId', '432');
url.searchParams.set('classId', String(classId));
@@ -713,6 +721,12 @@ async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
};
}
+// Beste Modpack-Datei eines CurseForge-Projekts auflösen (für den Katalog-Import,
+// analog zu modrinthResolveModpack).
+async function curseforgeResolveModpackFile(zugang, modId, gameVersion) {
+ return curseforgePickFile(zugang, modId, gameVersion, '', 'modpack');
+}
+
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
const rawId = String(projectId || '').replace(/^cf-/, '');
@@ -814,4 +828,5 @@ module.exports = {
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
+ curseforgeResolveModpackFile,
};
diff --git a/src/store.js b/src/store.js
index ebb881b..1ea26f8 100644
--- a/src/store.js
+++ b/src/store.js
@@ -134,8 +134,12 @@ function gameDir(id) {
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
* weiterlaufen.
*/
-const INHALT_ORDNER = { mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks' };
-const INHALT_INDEX = { mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json' };
+const INHALT_ORDNER = {
+ mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
+};
+const INHALT_INDEX = {
+ mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
+};
function inhaltsDir(id, art = 'mod') {
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
diff --git a/src/updater.js b/src/updater.js
index e76a240..84e0acc 100644
--- a/src/updater.js
+++ b/src/updater.js
@@ -230,8 +230,17 @@ async function downloadAndInstall(asset, onProgress) {
const dir = updateDir();
fs.mkdirSync(dir, { recursive: true });
- const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
- fs.rmSync(dest, { force: true });
+ let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
+ try {
+ fs.rmSync(dest, { force: true });
+ } catch {
+ // Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
+ // Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
+ // EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
+ // cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
+ const parsed = path.parse(dest);
+ dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
+ }
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);