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 ed67b67037..3c54c7abf8 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 @@ -175,22 +175,57 @@ private void displayFilterDialog() { TextView mSelectedVersion = dialog.findViewById(R.id.search_mod_selected_mc_version_textview); Button mSelectVersionButton = dialog.findViewById(R.id.search_mod_mc_version_button); Button mApplyButton = dialog.findViewById(R.id.search_mod_apply_filters); + android.widget.Spinner mLoaderSpinner = dialog.findViewById(R.id.search_mod_loader_spinner); assert mSelectedVersion != null; assert mSelectVersionButton != null; assert mApplyButton != null; - mSelectVersionButton.setOnClickListener(v -> - VersionSelectorDialog.open(v.getContext(), true, - (id, snapshot) -> mSelectedVersion.setText(id))); + // Set up loader spinner + if (mLoaderSpinner != null) { + String[] loaderLabels = {"Any loader", "Fabric", "Forge", "Quilt", "NeoForge"}; + final String[] loaderValues = {"", "fabric", "forge", "quilt", "neoforge"}; + android.widget.ArrayAdapter loaderAdapter = new android.widget.ArrayAdapter<>( + requireContext(), android.R.layout.simple_spinner_item, loaderLabels); + loaderAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mLoaderSpinner.setAdapter(loaderAdapter); + + // Restore current selection + String currentLoader = mSearchFilters.modLoader != null ? mSearchFilters.modLoader : ""; + for (int i = 0; i < loaderValues.length; i++) { + if (loaderValues[i].equals(currentLoader)) { + mLoaderSpinner.setSelection(i); + break; + } + } - mSelectedVersion.setText(mSearchFilters.mcVersion); + mSelectVersionButton.setOnClickListener(v -> + VersionSelectorDialog.open(v.getContext(), true, + (id, snapshot) -> mSelectedVersion.setText(id))); - mApplyButton.setOnClickListener(v -> { - mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); - searchMods(mSearchEditText.getText().toString()); - dialogInterface.dismiss(); - }); + mSelectedVersion.setText(mSearchFilters.mcVersion); + + mApplyButton.setOnClickListener(v -> { + mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); + int pos = mLoaderSpinner.getSelectedItemPosition(); + mSearchFilters.modLoader = loaderValues[pos]; + searchMods(mSearchEditText.getText().toString()); + dialogInterface.dismiss(); + }); + } else { + // Fallback if spinner view not found + mSelectVersionButton.setOnClickListener(v -> + VersionSelectorDialog.open(v.getContext(), true, + (id, snapshot) -> mSelectedVersion.setText(id))); + + mSelectedVersion.setText(mSearchFilters.mcVersion); + + mApplyButton.setOnClickListener(v -> { + mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); + searchMods(mSearchEditText.getText().toString()); + dialogInterface.dismiss(); + }); + } }); dialog.show(); @@ -219,7 +254,9 @@ 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); + String filterLoader = (mFilters.modLoader != null && !mFilters.modLoader.isEmpty()) + ? mFilters.modLoader : null; + return mModrinthApi.getModDetails(item, filterVer, filterLoader); } return super.getModDetails(item); } @@ -238,8 +275,9 @@ public void handleInstallation(Context context, ModDetail modDetail, int selecte && (modDetail.isRestricted || url == null || url.isEmpty()); if (isCfRestricted) { - // Show dialog directing user to download from CurseForge website - String cfUrl = "https://www.curseforge.com/minecraft/mc-mods/" + modDetail.id; + String cfUrl = (modDetail.websiteUrl != null && !modDetail.websiteUrl.isEmpty()) + ? modDetail.websiteUrl + : "https://www.curseforge.com/minecraft/mc-mods/" + modDetail.id; Context dialogCtx = mActivityContext != null ? mActivityContext : context; mMainHandler.post(() -> new AlertDialog.Builder(dialogCtx) @@ -358,10 +396,12 @@ private void downloadDependency(String projectId, File modsDir) { try { String filterVer = (mFilters.mcVersion != null && !mFilters.mcVersion.isEmpty()) ? mFilters.mcVersion : ""; + String filterLoader = (mFilters.modLoader != null && !mFilters.modLoader.isEmpty()) + ? mFilters.modLoader : null; 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); + ModDetail depDetail = mModrinthApi.getModDetails(depItem, filterVer.isEmpty() ? null : filterVer, filterLoader); if (depDetail == null || depDetail.versionUrls == null || depDetail.versionUrls.length == 0) return; String depUrl = depDetail.versionUrls[0]; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java index e2b8ddc039..59fb74d8b9 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/SearchModFragment.java @@ -5,10 +5,12 @@ import android.graphics.Color; import android.os.Bundle; import android.view.View; +import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ProgressBar; +import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.NonNull; @@ -23,6 +25,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.Constants; +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.profiles.VersionSelectorDialog; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; @@ -61,7 +67,7 @@ public SearchModFragment(){ @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - modpackApi = new CommonApi(context.getString(R.string.curseforge_api_key)); + modpackApi = new ModpackSearchApi(context.getString(R.string.curseforge_api_key), mSearchFilters); } @Override @@ -148,26 +154,85 @@ private void displayFilterDialog() { TextView mSelectedVersion = dialog.findViewById(R.id.search_mod_selected_mc_version_textview); Button mSelectVersionButton = dialog.findViewById(R.id.search_mod_mc_version_button); Button mApplyButton = dialog.findViewById(R.id.search_mod_apply_filters); + Spinner mLoaderSpinner = dialog.findViewById(R.id.search_mod_loader_spinner); assert mSelectVersionButton != null; assert mSelectedVersion != null; assert mApplyButton != null; - // Setup the expendable list behavior - mSelectVersionButton.setOnClickListener(v -> VersionSelectorDialog.open(v.getContext(), true, (id, snapshot)-> mSelectedVersion.setText(id))); + // Set up loader spinner + if (mLoaderSpinner != null) { + String[] loaderLabels = {"Any loader", "Fabric", "Forge", "Quilt", "NeoForge"}; + final String[] loaderValues = {"", "fabric", "forge", "quilt", "neoforge"}; + ArrayAdapter loaderAdapter = new ArrayAdapter<>( + requireContext(), android.R.layout.simple_spinner_item, loaderLabels); + loaderAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mLoaderSpinner.setAdapter(loaderAdapter); + + // Restore current selection + String currentLoader = mSearchFilters.modLoader != null ? mSearchFilters.modLoader : ""; + for (int i = 0; i < loaderValues.length; i++) { + if (loaderValues[i].equals(currentLoader)) { + mLoaderSpinner.setSelection(i); + break; + } + } + + mSelectVersionButton.setOnClickListener(v -> + VersionSelectorDialog.open(v.getContext(), true, + (id, snapshot) -> mSelectedVersion.setText(id))); + + mSelectedVersion.setText(mSearchFilters.mcVersion); + + mApplyButton.setOnClickListener(v -> { + mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); + int pos = mLoaderSpinner.getSelectedItemPosition(); + mSearchFilters.modLoader = loaderValues[pos]; + searchMods(mSearchEditText.getText().toString()); + dialogInterface.dismiss(); + }); + } else { + mSelectVersionButton.setOnClickListener(v -> + VersionSelectorDialog.open(v.getContext(), true, + (id, snapshot) -> mSelectedVersion.setText(id))); + mSelectedVersion.setText(mSearchFilters.mcVersion); + mApplyButton.setOnClickListener(v -> { + mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); + searchMods(mSearchEditText.getText().toString()); + dialogInterface.dismiss(); + }); + } + }); - // Apply visually all the current settings - mSelectedVersion.setText(mSearchFilters.mcVersion); + dialog.show(); + } - // Apply the new settings - mApplyButton.setOnClickListener(v -> { - mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); - searchMods(mSearchEditText.getText().toString()); - dialogInterface.dismiss(); - }); - }); + // ── ModpackSearchApi ────────────────────────────────────────────────────── + private static class ModpackSearchApi extends CommonApi { + private final SearchFilters mFilters; + private final ModrinthApi mModrinthApi = new ModrinthApi(); - dialog.show(); + ModpackSearchApi(String curseforgeApiKey, SearchFilters filters) { + super(curseforgeApiKey); + mFilters = filters; + } + + /** + * Override getModDetails so the version dropdown only shows versions + * matching the selected MC version and loader filter. + */ + @Override + public ModDetail getModDetails(ModItem item) { + if (item.apiSource == Constants.SOURCE_MODRINTH) { + String filterVer = (mFilters.mcVersion != null && !mFilters.mcVersion.isEmpty()) + ? mFilters.mcVersion : null; + String filterLoader = (mFilters.modLoader != null && !mFilters.modLoader.isEmpty()) + ? mFilters.modLoader : null; + return mModrinthApi.getModDetails(item, filterVer, filterLoader); + } + // CurseForge: delegate normally (CF search already filters by version/loader) + return super.getModDetails(item); + } } -} +} \ No newline at end of file diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java index 7036f3c9e5..7480e9c177 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java @@ -64,6 +64,17 @@ public SearchResult searchMod(SearchFilters searchFilters, SearchResult previous params.put("sortOrder", "desc"); if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty()) params.put("gameVersion", searchFilters.mcVersion); + if(searchFilters.modLoader != null && !searchFilters.modLoader.isEmpty()) { + // CF modLoaderType: 1=Forge, 4=Fabric, 5=Quilt, 6=NeoForge + int modLoaderType = 0; + switch(searchFilters.modLoader.toLowerCase()) { + case "forge": modLoaderType = 1; break; + case "fabric": modLoaderType = 4; break; + case "quilt": modLoaderType = 5; break; + case "neoforge": modLoaderType = 6; break; + } + if(modLoaderType != 0) params.put("modLoaderType", modLoaderType); + } if(previousPageResult != null) params.put("index", curseforgeSearchResult.previousOffset); @@ -79,6 +90,12 @@ public SearchResult searchMod(SearchFilters searchFilters, SearchResult previous // Gson automatically casts null to false, which leans to issues // So, only check the distribution flag if it is non-null boolean restricted = !allowModDistribution.isJsonNull() && !allowModDistribution.getAsBoolean(); + // For modpacks, skip restricted entries entirely (same as before) + // For individual mods, keep them so we can show the CF website dialog + if (restricted && searchFilters.isModpack) { + Log.i("CurseforgeApi", "Skipping modpack "+dataElement.get("name").getAsString() + " because curseforge sucks"); + continue; + } JsonObject logo = dataElement.getAsJsonObject("logo"); String thumbnailUrl = (logo != null && logo.has("thumbnailUrl") && !logo.get("thumbnailUrl").isJsonNull()) ? logo.get("thumbnailUrl").getAsString() : ""; @@ -89,6 +106,16 @@ public SearchResult searchMod(SearchFilters searchFilters, SearchResult previous dataElement.get("summary").getAsString(), thumbnailUrl); modItem.isRestricted = restricted; + // Capture the mod page URL from CF API for use in restriction dialog + JsonObject links = dataElement.getAsJsonObject("links"); + if (links != null && links.has("websiteUrl") && !links.get("websiteUrl").isJsonNull()) { + modItem.websiteUrl = links.get("websiteUrl").getAsString(); + } else { + // Fallback using slug if available, otherwise numeric id + String slug = dataElement.has("slug") && !dataElement.get("slug").isJsonNull() + ? dataElement.get("slug").getAsString() : modItem.id; + modItem.websiteUrl = "https://www.curseforge.com/minecraft/mc-mods/" + slug; + } modItemList.add(modItem); } if(curseforgeSearchResult == null) curseforgeSearchResult = new CurseforgeSearchResult(); 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 de799fb348..78e48fc10f 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 @@ -52,6 +52,8 @@ public SearchResult searchMod(SearchFilters searchFilters, SearchResult previous facetString.append(String.format("[\"project_type:%s\"]", searchFilters.isModpack ? "modpack" : "mod")); if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty()) facetString.append(String.format(",[\"versions:%s\"]", searchFilters.mcVersion)); + if(searchFilters.modLoader != null && !searchFilters.modLoader.isEmpty()) + facetString.append(String.format(",[\"categories:%s\"]", searchFilters.modLoader)); facetString.append("]"); params.put("facets", facetString.toString()); params.put("query", searchFilters.name); @@ -86,14 +88,18 @@ public SearchResult searchMod(SearchFilters searchFilters, SearchResult previous @Override public ModDetail getModDetails(ModItem item) { - return getModDetails(item, null); + return getModDetails(item, null, null); } public ModDetail getModDetails(ModItem item, String filterMcVersion) { + return getModDetails(item, filterMcVersion, null); + } + + public ModDetail getModDetails(ModItem item, String filterMcVersion, String filterLoader) { JsonArray response = mApiHandler.get(String.format("project/%s/version", item.id), JsonArray.class); if(response == null) return null; - // Collect versions, optionally filtering by MC version + // Collect versions, optionally filtering by MC version and/or loader java.util.List versions = new java.util.ArrayList<>(); for (int i = 0; i < response.size(); i++) { JsonObject v = response.get(i).getAsJsonObject(); @@ -108,6 +114,17 @@ public ModDetail getModDetails(ModItem item, String filterMcVersion) { } if (!matches) continue; } + if (filterLoader != null && !filterLoader.isEmpty()) { + JsonArray loaders = v.get("loaders").getAsJsonArray(); + boolean matches = false; + for (int j = 0; j < loaders.size(); j++) { + if (filterLoader.equalsIgnoreCase(loaders.get(j).getAsString())) { + matches = true; + break; + } + } + if (!matches) continue; + } versions.add(v); } @@ -209,4 +226,4 @@ private ModLoader installMrpack(File mrpackFile, File instanceDestination) throw class ModrinthSearchResult extends SearchResult { int previousOffset; } -} +} \ No newline at end of file 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 6bc3abc404..d080a2669a 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 @@ -24,6 +24,8 @@ public ModDetail(ModItem item, String[] versionNames, String[] mcVersionNames, S 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.isRestricted = item.isRestricted; + this.websiteUrl = item.websiteUrl; this.versionNames = versionNames; this.mcVersionNames = mcVersionNames; this.versionUrls = versionUrls; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java index 9980c18302..2c5b0d1633 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/ModItem.java @@ -10,6 +10,8 @@ public class ModItem extends ModSource { public String imageUrl; /** True if the mod author blocked third-party distribution (CF allowModDistribution=false) */ public boolean isRestricted; + /** Direct website URL for the mod page (used for CF restricted mods) */ + public String websiteUrl; public ModItem(int apiSource, boolean isModpack, String id, String title, String description, String imageUrl) { this.apiSource = apiSource; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java index 5694b3b1ea..e61bdca934 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/SearchFilters.java @@ -9,5 +9,7 @@ public class SearchFilters { public boolean isModpack; public String name; @Nullable public String mcVersion; + /** Mod loader filter: "fabric", "forge", "quilt", "neoforge", or null/empty for any */ + @Nullable public String modLoader; -} +} \ No newline at end of file diff --git a/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml b/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml index 911f02b39a..6b39f135eb 100644 --- a/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml +++ b/app_pojavlauncher/src/main/res/layout/dialog_mod_filters.xml @@ -16,21 +16,17 @@ android:layout_height="wrap_content" android:text="@string/profiles_profile_version" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toStartOf="@+id/search_mod_selected_mc_version_textview" /> @@ -40,13 +36,31 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:paddingHorizontal="@dimen/padding_large" - android:text="@string/global_select" - app:layout_constraintBottom_toBottomOf="@+id/search_mod_selected_mc_version_textview" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/search_mod_selected_mc_version_textview" /> + + + + + \ No newline at end of file