From 21d08ed34dbb66d695780cd8003ab4865e226f49 Mon Sep 17 00:00:00 2001 From: MaxJubayerYT Date: Mon, 27 Apr 2026 10:40:50 +0600 Subject: [PATCH 1/4] Feat: update strings.xml --- .../fragments/ModsSearchFragment.java | 178 ++++++++++++++---- .../modloaders/modpacks/api/ModrinthApi.java | 75 ++++++-- .../src/main/res/values/strings.xml | 3 + 3 files changed, 203 insertions(+), 53 deletions(-) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java index d22b0c13ee..f7ff39ee48 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java @@ -4,6 +4,8 @@ import android.content.res.ColorStateList; import android.graphics.Color; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.widget.Toast; import android.view.View; @@ -29,9 +31,10 @@ import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter; import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi; import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModrinthApi; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; -import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; import net.kdt.pojavlaunch.profiles.VersionSelectorDialog; @@ -41,10 +44,14 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; /** - * Searches and installs individual mods (not modpacks) into the current instance's mods folder. - * Auto-detects MC version from the selected profile's lastVersionId. + * Searches and installs individual mods into the current instance's mods folder. + * - Version filter: when an MC version is selected, only versions matching it are shown. + * - Dependency dialog: shown before download, matching the ModBundle UI. */ public class ModsSearchFragment extends Fragment implements ModItemAdapter.SearchResultCallback { @@ -74,15 +81,13 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { public ModsSearchFragment() { super(R.layout.fragment_mod_search); mSearchFilters = new SearchFilters(); - mSearchFilters.isModpack = false; // individual mods, not modpacks + mSearchFilters.isModpack = false; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - // Wrap CommonApi so installation goes to mods folder instead of creating a new instance - mModpackApi = new ModsInstallApi(context.getString(R.string.curseforge_api_key)); - // Auto-detect MC version from current profile + mModpackApi = new ModsInstallApi(context.getString(R.string.curseforge_api_key), mSearchFilters); } @Override @@ -120,10 +125,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat }); mFilterButton.setOnClickListener(v -> displayFilterDialog()); - - // Override hint — the layout uses fragment_mod_search which says "Search for modpacks" mSearchEditText.setHint(R.string.hint_search_mod); - searchMods(null); } @@ -192,26 +194,36 @@ private void displayFilterDialog() { dialog.show(); } - // ── Helpers ────────────────────────────────────────────────────────────── - - /** Parse MC version from lastVersionId like "fabric-loader-0.16.14-1.21.4" → "1.21.4" or "1.21.4" → "1.21.4" */ - - // ── ModsInstallApi ─────────────────────────────────────────────────────── + // ── ModsInstallApi ──────────────────────────────────────────────────────── - /** - * Wraps CommonApi and overrides handleInstallation to download the mod JAR - * directly into the current instance's mods/ folder instead of installing a modpack. - */ private static class ModsInstallApi extends CommonApi { - ModsInstallApi(String curseforgeApiKey) { + private final SearchFilters mFilters; + private final ModrinthApi mModrinthApi = new ModrinthApi(); + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + + ModsInstallApi(String curseforgeApiKey, SearchFilters filters) { super(curseforgeApiKey); + mFilters = filters; + } + + /** + * Override getModDetails to filter versions by the selected MC version. + * Only versions matching the filter are shown in the version dropdown. + */ + @Override + public ModDetail getModDetails(ModItem item) { + if (item.apiSource == net.kdt.pojavlaunch.modloaders.modpacks.models.Constants.SOURCE_MODRINTH) { + String filterVer = (mFilters.mcVersion != null && !mFilters.mcVersion.isEmpty()) + ? mFilters.mcVersion : null; + return mModrinthApi.getModDetails(item, filterVer); + } + return super.getModDetails(item); } @Override public void handleInstallation(Context context, ModDetail modDetail, int selectedVersion) { if (modDetail.isModpack) { - // Fall back to normal modpack install super.handleInstallation(context, modDetail, selectedVersion); return; } @@ -223,27 +235,90 @@ public void handleInstallation(Context context, ModDetail modDetail, int selecte return; } - // Determine file name from URL - String fileName = url.substring(url.lastIndexOf('/') + 1); - if (!fileName.endsWith(".jar")) fileName += ".jar"; - // Strip query params if present - if (fileName.contains("?")) fileName = fileName.substring(0, fileName.indexOf('?')); + // Extract filename + String rawName = url.substring(url.lastIndexOf('/') + 1); + if (rawName.contains("?")) rawName = rawName.substring(0, rawName.indexOf('?')); + final String fileName = rawName.endsWith(".jar") ? rawName : rawName + ".jar"; + + // Check if this version has dependencies + String[] depIds = (modDetail.versionDependencyIds != null) ? modDetail.versionDependencyIds[selectedVersion] : null; + String[] depTypes = (modDetail.versionDependencyTypes != null) ? modDetail.versionDependencyTypes[selectedVersion] : null; + + if (depIds == null || depIds.length == 0) { + // No deps — download directly + downloadMod(context, url, fileName, new String[0], new String[0]); + return; + } + // Fetch project names for all deps, then show dialog + String[] labels = new String[depIds.length]; + final boolean[] checkedDefaults = new boolean[depIds.length]; + AtomicInteger remaining = new AtomicInteger(depIds.length); + + for (int i = 0; i < depIds.length; i++) { + final int idx = i; + final String type = (depTypes != null && idx < depTypes.length) ? depTypes[idx] : "required"; + final String prefix = "required".equals(type) ? "Required: " : "Optional: "; + checkedDefaults[idx] = "required".equals(type); + + final String projectId = depIds[idx]; + PojavApplication.sExecutorService.execute(() -> { + // Fetch project name from Modrinth + String name = fetchProjectName(projectId); + labels[idx] = prefix + (name != null ? name : projectId); + if (remaining.decrementAndGet() == 0) { + mMainHandler.post(() -> showDepsDialog(context, url, fileName, + depIds, depTypes, labels, checkedDefaults)); + } + }); + } + } + + private void showDepsDialog(Context context, String url, String fileName, + String[] depIds, String[] depTypes, + String[] labels, boolean[] checkedDefaults) { + boolean[] selected = checkedDefaults.clone(); + + new AlertDialog.Builder(context) + .setTitle(R.string.mod_deps_title) + .setMultiChoiceItems(labels, selected, + (dialog, which, isChecked) -> selected[which] = isChecked) + .setPositiveButton(R.string.mod_deps_install_selected, (d, w) -> { + List selectedIds = new ArrayList<>(); + for (int i = 0; i < depIds.length; i++) { + if (selected[i]) selectedIds.add(depIds[i]); + } + downloadMod(context, url, fileName, + selectedIds.toArray(new String[0]), depTypes); + }) + .setNeutralButton(R.string.mod_deps_install_without, + (d, w) -> downloadMod(context, url, fileName, new String[0], new String[0])) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void downloadMod(Context context, String url, String fileName, + String[] depIds, String[] depTypes) { File modsDir = getModsDir(); if (!modsDir.exists()) modsDir.mkdirs(); - File destFile = new File(modsDir, fileName); - String finalFileName = fileName; ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.global_waiting); PojavApplication.sExecutorService.execute(() -> { try { - DownloadUtils.downloadFile(url, destFile); + // Download main mod + DownloadUtils.downloadFile(url, new File(modsDir, fileName)); + + // Download selected dependencies + for (String depId : depIds) { + if (depId == null || depId.isEmpty()) continue; + downloadDependency(depId, modsDir); + } + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); Tools.runOnUiThread(() -> - Toast.makeText(context, - context.getString(R.string.mod_install_success, finalFileName), - Toast.LENGTH_LONG).show() - ); + Toast.makeText(context, + context.getString(R.string.mod_install_success, fileName), + Toast.LENGTH_LONG).show()); } catch (Exception e) { ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); Tools.showErrorRemote(context, R.string.modpack_install_download_failed, e); @@ -251,6 +326,39 @@ public void handleInstallation(Context context, ModDetail modDetail, int selecte }); } + private void downloadDependency(String projectId, File modsDir) { + // Fetch latest version for the current MC version/loader filter + try { + String filterVer = (mFilters.mcVersion != null && !mFilters.mcVersion.isEmpty()) + ? mFilters.mcVersion : ""; + ModItem depItem = new ModItem( + net.kdt.pojavlaunch.modloaders.modpacks.models.Constants.SOURCE_MODRINTH, + false, projectId, projectId, "", ""); + ModDetail depDetail = mModrinthApi.getModDetails(depItem, filterVer.isEmpty() ? null : filterVer); + if (depDetail == null || depDetail.versionUrls == null || depDetail.versionUrls.length == 0) return; + + String depUrl = depDetail.versionUrls[0]; + String depName = depUrl.substring(depUrl.lastIndexOf('/') + 1); + if (depName.contains("?")) depName = depName.substring(0, depName.indexOf('?')); + if (!depName.endsWith(".jar")) depName += ".jar"; + + DownloadUtils.downloadFile(depUrl, new File(modsDir, depName)); + } catch (Exception e) { + Log.w(TAG, "Failed to download dependency " + projectId + ": " + e.getMessage()); + } + } + + private String fetchProjectName(String projectId) { + try { + net.kdt.pojavlaunch.modloaders.modpacks.api.ApiHandler handler = + new net.kdt.pojavlaunch.modloaders.modpacks.api.ApiHandler("https://api.modrinth.com/v2"); + com.google.gson.JsonObject obj = handler.get("project/" + projectId, + com.google.gson.JsonObject.class); + if (obj != null && obj.has("title")) return obj.get("title").getAsString(); + } catch (Exception ignored) {} + return null; + } + private static File getModsDir() { try { String key = LauncherPreferences.DEFAULT_PREF @@ -258,9 +366,7 @@ private static File getModsDir() { if (key != null && !key.isEmpty()) { LauncherProfiles.load(); MinecraftProfile profile = LauncherProfiles.mainProfileJson.profiles.get(key); - if (profile != null) { - return new File(Tools.getGameDirPath(profile), "mods"); - } + if (profile != null) return new File(Tools.getGameDirPath(profile), "mods"); } } catch (Exception ignored) {} return new File(Tools.DIR_GAME_NEW, "mods"); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java index 3253366247..de799fb348 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java @@ -86,32 +86,73 @@ public SearchResult searchMod(SearchFilters searchFilters, SearchResult previous @Override public ModDetail getModDetails(ModItem item) { + return getModDetails(item, null); + } + public ModDetail getModDetails(ModItem item, String filterMcVersion) { JsonArray response = mApiHandler.get(String.format("project/%s/version", item.id), JsonArray.class); if(response == null) return null; - System.out.println(response); - String[] names = new String[response.size()]; - String[] mcNames = new String[response.size()]; - String[] urls = new String[response.size()]; - String[] hashes = new String[response.size()]; - - for (int i=0; i versions = new java.util.ArrayList<>(); + for (int i = 0; i < response.size(); i++) { + JsonObject v = response.get(i).getAsJsonObject(); + if (filterMcVersion != null && !filterMcVersion.isEmpty()) { + JsonArray gameVersions = v.get("game_versions").getAsJsonArray(); + boolean matches = false; + for (int j = 0; j < gameVersions.size(); j++) { + if (filterMcVersion.equals(gameVersions.get(j).getAsString())) { + matches = true; + break; + } + } + if (!matches) continue; + } + versions.add(v); + } + + if (versions.isEmpty()) return null; + + int size = versions.size(); + String[] names = new String[size]; + String[] mcNames = new String[size]; + String[] urls = new String[size]; + String[] hashes = new String[size]; + String[][] depIds = new String[size][]; + String[][] depTypes = new String[size][]; + + for (int i = 0; i < size; i++) { + JsonObject version = versions.get(i); + names[i] = version.get("name").getAsString(); mcNames[i] = version.get("game_versions").getAsJsonArray().get(0).getAsString(); - urls[i] = version.get("files").getAsJsonArray().get(0).getAsJsonObject().get("url").getAsString(); - // Assume there may not be hashes, in case the API changes + urls[i] = version.get("files").getAsJsonArray().get(0).getAsJsonObject().get("url").getAsString(); + JsonObject hashesMap = version.getAsJsonArray("files").get(0).getAsJsonObject() .get("hashes").getAsJsonObject(); - if(hashesMap == null || hashesMap.get("sha1") == null){ - hashes[i] = null; - continue; + hashes[i] = (hashesMap == null || hashesMap.get("sha1") == null) ? null + : hashesMap.get("sha1").getAsString(); + + // Capture dependencies + if (version.has("dependencies") && !version.get("dependencies").isJsonNull()) { + JsonArray deps = version.getAsJsonArray("dependencies"); + java.util.List ids = new java.util.ArrayList<>(); + java.util.List types = new java.util.ArrayList<>(); + for (int j = 0; j < deps.size(); j++) { + JsonObject dep = deps.get(j).getAsJsonObject(); + if (dep.has("project_id") && !dep.get("project_id").isJsonNull()) { + ids.add(dep.get("project_id").getAsString()); + types.add(dep.has("dependency_type") ? dep.get("dependency_type").getAsString() : "required"); + } + } + depIds[i] = ids.toArray(new String[0]); + depTypes[i] = types.toArray(new String[0]); + } else { + depIds[i] = new String[0]; + depTypes[i] = new String[0]; } - - hashes[i] = hashesMap.get("sha1").getAsString(); } - return new ModDetail(item, names, mcNames, urls, hashes); + return new ModDetail(item, names, mcNames, urls, hashes, depIds, depTypes); } @Override diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index fa8d991043..d68adf9266 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -521,4 +521,7 @@ Failed to download, check your internet connection. Installing… + Select dependencies to install + Install selected + Install without deps From 69c553d0dc3acf06d088cc117def745d43928685 Mon Sep 17 00:00:00 2001 From: MaxJubayerYT Date: Mon, 27 Apr 2026 16:29:55 +0600 Subject: [PATCH 2/4] Fix: ModDetail.java --- .../modloaders/modpacks/models/ModDetail.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java index 5a54e78b75..211eb289ef 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModDetail.java @@ -12,12 +12,24 @@ public class ModDetail extends ModItem { public String[] versionUrls; /* SHA 1 hashes, null if a hash is unavailable */ public String[] versionHashes; + /* Per-version dependency project IDs */ + public String[][] versionDependencyIds; + /* Per-version dependency types — "required" or "optional" */ + public String[][] versionDependencyTypes; + public ModDetail(ModItem item, String[] versionNames, String[] mcVersionNames, String[] versionUrls, String[] hashes) { + this(item, versionNames, mcVersionNames, versionUrls, hashes, null, null); + } + + public ModDetail(ModItem item, String[] versionNames, String[] mcVersionNames, String[] versionUrls, String[] hashes, + String[][] depIds, String[][] depTypes) { super(item.apiSource, item.isModpack, item.id, item.title, item.description, item.imageUrl); this.versionNames = versionNames; this.mcVersionNames = mcVersionNames; this.versionUrls = versionUrls; this.versionHashes = hashes; + this.versionDependencyIds = depIds; + this.versionDependencyTypes = depTypes; // Add the mc version to the version model for (int i=0; i Date: Tue, 28 Apr 2026 04:25:21 +0600 Subject: [PATCH 3/4] Fix: ModsSearchFragment --- .../kdt/pojavlaunch/fragments/ModsSearchFragment.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java index f7ff39ee48..c1ae935599 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java @@ -88,6 +88,7 @@ public ModsSearchFragment() { public void onAttach(@NonNull Context context) { super.onAttach(context); mModpackApi = new ModsInstallApi(context.getString(R.string.curseforge_api_key), mSearchFilters); + ((ModsInstallApi) mModpackApi).mActivityContext = context; } @Override @@ -201,6 +202,7 @@ private static class ModsInstallApi extends CommonApi { private final SearchFilters mFilters; private final ModrinthApi mModrinthApi = new ModrinthApi(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private Context mActivityContext; ModsInstallApi(String curseforgeApiKey, SearchFilters filters) { super(curseforgeApiKey); @@ -277,9 +279,12 @@ public void handleInstallation(Context context, ModDetail modDetail, int selecte private void showDepsDialog(Context context, String url, String fileName, String[] depIds, String[] depTypes, String[] labels, boolean[] checkedDefaults) { + // context here is getApplicationContext() from ModItemAdapter — no window token. + // Use the stored Activity reference instead. + Context dialogCtx = mActivityContext != null ? mActivityContext : context; boolean[] selected = checkedDefaults.clone(); - new AlertDialog.Builder(context) + new AlertDialog.Builder(dialogCtx) .setTitle(R.string.mod_deps_title) .setMultiChoiceItems(labels, selected, (dialog, which, isChecked) -> selected[which] = isChecked) @@ -372,4 +377,4 @@ private static File getModsDir() { return new File(Tools.DIR_GAME_NEW, "mods"); } } -} +} \ No newline at end of file From be3f29e4f187c4e3b017216462943d41b44a6e4a Mon Sep 17 00:00:00 2001 From: MaxJubayerYT Date: Fri, 1 May 2026 20:37:59 +0600 Subject: [PATCH 4/4] Fix: App typo Signed-off-by: MaxJubayerYT --- app_pojavlauncher/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index e2e6249ad7..7d9a5eeddc 100644 --- a/app_pojavlauncher/build.gradle +++ b/app_pojavlauncher/build.gradle @@ -147,7 +147,7 @@ android { // multiDexEnabled = true // debuggable = true resValue "string", "app_name", "Copper" - resValue "string", "app_short_name", "Copper (Debug)" + resValue "string", "app_short_name", "Copper" resValue 'string', 'storageProviderAuthorities', 'com.maxjubayeryt.copper.gamefolder' resValue 'string', 'application_package', 'com.maxjubayeryt.copper' }