Skip to content

Commit a08667d

Browse files
authored
Feat/mods
* feat(mods): add Modrinth-app like version switcher + more install button states * fix: app_pojavlauncher/src/main/res/drawable/ic_swap_horiz.xml (forgot to hit Ctrl+S) * ModItemAdapter.java:405: error: static declarations not allowed in inner classes private enum InstallButtonState { INSTALL, INSTALLED, UPDATE, DOWNGRADE } * fix(mods): delete old jar on update/downgrade, fix collapsed switch-version dialog * fix(mods): show all versions by default in switch-version dialog, add diagnostic logging Signed-off-by: MaxJubayerYT <maxjubayergaming@gmail.com> * fix(mods): use fixed-height content area in switch-version dialog instead of unreliable window resize * fix(mods): sync switch-version dialog colors with active app theme * fix(mods): hide incompatible versions by default in switch-version dialog * refactor(mods): rebuild switch-version dialog + rows to match native app UI patterns * fix(mods): match version row + dialog to native UI style, fix icons, remove cancel button * fix(mods): Just use normal icons I gave up * fix(mods): Nah I like the pixlated icons (made by @Crex56) --------- Signed-off-by: MaxJubayerYT <maxjubayergaming@gmail.com>
1 parent 5722d7b commit a08667d

17 files changed

Lines changed: 894 additions & 21 deletions

app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -279,22 +279,83 @@ private static class ModsInstallApi extends CommonApi {
279279
/**
280280
* Override getModDetails to filter versions by the selected MC version.
281281
* Only versions matching the filter are shown in the version dropdown.
282+
* Also resolves which (if any) of the returned versions is already
283+
* installed in the current instance's mods folder, so the install
284+
* button can show Install / Installed / Update / Downgrade.
282285
*/
283286
@Override
284287
public ModDetail getModDetails(ModItem item) {
288+
ModDetail detail;
285289
if (item.apiSource == net.kdt.pojavlaunch.modloaders.modpacks.models.Constants.SOURCE_MODRINTH) {
286290
String filterVer = (mFilters.mcVersion != null && !mFilters.mcVersion.isEmpty())
287291
? mFilters.mcVersion : null;
288292
String filterLoader = (mFilters.modLoader != null && !mFilters.modLoader.isEmpty())
289293
? mFilters.modLoader : null;
290-
return mModrinthApi.getModDetails(item, filterVer, filterLoader);
291-
}
292-
if (item.apiSource == net.kdt.pojavlaunch.modloaders.modpacks.models.Constants.SOURCE_CURSEFORGE) {
294+
detail = mModrinthApi.getModDetails(item, filterVer, filterLoader);
295+
} else if (item.apiSource == net.kdt.pojavlaunch.modloaders.modpacks.models.Constants.SOURCE_CURSEFORGE) {
293296
String filterVer = (mFilters.mcVersion != null && !mFilters.mcVersion.isEmpty())
294297
? mFilters.mcVersion : null;
295-
return mCurseforgeApi.getModDetails(item, filterVer);
298+
detail = mCurseforgeApi.getModDetails(item, filterVer);
299+
} else {
300+
detail = super.getModDetails(item);
301+
}
302+
303+
if (detail != null && !detail.isModpack) {
304+
resolveInstalledVersionIndex(detail);
305+
}
306+
return detail;
307+
}
308+
309+
/**
310+
* Hashes every jar currently in the mods folder (enabled or disabled)
311+
* and matches against detail.versionHashes to find which version, if
312+
* any, is already installed. Sets detail.installedVersionIndex
313+
* accordingly (-1 if this mod isn't installed at all).
314+
*/
315+
private void resolveInstalledVersionIndex(ModDetail detail) {
316+
detail.installedVersionIndex = -1;
317+
detail.installedFilePath = null;
318+
if (detail.versionHashes == null || detail.versionHashes.length == 0) return;
319+
320+
File modsDir = getModsDir();
321+
File[] files = modsDir.listFiles(f -> f.isFile() &&
322+
(f.getName().endsWith(".jar") || f.getName().endsWith(".jar.disabled")));
323+
if (files == null || files.length == 0) return;
324+
325+
java.util.Map<String, File> installedHashToFile = new java.util.HashMap<>();
326+
for (File f : files) {
327+
String hash = sha1Hex(f);
328+
if (hash != null) installedHashToFile.put(hash, f);
329+
}
330+
if (installedHashToFile.isEmpty()) return;
331+
332+
for (int i = 0; i < detail.versionHashes.length; i++) {
333+
String hash = detail.versionHashes[i];
334+
if (hash == null) continue;
335+
File match = installedHashToFile.get(hash.toLowerCase(java.util.Locale.ROOT));
336+
if (match != null) {
337+
detail.installedVersionIndex = i;
338+
detail.installedFilePath = match.getAbsolutePath();
339+
break;
340+
}
341+
}
342+
}
343+
344+
private static String sha1Hex(File file) {
345+
try {
346+
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1");
347+
byte[] buffer = new byte[8192];
348+
try (java.io.FileInputStream fis = new java.io.FileInputStream(file)) {
349+
int read;
350+
while ((read = fis.read(buffer)) != -1) digest.update(buffer, 0, read);
351+
}
352+
byte[] bytes = digest.digest();
353+
StringBuilder sb = new StringBuilder(bytes.length * 2);
354+
for (byte b : bytes) sb.append(String.format("%02x", b));
355+
return sb.toString();
356+
} catch (Exception e) {
357+
return null;
296358
}
297-
return super.getModDetails(item);
298359
}
299360

300361
@Override
@@ -338,13 +399,19 @@ public void handleInstallation(Context context, ModDetail modDetail, int selecte
338399
if (rawName.contains("?")) rawName = rawName.substring(0, rawName.indexOf('?'));
339400
final String fileName = rawName.endsWith(".jar") ? rawName : rawName + ".jar";
340401

402+
// If a different version of this same mod is already installed under
403+
// a different file name (the Update/Downgrade case), remember its path
404+
// so downloadMod() can remove it once the new file is safely down —
405+
// otherwise both versions end up sitting in the mods folder together.
406+
final String oldFilePath = modDetail.installedFilePath;
407+
341408
// Check if this version has dependencies
342409
String[] depIds = (modDetail.versionDependencyIds != null) ? modDetail.versionDependencyIds[selectedVersion] : null;
343410
String[] depTypes = (modDetail.versionDependencyTypes != null) ? modDetail.versionDependencyTypes[selectedVersion] : null;
344411

345412
if (depIds == null || depIds.length == 0) {
346413
// No deps — download directly
347-
downloadMod(context, url, fileName, new String[0], new String[0]);
414+
downloadMod(context, url, fileName, new String[0], new String[0], oldFilePath);
348415
return;
349416
}
350417

@@ -372,15 +439,15 @@ public void handleInstallation(Context context, ModDetail modDetail, int selecte
372439
labels[idx] = prefix + (name != null ? name : projectId);
373440
if (remaining.decrementAndGet() == 0) {
374441
mMainHandler.post(() -> showDepsDialog(context, url, fileName,
375-
depIds, depTypes, labels, checkedDefaults));
442+
depIds, depTypes, labels, checkedDefaults, oldFilePath));
376443
}
377444
});
378445
}
379446
}
380447

