mirror of
https://github.com/AngelAuraMC/Amethyst-Android.git
synced 2026-05-19 15:34:39 -04:00
Merge pull request #155 from AngelAuraMC/feat/import-modpack
Add functionality to import modpacks
This commit is contained in:
@@ -9,6 +9,7 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
@@ -36,6 +37,10 @@ import net.kdt.pojavlaunch.fragments.SelectAuthFragment;
|
||||
import net.kdt.pojavlaunch.lifecycle.ContextAwareDoneListener;
|
||||
import net.kdt.pojavlaunch.lifecycle.ContextExecutor;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModLoader;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackInstaller;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.NotificationDownloadListener;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.IconCacheJanitor;
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
import net.kdt.pojavlaunch.prefs.screens.LauncherPreferenceFragment;
|
||||
@@ -50,7 +55,9 @@ import net.kdt.pojavlaunch.utils.NotificationUtils;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
|
||||
public class LauncherActivity extends BaseActivity {
|
||||
@@ -60,6 +67,25 @@ public class LauncherActivity extends BaseActivity {
|
||||
registerForActivityResult(new OpenDocumentWithExtension("jar"), (data)->{
|
||||
if(data != null) Tools.launchModInstaller(this, data);
|
||||
});
|
||||
public final ActivityResultLauncher<Object> modpackImportLauncher =
|
||||
registerForActivityResult(new OpenDocumentWithExtension(new String[]{"zip", "mrpack"}), (data)->{
|
||||
if(data != null) {
|
||||
PojavApplication.sExecutorService.execute(() -> {
|
||||
try {
|
||||
ModLoader loaderInfo = new CommonApi(getString(R.string.curseforge_api_key)).importModpack(this, data);
|
||||
if (loaderInfo == null) return;
|
||||
loaderInfo.getDownloadTask(new NotificationDownloadListener(this, loaderInfo)).run();
|
||||
} catch (IOException e) {
|
||||
Tools.showErrorRemote(this, R.string.modpack_install_download_failed, e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Tools.showError(this, R.string.not_modpack_file, e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should literally never happen because SHA-1 is required Java spec
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
private mcAccountSpinner mAccountSpinner;
|
||||
private FragmentContainerView mFragmentView;
|
||||
|
||||
@@ -39,7 +39,7 @@ public class PojavApplication extends Application {
|
||||
// Write to file, since some devices may not able to show error
|
||||
FileUtils.ensureParentDirectory(crashFile);
|
||||
PrintStream crashStream = new PrintStream(crashFile);
|
||||
crashStream.append("PojavLauncher crash report\n");
|
||||
crashStream.append("Amethyst crash report\n");
|
||||
crashStream.append(" - Time: ").append(DateFormat.getDateTimeInstance().format(new Date())).append("\n");
|
||||
crashStream.append(" - Device: ").append(Build.PRODUCT).append(" ").append(Build.MODEL).append("\n");
|
||||
crashStream.append(" - Android version: ").append(Build.VERSION.RELEASE).append("\n");
|
||||
|
||||
@@ -10,10 +10,14 @@ import androidx.activity.result.contract.ActivityResultContract;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
|
||||
// Android's OpenDocument contract is the basicmost crap that doesn't allow
|
||||
// you to specify practically anything. So i made this instead.
|
||||
public class OpenDocumentWithExtension extends ActivityResultContract<Object, Uri> {
|
||||
private final String mimeType;
|
||||
private final String[] mimeTypes;
|
||||
|
||||
/**
|
||||
* Create a new OpenDocumentWithExtension contract.
|
||||
@@ -25,6 +29,37 @@ public class OpenDocumentWithExtension extends ActivityResultContract<Object, Ur
|
||||
String extensionMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if(extensionMimeType == null) extensionMimeType = "*/*";
|
||||
mimeType = extensionMimeType;
|
||||
mimeTypes = null;
|
||||
}
|
||||
public OpenDocumentWithExtension(String[] extensions) {
|
||||
ArrayList<String> extensionsMimeType = new ArrayList<>();
|
||||
int count = 0;
|
||||
for (String extension: extensions) {
|
||||
String extensionMimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (Objects.equals(extension, "mrpack")) {
|
||||
// Special handling here because depending on whether the ROM has
|
||||
// `x-modrinth-modpack+zip` in their mimetypes or not because if it does,
|
||||
// `octet-stream` will no longer match mrpack files.
|
||||
|
||||
// Checking this with MimeTypeMap.hasExtension() and .hasMimeType() always returns
|
||||
// false so we do both instead.
|
||||
|
||||
// `octet-stream` highlights a lot of unrelated files but it's the best
|
||||
// we can do. Mimetypes are built into the ROM after all.
|
||||
// See https://android.googlesource.com/platform/external/mime-support/+/refs/heads/main
|
||||
// or https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/mime/java-res/android.mime.types
|
||||
extensionsMimeType.add("application/octet-stream");
|
||||
extensionsMimeType.add("application/x-modrinth-modpack+zip");
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(extensionMimetype == null) continue; // If null is passed, it matches all files
|
||||
extensionsMimeType.add(extensionMimetype);
|
||||
count++;
|
||||
}
|
||||
mimeType = "*/*";
|
||||
mimeTypes = extensionsMimeType.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -33,6 +68,7 @@ public class OpenDocumentWithExtension extends ActivityResultContract<Object, Ur
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType(mimeType);
|
||||
if(mimeTypes != null) intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog;
|
||||
import static net.kdt.pojavlaunch.Tools.hasOnlineProfile;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.kdt.mcgui.MineButton;
|
||||
|
||||
import net.kdt.pojavlaunch.BaseActivity;
|
||||
import net.kdt.pojavlaunch.LauncherActivity;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.contracts.OpenDocumentWithExtension;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
|
||||
|
||||
public class ModpackCreateFragment extends Fragment {
|
||||
public static final String TAG = "ModpackCreateFragment";
|
||||
public ModpackCreateFragment() {
|
||||
super(R.layout.fragment_create_modpack_profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
view.findViewById(R.id.button_browse_modpacks).setOnClickListener(v -> {
|
||||
tryInstall(SearchModFragment.class, SearchModFragment.TAG);
|
||||
});
|
||||
view.findViewById(R.id.button_import_modpack).setOnClickListener(v -> {
|
||||
Activity launcheractivity = requireActivity();
|
||||
if (!(launcheractivity instanceof LauncherActivity))
|
||||
throw new IllegalStateException("Cannot import modpack without LauncherActivity");
|
||||
((LauncherActivity) launcheractivity).modpackImportLauncher.launch(null);
|
||||
});;
|
||||
}
|
||||
|
||||
private void tryInstall(Class<? extends Fragment> fragmentClass, String tag){
|
||||
if(!hasOnlineProfile()){
|
||||
hasNoOnlineProfileDialog(requireActivity());
|
||||
} else {
|
||||
Tools.swapFragment(requireActivity(), fragmentClass, tag, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,41 @@ import android.widget.ExpandableListAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.modloaders.ForgeDownloadTask;
|
||||
import net.kdt.pojavlaunch.modloaders.ForgeUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.ForgeVersionListHandler;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
|
||||
import net.kdt.pojavlaunch.modloaders.NeoForgeDownloadTask;
|
||||
import net.kdt.pojavlaunch.modloaders.NeoForgeVersionListAdapter;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
public class NeoForgeInstallFragment extends ModVersionListFragment<List<String>> {
|
||||
public static final String TAG = "NeoForgeInstallFragment";
|
||||
public NeoForgeInstallFragment() {
|
||||
super(TAG);
|
||||
}
|
||||
|
||||
private static final String NEOFORGE_METADATA_URL = "https://meta.prismlauncher.org/v1/net.neoforged/index.json";
|
||||
private static final String NEOFORGE_METADATA_URL = "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml";
|
||||
|
||||
|
||||
@Override
|
||||
@@ -48,19 +61,39 @@ public class NeoForgeInstallFragment extends ModVersionListFragment<List<String>
|
||||
|
||||
@Override
|
||||
public List<String> loadVersionList() {
|
||||
String test = null;
|
||||
try {
|
||||
test = DownloadUtils.downloadStringCached(NEOFORGE_METADATA_URL, "neoforge_versions", input -> input);
|
||||
} catch (Exception e) {
|
||||
Tools.showErrorRemote(e);
|
||||
}
|
||||
return Collections.singletonList(test);
|
||||
// Moved the parsing logic to the adapter because there is no way to get this info easily, we use prism's index
|
||||
// since neoforge doesn't actually give this information easily anywhere.
|
||||
// To clarify, neoforge does not provide maven APIs to get supported Minecraft versions for each loader version
|
||||
|
||||
return downloadNeoForgeVersions();
|
||||
}
|
||||
|
||||
public static List<String> downloadNeoForgeVersions() {
|
||||
SAXParser saxParser;
|
||||
try {
|
||||
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
|
||||
saxParser = parserFactory.newSAXParser();
|
||||
}catch (SAXException | ParserConfigurationException e) {
|
||||
e.printStackTrace();
|
||||
// if we cant make a parser we might as well not even try to parse anything
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
//of_test();
|
||||
return DownloadUtils.downloadStringCached(NEOFORGE_METADATA_URL, "neoforge_versions", input -> {
|
||||
try {
|
||||
ForgeVersionListHandler handler = new ForgeVersionListHandler();
|
||||
saxParser.parse(new InputSource(new StringReader(input)), handler);
|
||||
return handler.getVersions();
|
||||
// IOException is present here StringReader throws it only if the parser called close()
|
||||
// sooner than needed, which is a parser issue and not an I/O one
|
||||
}catch (SAXException | IOException e) {
|
||||
throw new DownloadUtils.ParseException(e);
|
||||
}
|
||||
});
|
||||
}catch (DownloadUtils.ParseException | IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ExpandableListAdapter createAdapter(List<String> versionList, LayoutInflater layoutInflater) {
|
||||
return new NeoForgeVersionListAdapter(versionList, layoutInflater);
|
||||
@@ -74,7 +107,9 @@ public class NeoForgeInstallFragment extends ModVersionListFragment<List<String>
|
||||
@Override
|
||||
public void onDownloadFinished(Context context, File downloadedFile) {
|
||||
Intent modInstallerStartIntent = new Intent(context, JavaGUILauncherActivity.class);
|
||||
modInstallerStartIntent.putExtra("javaArgs", "-jar "+downloadedFile.getAbsolutePath()+" --install-client");
|
||||
modInstallerStartIntent
|
||||
.putExtra("javaArgs", "-jar "+downloadedFile.getAbsolutePath()+" --install-client")
|
||||
.putExtra("openLogOutput", true);
|
||||
context.startActivity(modInstallerStartIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ public class ProfileTypeSelectFragment extends Fragment {
|
||||
public ProfileTypeSelectFragment() {
|
||||
super(R.layout.fragment_profile_type);
|
||||
}
|
||||
public ProfileTypeSelectFragment(int layout) {
|
||||
super(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
@@ -39,7 +42,7 @@ public class ProfileTypeSelectFragment extends Fragment {
|
||||
view.findViewById(R.id.modded_profile_neoforge).setOnClickListener((v)->
|
||||
tryInstall(NeoForgeInstallFragment.class, NeoForgeInstallFragment.TAG));
|
||||
view.findViewById(R.id.modded_profile_modpack).setOnClickListener((v)->
|
||||
tryInstall(SearchModFragment.class, SearchModFragment.TAG));
|
||||
tryInstall(ModpackCreateFragment.class, ModpackCreateFragment.TAG));
|
||||
view.findViewById(R.id.modded_profile_quilt).setOnClickListener((v)->
|
||||
tryInstall(QuiltInstallFragment.class, QuiltInstallFragment.TAG));
|
||||
view.findViewById(R.id.modded_profile_bta).setOnClickListener((v)->
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.fragments.NeoForgeInstallFragment;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
@@ -13,26 +16,23 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback {
|
||||
private String mDownloadUrl;
|
||||
private String mFullVersion;
|
||||
private String mLoaderVersion;
|
||||
private String mGameVersion;
|
||||
private final String mDownloadUrl;
|
||||
private final String mLoaderVersion;
|
||||
|
||||
private final ModloaderDownloadListener mListener;
|
||||
public NeoForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion) {
|
||||
|
||||
public NeoForgeDownloadTask(ModloaderDownloadListener listener, @NonNull String loaderVersion) {
|
||||
this.mListener = listener;
|
||||
this.mDownloadUrl = "https://maven.neoforged.net/releases/net/neoforged/neoforge/"+ forgeVersion +"/neoforge-"+forgeVersion+"-installer.jar";
|
||||
this.mFullVersion = forgeVersion;
|
||||
this.mDownloadUrl = String.format(NEOFORGE_INSTALLER_URL, loaderVersion);
|
||||
this.mLoaderVersion = loaderVersion;
|
||||
}
|
||||
|
||||
public NeoForgeDownloadTask(ModloaderDownloadListener listener, String gameVersion, String loaderVersion) {
|
||||
this.mListener = listener;
|
||||
this.mLoaderVersion = loaderVersion;
|
||||
this.mGameVersion = gameVersion;
|
||||
}
|
||||
private static final String NEOFORGE_INSTALLER_URL = "https://maven.neoforged.net/releases/net/neoforged/neoforge/%1$s/neoforge-%1$s-installer.jar";
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if(determineDownloadUrl()) {
|
||||
downloadForge();
|
||||
downloadNeoForge();
|
||||
}
|
||||
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
|
||||
}
|
||||
@@ -40,11 +40,11 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback
|
||||
@Override
|
||||
public void updateProgress(int curr, int max) {
|
||||
int progress100 = (int)(((float)curr / (float)max)*100f);
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mFullVersion);
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mLoaderVersion);
|
||||
}
|
||||
|
||||
private void downloadForge() {
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mFullVersion);
|
||||
private void downloadNeoForge() {
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mLoaderVersion);
|
||||
try {
|
||||
File destinationFile = new File(Tools.DIR_CACHE, "neoforge-installer.jar");
|
||||
byte[] buffer = new byte[8192];
|
||||
@@ -58,8 +58,7 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback
|
||||
}
|
||||
|
||||
public boolean determineDownloadUrl() {
|
||||
if(mDownloadUrl != null && mFullVersion != null) return true;
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_searching);
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.neoforge_dl_searching);
|
||||
try {
|
||||
if(!findVersion()) {
|
||||
mListener.onDataNotAvailable();
|
||||
@@ -73,13 +72,10 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback
|
||||
}
|
||||
|
||||
public boolean findVersion() throws IOException {
|
||||
List<String> forgeVersions = ForgeUtils.downloadForgeVersions();
|
||||
if(forgeVersions == null) return false;
|
||||
String versionStart = mGameVersion+"-"+mLoaderVersion;
|
||||
for(String versionName : forgeVersions) {
|
||||
if(!versionName.startsWith(versionStart)) continue;
|
||||
mFullVersion = versionName;
|
||||
mDownloadUrl = ForgeUtils.getInstallerUrl(mFullVersion);
|
||||
List<String> neoforgeVersions = NeoForgeInstallFragment.downloadNeoForgeVersions();
|
||||
if(neoforgeVersions == null) return false;
|
||||
for(String versionName : neoforgeVersions) {
|
||||
if(!versionName.startsWith(mLoaderVersion)) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -7,58 +7,57 @@ import android.widget.BaseExpandableListAdapter;
|
||||
import android.widget.ExpandableListAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class NeoForgeVersionListAdapter extends BaseExpandableListAdapter implements ExpandableListAdapter {
|
||||
private final List<String> mGameVersions;
|
||||
private final List<List<String>> mNeoForgeVersions;
|
||||
private final LayoutInflater mLayoutInflater;
|
||||
private final LinkedHashMap<String, LinkedHashSet<String>> minecraftToLoaderVersionsHashmap;
|
||||
private LinkedHashSet<String> generatedHashSet = null;
|
||||
|
||||
|
||||
public NeoForgeVersionListAdapter(List<String> forgeVersions, LayoutInflater layoutInflater) {
|
||||
public NeoForgeVersionListAdapter(List<String> neoforgeVersions, LayoutInflater layoutInflater) {
|
||||
this.mLayoutInflater = layoutInflater;
|
||||
minecraftToLoaderVersionsHashmap = new LinkedHashMap<>();
|
||||
JsonArray versionsJsonArray = JsonParser.parseString(forgeVersions.get(0)).getAsJsonObject().getAsJsonArray("versions");
|
||||
|
||||
ArrayList<JsonElement> sortedVersionsList = new ArrayList<>();
|
||||
for (JsonElement elem : versionsJsonArray) {
|
||||
sortedVersionsList.add(elem);
|
||||
}
|
||||
Collections.sort(sortedVersionsList, (o1, o2) -> {
|
||||
String versionString1 = ((JsonObject) o1).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
|
||||
String versionString2 = ((JsonObject) o2).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
|
||||
return versionString2.compareTo(versionString1); // Sorts by Minecraft version
|
||||
});
|
||||
|
||||
for (JsonElement sortedVersionPick : sortedVersionsList) {
|
||||
String loaderVersion = ((JsonObject) sortedVersionPick).get("version").getAsString();
|
||||
String minecraftVersion = ((JsonObject) sortedVersionPick).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
|
||||
if (minecraftToLoaderVersionsHashmap.containsKey(minecraftVersion)) {
|
||||
minecraftToLoaderVersionsHashmap.get(minecraftVersion).add(loaderVersion);
|
||||
} else {
|
||||
generatedHashSet = new LinkedHashSet<>();
|
||||
generatedHashSet.add(loaderVersion);
|
||||
minecraftToLoaderVersionsHashmap.put(minecraftVersion, generatedHashSet);
|
||||
mGameVersions = new ArrayList<>();
|
||||
mNeoForgeVersions = new ArrayList<>();
|
||||
for(String version : neoforgeVersions) {
|
||||
String[] parts = version.split("\\.");
|
||||
String gameVersion;
|
||||
try {
|
||||
if (Integer.parseInt(parts[1]) < 25) { // Actual logic for normal mcvers
|
||||
gameVersion = "1." + parts[0] + "." + parts[1];
|
||||
} else gameVersion = parts[0] + "." + parts[1];
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Handling for april fools version
|
||||
gameVersion = parts[0] + "." + parts[1];
|
||||
}
|
||||
List<String> versionList;
|
||||
int gameVersionIndex = mGameVersions.indexOf(gameVersion);
|
||||
if(gameVersionIndex != -1) versionList = mNeoForgeVersions.get(gameVersionIndex);
|
||||
else {
|
||||
versionList = new ArrayList<>();
|
||||
mGameVersions.add(gameVersion);
|
||||
mNeoForgeVersions.add(versionList);
|
||||
}
|
||||
versionList.add(version);
|
||||
}
|
||||
// Make it latest to oldest, top to down.
|
||||
Collections.reverse(mGameVersions);
|
||||
Collections.reverse(mNeoForgeVersions);
|
||||
for (List<String> mNeoForgeVersion : mNeoForgeVersions){
|
||||
Collections.reverse(mNeoForgeVersion);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int getGroupCount() {
|
||||
return minecraftToLoaderVersionsHashmap.size();
|
||||
return mGameVersions.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildrenCount(int i) {
|
||||
return new ArrayList<>(minecraftToLoaderVersionsHashmap.values()).get(i).size();
|
||||
return mNeoForgeVersions.get(i).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,7 +67,7 @@ public class NeoForgeVersionListAdapter extends BaseExpandableListAdapter implem
|
||||
|
||||
@Override
|
||||
public Object getChild(int i, int i1) {
|
||||
return getForgeVersion(i, i1);
|
||||
return getNeoForgeVersion(i, i1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,16 +99,16 @@ public class NeoForgeVersionListAdapter extends BaseExpandableListAdapter implem
|
||||
public View getChildView(int i, int i1, boolean b, View convertView, ViewGroup viewGroup) {
|
||||
if(convertView == null)
|
||||
convertView = mLayoutInflater.inflate(android.R.layout.simple_expandable_list_item_1, viewGroup, false);
|
||||
((TextView) convertView).setText(getForgeVersion(i, i1));
|
||||
((TextView) convertView).setText(getNeoForgeVersion(i, i1));
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private String getGameVersion(int i) {
|
||||
return minecraftToLoaderVersionsHashmap.keySet().toArray()[i].toString();
|
||||
return mGameVersions.get(i);
|
||||
}
|
||||
|
||||
private String getForgeVersion(int i, int i1){
|
||||
return new ArrayList<>(minecraftToLoaderVersionsHashmap.values()).get(i).toArray()[i1].toString();
|
||||
private String getNeoForgeVersion(int i, int i1){
|
||||
return mNeoForgeVersions.get(i).get(i1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.kdt.pojavlaunch.PojavApplication;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
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.modloaders.modpacks.models.SearchResult;
|
||||
|
||||
import org.jdom2.IllegalDataException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* Group all apis under the same umbrella, as another layer of abstraction
|
||||
@@ -112,6 +123,11 @@ public class CommonApi implements ModpackApi {
|
||||
return getModpackApi(modDetail.apiSource).installMod(modDetail, selectedVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
|
||||
return getModpackApi(activity, zipUri).importModpack(activity, zipUri);
|
||||
}
|
||||
|
||||
private @NonNull ModpackApi getModpackApi(int apiSource) {
|
||||
switch (apiSource) {
|
||||
case Constants.SOURCE_MODRINTH:
|
||||
@@ -123,6 +139,31 @@ public class CommonApi implements ModpackApi {
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull ModpackApi getModpackApi(Activity activity, Uri zipUri){
|
||||
String modrinthPackInfoFileName = "modrinth.index.json";
|
||||
String curseforgePackInfoFileName = "manifest.json";
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = activity.getContentResolver().openInputStream(zipUri);
|
||||
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
|
||||
ZipEntry zipEntry;
|
||||
boolean isModrinth;
|
||||
boolean isCurseforge;
|
||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
||||
isModrinth = zipEntry.getName().equals(modrinthPackInfoFileName);
|
||||
isCurseforge = zipEntry.getName().equals(curseforgePackInfoFileName);
|
||||
if(isModrinth) {
|
||||
return mModrinthApi;
|
||||
} else if (isCurseforge) {
|
||||
return mCurseforgeApi;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
throw new IllegalArgumentException("Zip provided does not contain a manifest file");
|
||||
}
|
||||
|
||||
/** Fuse the arrays in a way that's fair for every endpoint */
|
||||
private ModItem[] buildFusedResponse(List<ModItem[]> modMatrix){
|
||||
int totalSize = 0;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -25,6 +27,7 @@ import net.kdt.pojavlaunch.utils.ZipUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -137,6 +140,11 @@ public class CurseforgeApi implements ModpackApi{
|
||||
return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installCurseforgeZip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
|
||||
return ModpackInstaller.importModpack(activity, zipUri, this::installCurseforgeZip);
|
||||
}
|
||||
|
||||
|
||||
private int getPaginatedDetails(ArrayList<JsonObject> objectList, int index, String modId) {
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
@@ -211,6 +219,9 @@ public class CurseforgeApi implements ModpackApi{
|
||||
case "fabric":
|
||||
modLoaderTypeInt = ModLoader.MOD_LOADER_FABRIC;
|
||||
break;
|
||||
case "neoforge":
|
||||
modLoaderTypeInt = ModLoader.MOD_LOADER_NEOFORGE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
//TODO: Quilt is also Forge? How does that work?
|
||||
|
||||
@@ -9,6 +9,7 @@ import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.ForgeDownloadTask;
|
||||
import net.kdt.pojavlaunch.modloaders.ForgeUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
|
||||
import net.kdt.pojavlaunch.modloaders.NeoForgeDownloadTask;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -16,6 +17,7 @@ public class ModLoader {
|
||||
public static final int MOD_LOADER_FORGE = 0;
|
||||
public static final int MOD_LOADER_FABRIC = 1;
|
||||
public static final int MOD_LOADER_QUILT = 2;
|
||||
public static final int MOD_LOADER_NEOFORGE = 3;
|
||||
public final int modLoaderType;
|
||||
public final String modLoaderVersion;
|
||||
public final String minecraftVersion;
|
||||
@@ -38,6 +40,8 @@ public class ModLoader {
|
||||
return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion;
|
||||
case MOD_LOADER_QUILT:
|
||||
return "quilt-loader-"+modLoaderVersion+"-"+minecraftVersion;
|
||||
case MOD_LOADER_NEOFORGE:
|
||||
return "neoforge-"+modLoaderVersion;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -57,6 +61,8 @@ public class ModLoader {
|
||||
return createFabriclikeTask(listener, FabriclikeUtils.FABRIC_UTILS);
|
||||
case MOD_LOADER_QUILT:
|
||||
return createFabriclikeTask(listener, FabriclikeUtils.QUILT_UTILS);
|
||||
case MOD_LOADER_NEOFORGE:
|
||||
return new NeoForgeDownloadTask(listener, modLoaderVersion);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -77,6 +83,11 @@ public class ModLoader {
|
||||
case MOD_LOADER_FORGE:
|
||||
ForgeUtils.addAutoInstallArgs(baseIntent, modInstallerJar, getVersionId());
|
||||
return baseIntent;
|
||||
case MOD_LOADER_NEOFORGE:
|
||||
return baseIntent
|
||||
.putExtra("javaArgs", "-jar "+modInstallerJar.getAbsolutePath()+" --install-client")
|
||||
.putExtra("openLogOutput", true)
|
||||
;
|
||||
case MOD_LOADER_QUILT:
|
||||
case MOD_LOADER_FABRIC:
|
||||
default:
|
||||
@@ -91,6 +102,7 @@ public class ModLoader {
|
||||
public boolean requiresGuiInstallation() {
|
||||
switch (modLoaderType) {
|
||||
case MOD_LOADER_FORGE:
|
||||
case MOD_LOADER_NEOFORGE:
|
||||
return true;
|
||||
case MOD_LOADER_FABRIC:
|
||||
case MOD_LOADER_QUILT:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
@@ -12,8 +14,12 @@ 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.SearchResult;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -70,4 +76,13 @@ public interface ModpackApi {
|
||||
* @param selectedVersion The selected version
|
||||
*/
|
||||
ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException;
|
||||
|
||||
/**
|
||||
* Imports the mod(pack) from a file.
|
||||
* May require the download of additional files.
|
||||
* May requires launching the installation of a modloader
|
||||
* @param activity any activity
|
||||
* @param zipUri URI to DocumentsUI selected zip file
|
||||
*/
|
||||
ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
@@ -11,10 +16,20 @@ import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class ModpackInstaller {
|
||||
|
||||
@@ -69,7 +84,94 @@ public class ModpackInstaller {
|
||||
return modLoaderInfo;
|
||||
}
|
||||
|
||||
interface InstallFunction {
|
||||
public static ModLoader importModpack(Activity activity, Uri zipUri, InstallFunction installFunction) throws IOException, NoSuchAlgorithmException {
|
||||
String modrinthPackInfoFileName = "modrinth.index.json";
|
||||
String curseforgePackInfoFileName = "manifest.json";
|
||||
InputStream inputStream = null;
|
||||
inputStream = activity.getContentResolver().openInputStream(zipUri);
|
||||
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
|
||||
ZipEntry zipEntry;
|
||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
||||
boolean isModrinth = zipEntry.getName().equals(modrinthPackInfoFileName);
|
||||
boolean isCurseforge = zipEntry.getName().equals(curseforgePackInfoFileName);
|
||||
if (!(isModrinth || isCurseforge)) continue;
|
||||
// Read Manifest JSON
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(zipInputStream));
|
||||
String str;
|
||||
StringBuilder jsonString = new StringBuilder();
|
||||
while ((str = reader.readLine()) != null) {
|
||||
jsonString.append(str).append("\n");
|
||||
}
|
||||
zipInputStream.close();
|
||||
|
||||
// Hash the ZIP File
|
||||
inputStream = activity.getContentResolver().openInputStream(zipUri);
|
||||
MessageDigest algorithm = MessageDigest.getInstance("SHA-1");
|
||||
DigestInputStream hashingStream = new DigestInputStream(inputStream, algorithm);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
while (hashingStream.read(buffer) != -1) {} // just read to update the digest
|
||||
hashingStream.close();
|
||||
byte[] digest = algorithm.digest();
|
||||
StringBuilder sb = new StringBuilder(digest.length * 2);
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
String hash = sb.toString();
|
||||
|
||||
// Parse the JSON to prepare for instance creation
|
||||
JsonObject packInfoJson = JsonParser.parseString(jsonString.toString()).getAsJsonObject();
|
||||
String modpackName;
|
||||
if(isModrinth){
|
||||
// Added a for because there is an awkward __ that I can't be bothered to fix
|
||||
// FO only deduplication be like:
|
||||
modpackName = (packInfoJson.get("name").getAsString().toLowerCase(Locale.ROOT) +
|
||||
packInfoJson.get("versionId") + "for" +
|
||||
packInfoJson.get("dependencies").getAsJsonObject().get("minecraft"));
|
||||
} else {
|
||||
modpackName = (packInfoJson.get("name").getAsString().toLowerCase(Locale.ROOT) +
|
||||
packInfoJson.get("version") + "for" +
|
||||
packInfoJson.get("minecraft").getAsJsonObject().get("version"));
|
||||
}
|
||||
modpackName = modpackName.trim().replaceAll("[\\\\/:*?\"<>| \\t\\n]", "_");
|
||||
modpackName = modpackName + hash;
|
||||
|
||||
// Copy ZIP file to cache
|
||||
if(modpackName == null) throw new IOException("Corrupt Modpack manifest file.");
|
||||
File modpackFile = null;
|
||||
modpackFile = new File(Tools.DIR_CACHE, modpackName + ".cf");
|
||||
inputStream = activity.getContentResolver().openInputStream(zipUri);
|
||||
FileOutputStream output = new FileOutputStream(modpackFile);
|
||||
byte[] b = new byte[4 * 1024];
|
||||
int read;
|
||||
while ((read = inputStream.read(b)) != -1) {
|
||||
output.write(b, 0, read);
|
||||
}
|
||||
output.flush();
|
||||
|
||||
// Install the actual pack into custom_instances
|
||||
ModLoader modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName));
|
||||
// We have to do this because installModpack doesn't clean up after itself
|
||||
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
|
||||
modpackFile.delete();
|
||||
if(modLoaderInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the instance (We don't have a picture guys)
|
||||
MinecraftProfile profile = new MinecraftProfile();
|
||||
profile.gameDir = "./custom_instances/" + modpackName;
|
||||
profile.name = packInfoJson.get("name").getAsString();
|
||||
profile.lastVersionId = modLoaderInfo.getVersionId();
|
||||
LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile);
|
||||
LauncherProfiles.write();
|
||||
|
||||
return modLoaderInfo;
|
||||
}
|
||||
throw new IOException("Can't find manifest file in modpack provided");
|
||||
}
|
||||
|
||||
interface InstallFunction {
|
||||
ModLoader installModpack(File modpackFile, File instanceDestination) throws IOException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
@@ -17,6 +20,7 @@ import net.kdt.pojavlaunch.utils.ZipUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipFile;
|
||||
@@ -116,6 +120,11 @@ public class ModrinthApi implements ModpackApi{
|
||||
return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installMrpack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
|
||||
return ModpackInstaller.importModpack(activity, zipUri, this::installMrpack);
|
||||
}
|
||||
|
||||
private static ModLoader createInfo(ModrinthIndex modrinthIndex) {
|
||||
if(modrinthIndex == null) return null;
|
||||
Map<String, String> dependencies = modrinthIndex.dependencies;
|
||||
@@ -131,6 +140,9 @@ public class ModrinthApi implements ModpackApi{
|
||||
if((modLoaderVersion = dependencies.get("quilt-loader")) != null) {
|
||||
return new ModLoader(ModLoader.MOD_LOADER_QUILT, modLoaderVersion, mcVersion);
|
||||
}
|
||||
if((modLoaderVersion = dependencies.get("neoforge")) != null) {
|
||||
return new ModLoader(ModLoader.MOD_LOADER_NEOFORGE, modLoaderVersion, mcVersion);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:background="@color/background_app"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/fragment_padding_medium">
|
||||
|
||||
<View
|
||||
android:id="@+id/create_modpack_profile_menu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/_200sdp"
|
||||
|
||||
android:background="@drawable/background_card"
|
||||
android:translationZ="-1dp"
|
||||
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<com.kdt.mcgui.MineButton
|
||||
android:id="@+id/button_browse_modpacks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Browse Modpacks"
|
||||
android:textSize="@dimen/_12ssp"
|
||||
android:layout_marginHorizontal="@dimen/_25sdp"
|
||||
android:layout_marginBottom="@dimen/_20sdp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/button_import_modpack"
|
||||
app:layout_constraintEnd_toEndOf="@+id/create_modpack_profile_menu"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/create_modpack_profile_menu"
|
||||
app:layout_constraintTop_toTopOf="@id/create_modpack_profile_menu"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
/>
|
||||
|
||||
<com.kdt.mcgui.MineButton
|
||||
android:id="@+id/button_import_modpack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Import Zip or mrpack"
|
||||
android:textSize="@dimen/_12ssp"
|
||||
android:layout_marginHorizontal="@dimen/_25sdp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/create_modpack_profile_menu"
|
||||
app:layout_constraintEnd_toEndOf="@+id/create_modpack_profile_menu"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/create_modpack_profile_menu"
|
||||
app:layout_constraintTop_toBottomOf="@+id/button_browse_modpacks" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -301,6 +301,7 @@
|
||||
<string name="version_select_hint">Select a version</string>
|
||||
<string name="forge_dl_progress">Downloading installer for %s</string>
|
||||
<string name="forge_dl_searching">Searching for Forge version number</string>
|
||||
<string name="neoforge_dl_searching">Searching for NeoForge version number</string>
|
||||
<string name="modloader_dl_failed_to_load_list">Failed to load the version list!</string>
|
||||
<string name="forge_dl_no_installer">Sorry, but this version of Forge does not have an installer, which is not yet supported.</string>
|
||||
<string name="forge_dl_select_version">Select Forge version</string>
|
||||
@@ -498,5 +499,6 @@
|
||||
<string name="sodium_math_warning_message" translatable="false">
|
||||
Sodium is unsupported, you are on your own. No support will be given in the discord server.\nUsing sodium may result in bugs, glitches, and crashes. No help will be given even if you lose any of your worlds or saves.\n\nNow what\'s %1$s multiplied by %2$s plus %3$s minus %4$s?
|
||||
</string>
|
||||
<string name="not_modpack_file">Not a modpack file!</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user