mirror of
https://github.com/AngelAuraMC/Amethyst-Android.git
synced 2025-09-15 15:48:26 -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.Tools;
|
||||
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.ModrinthApi;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
|
||||
@ -50,7 +52,7 @@ public class SearchModFragment extends Fragment {
|
||||
|
||||
public SearchModFragment(){
|
||||
super(R.layout.fragment_mod_search);
|
||||
modpackApi = new ModrinthApi();
|
||||
modpackApi = new CurseforgeApi();
|
||||
mModItemAdapter = new ModItemAdapter(modpackApi);
|
||||
mSearchFilters = new SearchFilters();
|
||||
mSearchFilters.isModpack = true;
|
||||
@ -82,43 +84,26 @@ public class SearchModFragment extends Fragment {
|
||||
}
|
||||
mSearchProgressBar.setVisibility(View.VISIBLE);
|
||||
mSearchFilters.name = mSearchEditText.getText().toString();
|
||||
SearchModRunnable searchModRunnable = new SearchModRunnable(mSearchFilters);
|
||||
mSearchFuture = PojavApplication.sExecutorService.submit(searchModRunnable);
|
||||
searchModRunnable.setFuture(mSearchFuture);
|
||||
mSearchFuture = new SelfReferencingFuture(new SearchModTask(mSearchFilters))
|
||||
.startOnExecutor(PojavApplication.sExecutorService);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
class SearchModRunnable implements Runnable{
|
||||
private final Object mFutureLock = new Object();
|
||||
private final SearchFilters mRunnableFilters;
|
||||
private Future<?> mMyFuture;
|
||||
class SearchModTask implements SelfReferencingFuture.FutureInterface {
|
||||
|
||||
SearchModRunnable(SearchFilters mSearchFilters) {
|
||||
this.mRunnableFilters = mSearchFilters;
|
||||
}
|
||||
|
||||
public void setFuture(Future<?> future) {
|
||||
synchronized (mFutureLock) {
|
||||
mMyFuture = future;
|
||||
mFutureLock.notifyAll();
|
||||
}
|
||||
private final SearchFilters mTaskFilters;
|
||||
SearchModTask(SearchFilters mSearchFilters) {
|
||||
this.mTaskFilters = mSearchFilters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mFutureLock) {
|
||||
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);
|
||||
public void run(Future<?> myFuture) {
|
||||
ModItem[] items = modpackApi.searchMod(mTaskFilters);
|
||||
Log.d(SearchModFragment.class.toString(), Arrays.toString(items));
|
||||
Tools.runOnUiThread(() -> {
|
||||
ModItem[] localItems = items;
|
||||
if(mMyFuture.isCancelled()) return;
|
||||
if(myFuture.isCancelled()) return;
|
||||
mSearchProgressBar.setVisibility(View.GONE);
|
||||
if(localItems == null) {
|
||||
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 java.util.Arrays;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHolder> {
|
||||
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 ModItem mModItem = null;
|
||||
private final TextView mTitle, mDescription;
|
||||
private final ImageView mIconView;
|
||||
private View mExtendedLayout;
|
||||
private Spinner mExtendedSpinner;
|
||||
private Button mExtendedButton;
|
||||
private final ImageView mIconView;
|
||||
private TextView mExtendedErrorTextView;
|
||||
private Future<?> mExtensionFuture;
|
||||
private Bitmap mThumbnailBitmap;
|
||||
private ImageReceiver mImageReceiver;
|
||||
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();
|
||||
mExtendedButton = mExtendedLayout.findViewById(R.id.mod_extended_select_version_button);
|
||||
mExtendedSpinner = mExtendedLayout.findViewById(R.id.mod_extended_version_spinner);
|
||||
mExtendedErrorTextView = mExtendedLayout.findViewById(R.id.mod_extended_error_textview);
|
||||
|
||||
mExtendedButton.setOnClickListener(v1 -> {
|
||||
mModpackApi.handleInstallation(
|
||||
@ -69,12 +73,39 @@ public class ModItemAdapter extends RecyclerView.Adapter<ModItemAdapter.ViewHold
|
||||
else mExtendedLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if(isExtended() && mModDetail == null) {
|
||||
PojavApplication.sExecutorService.execute(() -> {
|
||||
if(isExtended() && mModDetail == null && mExtensionFuture == null) { // only reload if no reloads are in progress
|
||||
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);
|
||||
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) {
|
||||
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;
|
||||
// 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 */
|
||||
private void setStateDetailed(ModDetail detailedItem) {
|
||||
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(){
|
||||
|
@ -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;
|
||||
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
@ -13,35 +14,50 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ApiHandler {
|
||||
public final String baseUrl;
|
||||
public final Map<String, String> additionalHeaders;
|
||||
|
||||
public ApiHandler(String 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) {
|
||||
return getFullUrl(baseUrl + "/" + endpoint, tClass);
|
||||
return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, 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) {
|
||||
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) {
|
||||
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;
|
||||
public static String getRaw(String url) {
|
||||
return getRaw(null, url);
|
||||
}
|
||||
|
||||
public static String getRaw(Map<String, String> headers, String url) {
|
||||
Log.d("ApiHandler", url);
|
||||
try {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
addHeaders(conn, headers);
|
||||
InputStream inputStream = conn.getInputStream();
|
||||
String data = Tools.read(inputStream);
|
||||
Log.d(ApiHandler.class.toString(), data);
|
||||
@ -55,11 +71,16 @@ public class ApiHandler {
|
||||
}
|
||||
|
||||
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 {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
addHeaders(conn, headers);
|
||||
conn.setDoOutput(true);
|
||||
|
||||
OutputStream outputStream = conn.getOutputStream();
|
||||
@ -79,6 +100,13 @@ public class ApiHandler {
|
||||
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) {
|
||||
StringBuilder params = new StringBuilder("?");
|
||||
for (String param : query.keySet()) {
|
||||
@ -89,18 +117,34 @@ public class ApiHandler {
|
||||
}
|
||||
|
||||
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) {
|
||||
return getFullUrl(url + parseQueries(query), tClass);
|
||||
return getFullUrl(null, url, query, 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) {
|
||||
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">
|
||||
|
||||
|
||||
<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
|
||||
android:id="@+id/mod_extended_version_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/_30sdp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/mod_extended_error_textview"
|
||||
/>
|
||||
|
||||
<com.kdt.mcgui.MineButton
|
||||
@ -18,9 +27,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="@dimen/padding_large"
|
||||
android:enabled="false"
|
||||
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>
|
@ -412,6 +412,8 @@
|
||||
|
||||
<string name="search_modpack_no_result">No modpacks found</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_mods">Downloading mods (%.2f MB / %.2f MB)</string>
|
||||
<string name="modpack_download_applying_overrides">Applying overrides (%d/%d)</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user