mirror of
https://github.com/AngelAuraMC/Amethyst-Android.git
synced 2025-09-15 07:39:00 -04:00
Async changes + WIP CurseForge implementation
- Implemented IOException handling and reliable Futures for the ModItemAdapter - Generified self-referencing Futures into a SelfReferencingFuture class - Added Work-in-progress CurseForge implementation TODO: - Handle Exceptions from the modpack install process - Install the modloader after finishing instance installation - Finish CurseForge support (current roadblock: lack of documentation)
This commit is contained in:
parent
5fde03dbaa
commit
b430edbc0b
@ -20,6 +20,8 @@ import net.kdt.pojavlaunch.PojavApplication;
|
|||||||
import net.kdt.pojavlaunch.R;
|
import net.kdt.pojavlaunch.R;
|
||||||
import net.kdt.pojavlaunch.Tools;
|
import net.kdt.pojavlaunch.Tools;
|
||||||
import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter;
|
import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.SelfReferencingFuture;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.api.CurseforgeApi;
|
||||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi;
|
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi;
|
||||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModrinthApi;
|
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModrinthApi;
|
||||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
|
||||||
@ -50,7 +52,7 @@ public class SearchModFragment extends Fragment {
|
|||||||
|
|
||||||
public SearchModFragment(){
|
public SearchModFragment(){
|
||||||
super(R.layout.fragment_mod_search);
|
super(R.layout.fragment_mod_search);
|
||||||
modpackApi = new ModrinthApi();
|
modpackApi = new CurseforgeApi();
|
||||||
mModItemAdapter = new ModItemAdapter(modpackApi);
|
mModItemAdapter = new ModItemAdapter(modpackApi);
|
||||||
mSearchFilters = new SearchFilters();
|
mSearchFilters = new SearchFilters();
|
||||||
mSearchFilters.isModpack = true;
|
mSearchFilters.isModpack = true;
|
||||||
@ -82,43 +84,26 @@ public class SearchModFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
mSearchProgressBar.setVisibility(View.VISIBLE);
|
mSearchProgressBar.setVisibility(View.VISIBLE);
|
||||||
mSearchFilters.name = mSearchEditText.getText().toString();
|
mSearchFilters.name = mSearchEditText.getText().toString();
|
||||||
SearchModRunnable searchModRunnable = new SearchModRunnable(mSearchFilters);
|
mSearchFuture = new SelfReferencingFuture(new SearchModTask(mSearchFilters))
|
||||||
mSearchFuture = PojavApplication.sExecutorService.submit(searchModRunnable);
|
.startOnExecutor(PojavApplication.sExecutorService);
|
||||||
searchModRunnable.setFuture(mSearchFuture);
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchModRunnable implements Runnable{
|
class SearchModTask implements SelfReferencingFuture.FutureInterface {
|
||||||
private final Object mFutureLock = new Object();
|
|
||||||
private final SearchFilters mRunnableFilters;
|
|
||||||
private Future<?> mMyFuture;
|
|
||||||
|
|
||||||
SearchModRunnable(SearchFilters mSearchFilters) {
|
private final SearchFilters mTaskFilters;
|
||||||
this.mRunnableFilters = mSearchFilters;
|
SearchModTask(SearchFilters mSearchFilters) {
|
||||||
}
|
this.mTaskFilters = mSearchFilters;
|
||||||
|
|
||||||
public void setFuture(Future<?> future) {
|
|
||||||
synchronized (mFutureLock) {
|
|
||||||
mMyFuture = future;
|
|
||||||
mFutureLock.notifyAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run(Future<?> myFuture) {
|
||||||
synchronized (mFutureLock) {
|
ModItem[] items = modpackApi.searchMod(mTaskFilters);
|
||||||
try {
|
|
||||||
if (mMyFuture == null) mFutureLock.wait();
|
|
||||||
}catch (InterruptedException e) {
|
|
||||||
return; // if we got interrupted on the future lock theres no point in proceeding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ModItem[] items = modpackApi.searchMod(mRunnableFilters);
|
|
||||||
Log.d(SearchModFragment.class.toString(), Arrays.toString(items));
|
Log.d(SearchModFragment.class.toString(), Arrays.toString(items));
|
||||||
Tools.runOnUiThread(() -> {
|
Tools.runOnUiThread(() -> {
|
||||||
ModItem[] localItems = items;
|
ModItem[] localItems = items;
|
||||||
if(mMyFuture.isCancelled()) return;
|
if(myFuture.isCancelled()) return;
|
||||||
mSearchProgressBar.setVisibility(View.GONE);
|
mSearchProgressBar.setVisibility(View.GONE);
|
||||||
if(localItems == null) {
|
if(localItems == null) {
|
||||||
mStatusTextView.setVisibility(View.VISIBLE);
|
mStatusTextView.setVisibility(View.VISIBLE);
|
||||||
|
@ -26,6 +26,7 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
|
|||||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHolder> {
|
public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHolder> {
|
||||||
private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0];
|
private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0];
|
||||||
@ -41,10 +42,12 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
|
|||||||
private ModDetail mModDetail = null;
|
private ModDetail mModDetail = null;
|
||||||
private ModItem mModItem = null;
|
private ModItem mModItem = null;
|
||||||
private final TextView mTitle, mDescription;
|
private final TextView mTitle, mDescription;
|
||||||
|
private final ImageView mIconView;
|
||||||
private View mExtendedLayout;
|
private View mExtendedLayout;
|
||||||
private Spinner mExtendedSpinner;
|
private Spinner mExtendedSpinner;
|
||||||
private Button mExtendedButton;
|
private Button mExtendedButton;
|
||||||
private final ImageView mIconView;
|
private TextView mExtendedErrorTextView;
|
||||||
|
private Future<?> mExtensionFuture;
|
||||||
private Bitmap mThumbnailBitmap;
|
private Bitmap mThumbnailBitmap;
|
||||||
private ImageReceiver mImageReceiver;
|
private ImageReceiver mImageReceiver;
|
||||||
public ViewHolder(View view) {
|
public ViewHolder(View view) {
|
||||||
@ -56,6 +59,7 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
|
|||||||
mExtendedLayout = ((ViewStub)v.findViewById(R.id.mod_limited_state_stub)).inflate();
|
mExtendedLayout = ((ViewStub)v.findViewById(R.id.mod_limited_state_stub)).inflate();
|
||||||
mExtendedButton = mExtendedLayout.findViewById(R.id.mod_extended_select_version_button);
|
mExtendedButton = mExtendedLayout.findViewById(R.id.mod_extended_select_version_button);
|
||||||
mExtendedSpinner = mExtendedLayout.findViewById(R.id.mod_extended_version_spinner);
|
mExtendedSpinner = mExtendedLayout.findViewById(R.id.mod_extended_version_spinner);
|
||||||
|
mExtendedErrorTextView = mExtendedLayout.findViewById(R.id.mod_extended_error_textview);
|
||||||
|
|
||||||
mExtendedButton.setOnClickListener(v1 -> {
|
mExtendedButton.setOnClickListener(v1 -> {
|
||||||
mModpackApi.handleInstallation(
|
mModpackApi.handleInstallation(
|
||||||
@ -69,12 +73,39 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
|
|||||||
else mExtendedLayout.setVisibility(View.VISIBLE);
|
else mExtendedLayout.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isExtended() && mModDetail == null) {
|
if(isExtended() && mModDetail == null && mExtensionFuture == null) { // only reload if no reloads are in progress
|
||||||
PojavApplication.sExecutorService.execute(() -> {
|
setDetailedStateDefault();
|
||||||
|
/*
|
||||||
|
* Why do we do this?
|
||||||
|
* The reason is simple: multithreading is difficult as hell to manage
|
||||||
|
* Let me explain:
|
||||||
|
*/
|
||||||
|
mExtensionFuture = new SelfReferencingFuture(myFuture -> {
|
||||||
|
/*
|
||||||
|
* While we are sitting in the function below doing networking, the view might have already gotten recycled.
|
||||||
|
* If we didn't use a Future, we would have extended a ViewHolder with completely unrelated content
|
||||||
|
* or with an error that has never actually happened
|
||||||
|
*/
|
||||||
mModDetail = mModpackApi.getModDetails(mModItem);
|
mModDetail = mModpackApi.getModDetails(mModItem);
|
||||||
System.out.println(mModDetail);
|
System.out.println(mModDetail);
|
||||||
Tools.runOnUiThread(() -> setStateDetailed(mModDetail));
|
Tools.runOnUiThread(() -> {
|
||||||
});
|
/*
|
||||||
|
* Once we enter here, the state we're in is already defined - no view shuffling can happen on the UI
|
||||||
|
* thread while we are on the UI thread ourselves. If we were cancelled, this means that the future
|
||||||
|
* we were supposed to have no longer makes sense, so we return and do not alter the state (since we might
|
||||||
|
* alter the state of an unrelated item otherwise)
|
||||||
|
*/
|
||||||
|
if(myFuture.isCancelled()) return;
|
||||||
|
/*
|
||||||
|
* We do not null the future before returning since this field might already belong to a different item with its
|
||||||
|
* own Future, which we don't want to interfere with.
|
||||||
|
* But if the future is not cancelled, it is the right one for this ViewHolder, and we don't need it anymore, so
|
||||||
|
* let's help GC clean it up once we exit!
|
||||||
|
*/
|
||||||
|
mExtensionFuture = null;
|
||||||
|
setStateDetailed(mModDetail);
|
||||||
|
});
|
||||||
|
}).startOnExecutor(PojavApplication.sExecutorService);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,6 +125,14 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
|
|||||||
if(mImageReceiver != null) {
|
if(mImageReceiver != null) {
|
||||||
mIconCache.cancelImage(mImageReceiver);
|
mIconCache.cancelImage(mImageReceiver);
|
||||||
}
|
}
|
||||||
|
if(mExtensionFuture != null) {
|
||||||
|
/*
|
||||||
|
* Since this method reinitializes the ViewHolder for a new mod, this Future stops being ours, so we cancel it
|
||||||
|
* and null it. The rest is handled above
|
||||||
|
*/
|
||||||
|
mExtensionFuture.cancel(true);
|
||||||
|
mExtensionFuture = null;
|
||||||
|
}
|
||||||
|
|
||||||
mModItem = item;
|
mModItem = item;
|
||||||
// here the previous reference to the image receiver will disappear
|
// here the previous reference to the image receiver will disappear
|
||||||
@ -114,7 +153,22 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
|
|||||||
/** Display extended info/interaction about a modpack */
|
/** Display extended info/interaction about a modpack */
|
||||||
private void setStateDetailed(ModDetail detailedItem) {
|
private void setStateDetailed(ModDetail detailedItem) {
|
||||||
mExtendedLayout.setVisibility(View.VISIBLE);
|
mExtendedLayout.setVisibility(View.VISIBLE);
|
||||||
mExtendedSpinner.setAdapter(new SimpleArrayAdapter<>(Arrays.asList(detailedItem.versionNames)));
|
if(detailedItem != null) {
|
||||||
|
mExtendedButton.setEnabled(true);
|
||||||
|
mExtendedErrorTextView.setVisibility(View.GONE);
|
||||||
|
mExtendedSpinner.setAdapter(new SimpleArrayAdapter<>(Arrays.asList(detailedItem.versionNames)));
|
||||||
|
} else {
|
||||||
|
mExtendedButton.setEnabled(false);
|
||||||
|
mExtendedErrorTextView.setVisibility(View.VISIBLE);
|
||||||
|
mExtendedSpinner.setAdapter(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDetailedStateDefault() {
|
||||||
|
mExtendedButton.setEnabled(false);
|
||||||
|
mExtendedSpinner.setAdapter(null);
|
||||||
|
mExtendedErrorTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasExtended(){
|
private boolean hasExtended(){
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
package net.kdt.pojavlaunch.modloaders.modpacks;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
public class SelfReferencingFuture {
|
||||||
|
private final Object mFutureLock = new Object();
|
||||||
|
private final FutureInterface mFutureInterface;
|
||||||
|
private Future<?> mMyFuture;
|
||||||
|
|
||||||
|
public SelfReferencingFuture(FutureInterface futureInterface) {
|
||||||
|
this.mFutureInterface = futureInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<?> startOnExecutor(ExecutorService executorService) {
|
||||||
|
Future<?> future = executorService.submit(this::run);
|
||||||
|
synchronized (mFutureLock) {
|
||||||
|
mMyFuture = future;
|
||||||
|
mFutureLock.notify();
|
||||||
|
}
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run() {
|
||||||
|
try {
|
||||||
|
synchronized (mFutureLock) {
|
||||||
|
if (mMyFuture == null) mFutureLock.wait();
|
||||||
|
}
|
||||||
|
mFutureInterface.run(mMyFuture);
|
||||||
|
}catch (InterruptedException e) {
|
||||||
|
Log.i("SelfReferencingFuture", "Interrupted while acquiring own Future");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface FutureInterface {
|
||||||
|
void run(Future<?> myFuture);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||||
|
|
||||||
|
import android.util.ArrayMap;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
@ -13,35 +14,50 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public class ApiHandler {
|
public class ApiHandler {
|
||||||
public final String baseUrl;
|
public final String baseUrl;
|
||||||
|
public final Map<String, String> additionalHeaders;
|
||||||
|
|
||||||
public ApiHandler(String url) {
|
public ApiHandler(String url) {
|
||||||
baseUrl = url;
|
baseUrl = url;
|
||||||
|
additionalHeaders = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiHandler(String url, String apiKey) {
|
||||||
|
baseUrl = url;
|
||||||
|
additionalHeaders = new ArrayMap<>();
|
||||||
|
additionalHeaders.put("x-api-key", apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T get(String endpoint, Class<T> tClass) {
|
public <T> T get(String endpoint, Class<T> tClass) {
|
||||||
return getFullUrl(baseUrl + "/" + endpoint, tClass);
|
return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, tClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T get(String endpoint, HashMap<String, Object> query, Class<T> tClass) {
|
public <T> T get(String endpoint, HashMap<String, Object> query, Class<T> tClass) {
|
||||||
return getFullUrl(baseUrl + "/" + endpoint, query, tClass);
|
return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, tClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T post(String endpoint, T body, Class<T> tClass) {
|
public <T> T post(String endpoint, T body, Class<T> tClass) {
|
||||||
return postFullUrl(baseUrl + "/" + endpoint, body, tClass);
|
return postFullUrl(additionalHeaders, baseUrl + "/" + endpoint, body, tClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T post(String endpoint, HashMap<String, Object> query, T body, Class<T> tClass) {
|
public <T> T post(String endpoint, HashMap<String, Object> query, T body, Class<T> tClass) {
|
||||||
return postFullUrl(baseUrl + "/" + endpoint, query, body, tClass);
|
return postFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, body, tClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Make a get request and return the response as a raw string;
|
//Make a get request and return the response as a raw string;
|
||||||
public static String getRaw(String url) {
|
public static String getRaw(String url) {
|
||||||
|
return getRaw(null, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRaw(Map<String, String> headers, String url) {
|
||||||
Log.d("ApiHandler", url);
|
Log.d("ApiHandler", url);
|
||||||
try {
|
try {
|
||||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||||
|
addHeaders(conn, headers);
|
||||||
InputStream inputStream = conn.getInputStream();
|
InputStream inputStream = conn.getInputStream();
|
||||||
String data = Tools.read(inputStream);
|
String data = Tools.read(inputStream);
|
||||||
Log.d(ApiHandler.class.toString(), data);
|
Log.d(ApiHandler.class.toString(), data);
|
||||||
@ -55,11 +71,16 @@ public class ApiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String postRaw(String url, String body) {
|
public static String postRaw(String url, String body) {
|
||||||
|
return postRaw(null, url, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String postRaw(Map<String, String> headers, String url, String body) {
|
||||||
try {
|
try {
|
||||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||||
conn.setRequestMethod("POST");
|
conn.setRequestMethod("POST");
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
addHeaders(conn, headers);
|
||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
|
|
||||||
OutputStream outputStream = conn.getOutputStream();
|
OutputStream outputStream = conn.getOutputStream();
|
||||||
@ -79,6 +100,13 @@ public class ApiHandler {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addHeaders(HttpURLConnection connection, Map<String, String> headers) {
|
||||||
|
if(headers != null) {
|
||||||
|
for(String key : headers.keySet())
|
||||||
|
connection.addRequestProperty(key, headers.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String parseQueries(HashMap<String, Object> query) {
|
private static String parseQueries(HashMap<String, Object> query) {
|
||||||
StringBuilder params = new StringBuilder("?");
|
StringBuilder params = new StringBuilder("?");
|
||||||
for (String param : query.keySet()) {
|
for (String param : query.keySet()) {
|
||||||
@ -89,18 +117,34 @@ public class ApiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T getFullUrl(String url, Class<T> tClass) {
|
public static <T> T getFullUrl(String url, Class<T> tClass) {
|
||||||
return new Gson().fromJson(getRaw(url), tClass);
|
return getFullUrl(null, url, tClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T getFullUrl(String url, HashMap<String, Object> query, Class<T> tClass) {
|
public static <T> T getFullUrl(String url, HashMap<String, Object> query, Class<T> tClass) {
|
||||||
return getFullUrl(url + parseQueries(query), tClass);
|
return getFullUrl(null, url, query, tClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T postFullUrl(String url, T body, Class<T> tClass) {
|
public static <T> T postFullUrl(String url, T body, Class<T> tClass) {
|
||||||
return new Gson().fromJson(postRaw(url, body.toString()), tClass);
|
return postFullUrl(null, url, body, tClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T postFullUrl(String url, HashMap<String, Object> query, T body, Class<T> tClass) {
|
public static <T> T postFullUrl(String url, HashMap<String, Object> query, T body, Class<T> tClass) {
|
||||||
return new Gson().fromJson(postRaw(url + parseQueries(query), body.toString()), tClass);
|
return postFullUrl(null, url, query, body, tClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T getFullUrl(Map<String, String> headers, String url, Class<T> tClass) {
|
||||||
|
return new Gson().fromJson(getRaw(headers, url), tClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T getFullUrl(Map<String, String> headers, String url, HashMap<String, Object> query, Class<T> tClass) {
|
||||||
|
return getFullUrl(headers, url + parseQueries(query), tClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T postFullUrl(Map<String, String> headers, String url, T body, Class<T> tClass) {
|
||||||
|
return new Gson().fromJson(postRaw(headers, url, body.toString()), tClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T postFullUrl(Map<String, String> headers, String url, HashMap<String, Object> query, T body, Class<T> tClass) {
|
||||||
|
return new Gson().fromJson(postRaw(headers, url + parseQueries(query), body.toString()), tClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,134 @@
|
|||||||
|
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import net.kdt.pojavlaunch.Tools;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.CurseManifest;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex;
|
||||||
|
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
|
||||||
|
import net.kdt.pojavlaunch.utils.ZipUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
public class CurseforgeApi implements ModpackApi{
|
||||||
|
private static final Pattern sMcVersionPattern = Pattern.compile("([0-9]+)\\.([0-9]+)\\.?([0-9]+)?");
|
||||||
|
// Stolen from
|
||||||
|
// https://github.com/AnzhiZhang/CurseForgeModpackDownloader/blob/6cb3f428459f0cc8f444d16e54aea4cd1186fd7b/utils/requester.py#L93
|
||||||
|
private static final int CURSEFORGE_MINECRAFT_GAME_ID = 432;
|
||||||
|
private static final int CURSEFORGE_MODPACK_CLASS_ID = 4471;
|
||||||
|
// https://api.curseforge.com/v1/categories?gameId=432 and search for "Mods" (case-sensitive)
|
||||||
|
private static final int CURSEFORGE_MOD_CLASS_ID = 6;
|
||||||
|
private static final int CURSEFORGE_PAGINATION_SIZE = 50;
|
||||||
|
|
||||||
|
private final ApiHandler mApiHandler = new ApiHandler("https://api.curseforge.com/v1", "$2a$10$Vxkj4kH1Ekf8EsS4Mx8b2eVTHsht107Lk2erVEUtnbqvojsLy.jYq");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModItem[] searchMod(SearchFilters searchFilters) {
|
||||||
|
HashMap<String, Object> params = new HashMap<>();
|
||||||
|
params.put("gameId", CURSEFORGE_MINECRAFT_GAME_ID);
|
||||||
|
params.put("classId", searchFilters.isModpack ? CURSEFORGE_MODPACK_CLASS_ID : CURSEFORGE_MOD_CLASS_ID);
|
||||||
|
params.put("searchFilter", searchFilters.name);
|
||||||
|
if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty())
|
||||||
|
params.put("gameVersion", searchFilters.mcVersion);
|
||||||
|
JsonObject response = mApiHandler.get("mods/search", params, JsonObject.class);
|
||||||
|
if(response == null) return null;
|
||||||
|
JsonArray dataArray = response.getAsJsonArray("data");
|
||||||
|
if(dataArray == null) return null;
|
||||||
|
ArrayList<ModItem> modItemList = new ArrayList<>(dataArray.size());
|
||||||
|
for(int i = 0; i < dataArray.size(); i++) {
|
||||||
|
JsonObject dataElement = dataArray.get(i).getAsJsonObject();
|
||||||
|
JsonElement allowModDistribution = dataElement.get("allowModDistribution");
|
||||||
|
// Gson automatically casts null to false, which leans to issues
|
||||||
|
// So, only check the distribution flag if it is non-null
|
||||||
|
if(!allowModDistribution.isJsonNull() && !allowModDistribution.getAsBoolean()) {
|
||||||
|
Log.i("CurseforgeApi", "Skipping modpack "+dataElement.get("name").getAsString() + " because curseforge sucks");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ModItem modItem = new ModItem(Constants.SOURCE_CURSEFORGE,
|
||||||
|
searchFilters.isModpack,
|
||||||
|
dataElement.get("id").getAsString(),
|
||||||
|
dataElement.get("name").getAsString(),
|
||||||
|
dataElement.get("summary").getAsString(),
|
||||||
|
dataElement.getAsJsonObject("logo").get("thumbnailUrl").getAsString());
|
||||||
|
modItemList.add(modItem);
|
||||||
|
}
|
||||||
|
return modItemList.toArray(new ModItem[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModDetail getModDetails(ModItem item) {
|
||||||
|
ArrayList<JsonObject> allModDetails = new ArrayList<>();
|
||||||
|
int index = 0;
|
||||||
|
while(index != -1 && index != -2) {
|
||||||
|
index = getPaginatedDetails(allModDetails, index, item.id);
|
||||||
|
}
|
||||||
|
if(index == -2) return null;
|
||||||
|
int length = allModDetails.size();
|
||||||
|
String[] versionNames = new String[length];
|
||||||
|
String[] mcVersionNames = new String[length];
|
||||||
|
String[] versionUrls = new String[length];
|
||||||
|
for(int i = 0; i < allModDetails.size(); i++) {
|
||||||
|
JsonObject modDetail = allModDetails.get(i);
|
||||||
|
versionNames[i] = modDetail.get("displayName").getAsString();
|
||||||
|
JsonElement downloadUrl = modDetail.get("downloadUrl");
|
||||||
|
versionUrls[i] = downloadUrl.getAsString();
|
||||||
|
JsonArray gameVersions = modDetail.getAsJsonArray("gameVersions");
|
||||||
|
for(JsonElement jsonElement : gameVersions) {
|
||||||
|
String gameVersion = jsonElement.getAsString();
|
||||||
|
if(!sMcVersionPattern.matcher(gameVersion).matches()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mcVersionNames[i] = gameVersion;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ModDetail(item, versionNames, mcVersionNames, versionUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void installMod(ModDetail modDetail, int selectedVersion) {
|
||||||
|
String versionUrl = modDetail.versionUrls[selectedVersion];
|
||||||
|
File modpackFile = new File(Tools.DIR_CACHE, modDetail.id+".zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private int getPaginatedDetails(ArrayList<JsonObject> objectList, int index, String modId) {
|
||||||
|
HashMap<String, Object> params = new HashMap<>();
|
||||||
|
params.put("index", index);
|
||||||
|
params.put("pageSize", CURSEFORGE_PAGINATION_SIZE);
|
||||||
|
JsonObject response = mApiHandler.get("mods/"+modId+"/files", params, JsonObject.class);
|
||||||
|
if(response == null) return -2;
|
||||||
|
JsonArray data = response.getAsJsonArray("data");
|
||||||
|
if(data == null) return -2;
|
||||||
|
for(int i = 0; i < data.size(); i++) {
|
||||||
|
JsonObject fileInfo = data.get(i).getAsJsonObject();
|
||||||
|
if(fileInfo.get("isServerPack").getAsBoolean()) continue;
|
||||||
|
objectList.add(fileInfo);
|
||||||
|
}
|
||||||
|
if(data.size() < CURSEFORGE_PAGINATION_SIZE) {
|
||||||
|
return -1; // we read the remainder! yay!
|
||||||
|
}
|
||||||
|
return index + data.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModLoaderInfo installCurseforgeZip(File zipFile, File instanceDestination) throws IOException {
|
||||||
|
try (ZipFile modpackZipFile = new ZipFile(zipFile)){
|
||||||
|
CurseManifest curseManifest = Tools.GLOBAL_GSON.fromJson(
|
||||||
|
Tools.read(ZipUtils.getEntryStream(modpackZipFile, "manifest.json")),
|
||||||
|
CurseManifest.class);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||||
|
|
||||||
|
public class CurseManifest {
|
||||||
|
public String name;
|
||||||
|
public String version;
|
||||||
|
public String author;
|
||||||
|
public String manifestType;
|
||||||
|
public int manifestVersion;
|
||||||
|
public CurseFile[] files;
|
||||||
|
public CurseMinecraft minecraft;
|
||||||
|
public String overrides;
|
||||||
|
public static class CurseFile {
|
||||||
|
public long projectID;
|
||||||
|
public long fileID;
|
||||||
|
}
|
||||||
|
public static class CurseMinecraft {
|
||||||
|
public String version;
|
||||||
|
public CurseModLoader[] modLoaders;
|
||||||
|
}
|
||||||
|
public static class CurseModLoader {
|
||||||
|
public String id;
|
||||||
|
public boolean primary;
|
||||||
|
}
|
||||||
|
}
|
@ -6,11 +6,20 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/mod_extended_error_textview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/search_modpack_download_error"
|
||||||
|
android:textColor="#FFFF0000"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatSpinner
|
<androidx.appcompat.widget.AppCompatSpinner
|
||||||
android:id="@+id/mod_extended_version_spinner"
|
android:id="@+id/mod_extended_version_spinner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/_30sdp"
|
android:layout_height="@dimen/_30sdp"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toBottomOf="@+id/mod_extended_error_textview"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.kdt.mcgui.MineButton
|
<com.kdt.mcgui.MineButton
|
||||||
@ -18,9 +27,9 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginVertical="@dimen/padding_large"
|
android:layout_marginVertical="@dimen/padding_large"
|
||||||
|
android:enabled="false"
|
||||||
android:text="@string/generic_install"
|
android:text="@string/generic_install"
|
||||||
app:layout_constraintTop_toBottomOf="@id/mod_extended_version_spinner"
|
|
||||||
|
|
||||||
/>
|
app:layout_constraintTop_toBottomOf="@id/mod_extended_version_spinner" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -412,6 +412,8 @@
|
|||||||
|
|
||||||
<string name="search_modpack_no_result">No modpacks found</string>
|
<string name="search_modpack_no_result">No modpacks found</string>
|
||||||
<string name="search_modpack_error">Failed to find modpacks</string>
|
<string name="search_modpack_error">Failed to find modpacks</string>
|
||||||
|
<string name="search_modpack_download_error">Failed to download modpack metadata</string>
|
||||||
|
|
||||||
<string name="modpack_download_downloading_metadata">Downloading modpack metadata (%.2f MB / %.2f MB)</string>
|
<string name="modpack_download_downloading_metadata">Downloading modpack metadata (%.2f MB / %.2f MB)</string>
|
||||||
<string name="modpack_download_downloading_mods">Downloading mods (%.2f MB / %.2f MB)</string>
|
<string name="modpack_download_downloading_mods">Downloading mods (%.2f MB / %.2f MB)</string>
|
||||||
<string name="modpack_download_applying_overrides">Applying overrides (%d/%d)</string>
|
<string name="modpack_download_applying_overrides">Applying overrides (%d/%d)</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user