@@ -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