381448
private void showDepsDialog(Context context, String url, String fileName,
382449
String[] depIds, String[] depTypes,
383-
String[] labels, boolean[] checkedDefaults) {
450+
String[] labels, boolean[] checkedDefaults, String oldFilePath) {
384451
// context here is getApplicationContext() from ModItemAdapter — no window token.
385452
// Use the stored Activity reference instead.
386453
Context dialogCtx = mActivityContext != null ? mActivityContext : context;
@@ -396,24 +463,37 @@ private void showDepsDialog(Context context, String url, String fileName,
396463
if (selected[i]) selectedIds.add(depIds[i]);
397464
}
398465
downloadMod(context, url, fileName,
399-
selectedIds.toArray(new String[0]), depTypes);
466+
selectedIds.toArray(new String[0]), depTypes, oldFilePath);
400467
})
401468
.setNeutralButton(R.string.mod_deps_install_without,
402-
(d, w) -> downloadMod(context, url, fileName, new String[0], new String[0]))
469+
(d, w) -> downloadMod(context, url, fileName, new String[0], new String[0], oldFilePath))
403470
.setNegativeButton(android.R.string.cancel, null)
404471
.show();
405472
}
406473

407474
private void downloadMod(Context context, String url, String fileName,
408-
String[] depIds, String[] depTypes) {
475+
String[] depIds, String[] depTypes, @Nullable String oldFilePath) {
409476
File modsDir = getModsDir();
410477
if (!modsDir.exists()) modsDir.mkdirs();
411478

479+
final File targetFile = new File(modsDir, fileName);
480+
412481
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.global_waiting);
413482
PojavApplication.sExecutorService.execute(() -> {
414483
try {
415484
// Download main mod
416-
DownloadUtils.downloadFile(url, new File(modsDir, fileName));
485+
DownloadUtils.downloadFile(url, targetFile);
486+
487+
// This is an update/downgrade of an already-installed mod under a
488+
// different file name — remove the old jar now that the new one
489+
// downloaded successfully, so both versions don't end up sitting
490+
// in the mods folder side-by-side.
491+
if (oldFilePath != null) {
492+
File oldFile = new File(oldFilePath);
493+
if (oldFile.exists() && !oldFile.getAbsolutePath().equals(targetFile.getAbsolutePath())) {
494+
oldFile.delete();
495+
}
496+
}
417497

418498
// Download selected dependencies
419499
for (String depId : depIds) {

0 commit comments

Comments
 (0)