mirror of
https://github.com/AngelAuraMC/Amethyst-Android.git
synced 2026-04-18 16:46:58 -04:00
Update zink with latest v3_openjdk commits
This commit is contained in:
1
.github/workflows/android.yml
vendored
1
.github/workflows/android.yml
vendored
@@ -15,6 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
GPLAY_KEYSTORE_PASSWORD: ${{ secrets.GPLAY_KEYSTORE_PASSWORD }}
|
||||
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,4 +5,5 @@ app_pojavlauncher/src/main/assets/components/jre
|
||||
local.properties
|
||||
.idea/
|
||||
app_pojavlauncher/.cxx/
|
||||
.vs/
|
||||
.vs/
|
||||
/curseforge_key.txt
|
||||
|
||||
@@ -69,6 +69,17 @@ def getVersionName = {
|
||||
return TAG_STRING.trim().replace("-g", "-") + "-" + BRANCH.toString().trim()
|
||||
}
|
||||
|
||||
def getCFApiKey = {
|
||||
String key = System.getenv("CURSEFORGE_API_KEY");
|
||||
if(key != null) return key;
|
||||
File curseforgeKeyFile = new File("./curseforge_key.txt");
|
||||
if(curseforgeKeyFile.canRead() && curseforgeKeyFile.isFile()) {
|
||||
return curseforgeKeyFile.text;
|
||||
}
|
||||
logger.warn('BUILD: You have no CurseForge key, the curseforge api will get disabled !');
|
||||
return "DUMMY";
|
||||
}
|
||||
|
||||
configurations {
|
||||
instrumentedClasspath {
|
||||
canBeConsumed = false
|
||||
@@ -107,6 +118,7 @@ android {
|
||||
versionCode getDateSeconds()
|
||||
versionName getVersionName()
|
||||
multiDexEnabled true //important
|
||||
resValue 'string', 'curseforge_api_key', getCFApiKey()
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -187,6 +199,7 @@ dependencies {
|
||||
implementation 'com.github.PojavLauncherTeam:portrait-ssp:6c02fd739b'
|
||||
implementation 'com.github.Mathias-Boulay:ExtendedView:1.0.0'
|
||||
implementation 'com.github.Mathias-Boulay:android_gamepad_remapper:eb92e3a5bb'
|
||||
implementation 'com.github.Mathias-Boulay:virtual-joystick-android:cb7bf45ba5'
|
||||
|
||||
|
||||
// implementation 'com.intuit.sdp:sdp-android:1.0.5'
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
android:name="android.hardware.type.pc"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
@@ -77,11 +78,15 @@
|
||||
android:name=".FatalErrorActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
|
||||
<activity android:name=".ShowErrorActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
|
||||
<activity
|
||||
android:process=":gui_installer"
|
||||
android:name=".JavaGUILauncherActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|keyboard|navigation|uiMode"
|
||||
android:screenOrientation="sensorLandscape" />
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
1688133008591
|
||||
1692525087345
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
1689180036097
|
||||
1687078018167
|
||||
@@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"(Default)": {
|
||||
"name": "(Default)",
|
||||
"lastVersionId": "Unknown"
|
||||
"lastVersionId": "1.7.10"
|
||||
}
|
||||
},
|
||||
"selectedProfile": "(Default)"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.kdt;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Basic adapter, expect it uses the what is passed by the code, no the resources
|
||||
* @param <T>
|
||||
*/
|
||||
public class SimpleArrayAdapter<T> extends BaseAdapter {
|
||||
private List<T> mObjects;
|
||||
public SimpleArrayAdapter(List<T> objects) {
|
||||
setObjects(objects);
|
||||
}
|
||||
|
||||
public void setObjects(@Nullable List<T> objects) {
|
||||
if(objects == null){
|
||||
if(mObjects != Collections.emptyList()) {
|
||||
mObjects = Collections.emptyList();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
} else {
|
||||
if(objects != mObjects){
|
||||
mObjects = objects;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mObjects.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getItem(int position) {
|
||||
return mObjects.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
if(convertView == null){
|
||||
convertView = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||
}
|
||||
|
||||
TextView v = (TextView) convertView;
|
||||
v.setText(mObjects.get(position).toString());
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class TextProgressBar extends ProgressBar {
|
||||
protected synchronized void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
mTextPaint.setTextSize((float) ((getHeight()- getPaddingBottom() - getPaddingTop()) * 0.55));
|
||||
int xPos = (int) Math.max(Math.min(getProgress() * getWidth() / getMax(), getWidth() - mTextPaint.measureText(mText)) - mTextPadding, mTextPadding);
|
||||
int xPos = (int) Math.max(Math.min((getProgress() * getWidth() / getMax()) + mTextPadding, getWidth() - mTextPaint.measureText(mText) - mTextPadding) , mTextPadding);
|
||||
int yPos = (int) ((getHeight() / 2) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)) ;
|
||||
|
||||
canvas.drawText(mText, xPos, yPos, mTextPaint);
|
||||
|
||||
@@ -56,7 +56,7 @@ public class mcVersionSpinner extends ExtendedTextView {
|
||||
private ListView mListView = null;
|
||||
private PopupWindow mPopupWindow = null;
|
||||
private Object mPopupAnimation;
|
||||
private final ProfileAdapter mProfileAdapter = new ProfileAdapter(getContext(), new ProfileAdapterExtra[]{
|
||||
private final ProfileAdapter mProfileAdapter = new ProfileAdapter(new ProfileAdapterExtra[]{
|
||||
new ProfileAdapterExtra(VERSION_SPINNER_PROFILE_CREATE,
|
||||
R.string.create_profile,
|
||||
ResourcesCompat.getDrawable(getResources(), R.drawable.ic_add, null)),
|
||||
@@ -77,6 +77,11 @@ public class mcVersionSpinner extends ExtendedTextView {
|
||||
mProfileAdapter.setViewProfile(this, (String) mProfileAdapter.getItem(position), false);
|
||||
}
|
||||
|
||||
/** Reload profiles from the file, forcing the spinner to consider the new data */
|
||||
public void reloadProfiles(){
|
||||
mProfileAdapter.reloadProfiles();
|
||||
}
|
||||
|
||||
/** Initialize various behaviors */
|
||||
private void init(){
|
||||
// Setup various attributes
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlDrawerData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlJoystickData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
|
||||
import net.kdt.pojavlaunch.customcontrols.EditorExitable;
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
@@ -43,11 +44,11 @@ public class CustomControlsActivity extends BaseActivity implements EditorExitab
|
||||
switch(position) {
|
||||
case 0: mControlLayout.addControlButton(new ControlData("New")); break;
|
||||
case 1: mControlLayout.addDrawer(new ControlDrawerData()); break;
|
||||
//case 2: mControlLayout.addJoystickButton(new ControlData()); break;
|
||||
case 2: mControlLayout.openLoadDialog(); break;
|
||||
case 3: mControlLayout.openSaveDialog(this); break;
|
||||
case 4: mControlLayout.openSetDefaultDialog(); break;
|
||||
case 5: // Saving the currently shown control
|
||||
case 2: mControlLayout.addJoystickButton(new ControlJoystickData()); break;
|
||||
case 3: mControlLayout.openLoadDialog(); break;
|
||||
case 4: mControlLayout.openSaveDialog(this); break;
|
||||
case 5: mControlLayout.openSetDefaultDialog(); break;
|
||||
case 6: // Saving the currently shown control
|
||||
try {
|
||||
Uri contentUri = DocumentsContract.buildDocumentUri(getString(R.string.storageProviderAuthorities), mControlLayout.saveToDirectory(mControlLayout.mLayoutFileName));
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ public class JMinecraftVersionList {
|
||||
public static class JavaVersionInfo {
|
||||
public String component;
|
||||
public int majorVersion;
|
||||
public int version; // parameter used by LabyMod 4
|
||||
}
|
||||
@Keep
|
||||
public static class LoggingConfig {
|
||||
|
||||
@@ -55,7 +55,7 @@ public class JRE17Util {
|
||||
if (versionInfo.javaVersion == null || versionInfo.javaVersion.component.equalsIgnoreCase("jre-legacy"))
|
||||
return true;
|
||||
|
||||
LauncherProfiles.update();
|
||||
LauncherProfiles.load();
|
||||
MinecraftProfile minecraftProfile = LauncherProfiles.getCurrentProfile();
|
||||
|
||||
String selectedRuntime = Tools.getSelectedRuntime(minecraftProfile);
|
||||
@@ -71,7 +71,7 @@ public class JRE17Util {
|
||||
JRE17Util.checkInternalNewJre(activity.getAssets());
|
||||
}
|
||||
minecraftProfile.javaDir = Tools.LAUNCHERPROFILES_RTPREFIX + appropriateRuntime;
|
||||
LauncherProfiles.update();
|
||||
LauncherProfiles.load();
|
||||
} else {
|
||||
if (versionInfo.javaVersion.majorVersion <= 17) { // there's a chance we have an internal one for this case
|
||||
if (!JRE17Util.checkInternalNewJre(activity.getAssets())){
|
||||
@@ -79,7 +79,7 @@ public class JRE17Util {
|
||||
return false;
|
||||
} else {
|
||||
minecraftProfile.javaDir = Tools.LAUNCHERPROFILES_RTPREFIX + JRE17Util.NEW_JRE_NAME;
|
||||
LauncherProfiles.update();
|
||||
LauncherProfiles.load();
|
||||
}
|
||||
} else {
|
||||
showRuntimeFail(activity, versionInfo);
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.fragment.app.FragmentManager;
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
import com.kdt.mcgui.mcAccountSpinner;
|
||||
|
||||
import net.kdt.pojavlaunch.contextexecutor.ContextExecutor;
|
||||
import net.kdt.pojavlaunch.fragments.MainMenuFragment;
|
||||
import net.kdt.pojavlaunch.fragments.MicrosoftLoginFragment;
|
||||
import net.kdt.pojavlaunch.extra.ExtraConstants;
|
||||
@@ -31,6 +32,8 @@ import net.kdt.pojavlaunch.extra.ExtraCore;
|
||||
import net.kdt.pojavlaunch.extra.ExtraListener;
|
||||
|
||||
import net.kdt.pojavlaunch.fragments.SelectAuthFragment;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.IconCacheJanitor;
|
||||
import net.kdt.pojavlaunch.multirt.MultiRTConfigDialog;
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
import net.kdt.pojavlaunch.prefs.screens.LauncherPreferenceFragment;
|
||||
@@ -53,6 +56,7 @@ public class LauncherActivity extends BaseActivity {
|
||||
private ImageButton mSettingsButton, mDeleteAccountButton;
|
||||
private ProgressLayout mProgressLayout;
|
||||
private ProgressServiceKeeper mProgressServiceKeeper;
|
||||
private ModloaderInstallTracker mInstallTracker;
|
||||
|
||||
/* Allows to switch from one button "type" to another */
|
||||
private final FragmentManager.FragmentLifecycleCallbacks mFragmentCallbackListener = new FragmentManager.FragmentLifecycleCallbacks() {
|
||||
@@ -152,6 +156,7 @@ public class LauncherActivity extends BaseActivity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pojav_launcher);
|
||||
IconCacheJanitor.runJanitor();
|
||||
getWindow().setBackgroundDrawable(null);
|
||||
bindViews();
|
||||
ProgressKeeper.addTaskCountListener((mProgressServiceKeeper = new ProgressServiceKeeper(this)));
|
||||
@@ -167,6 +172,8 @@ public class LauncherActivity extends BaseActivity {
|
||||
|
||||
new AsyncVersionList().getVersionList(versions -> ExtraCore.setValue(ExtraConstants.RELEASE_TABLE, versions), false);
|
||||
|
||||
mInstallTracker = new ModloaderInstallTracker(this);
|
||||
|
||||
mProgressLayout.observe(ProgressLayout.DOWNLOAD_MINECRAFT);
|
||||
mProgressLayout.observe(ProgressLayout.UNPACK_RUNTIME);
|
||||
mProgressLayout.observe(ProgressLayout.INSTALL_MODPACK);
|
||||
@@ -174,6 +181,20 @@ public class LauncherActivity extends BaseActivity {
|
||||
mProgressLayout.observe(ProgressLayout.DOWNLOAD_VERSION_LIST);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
ContextExecutor.setActivity(this);
|
||||
mInstallTracker.attach();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
ContextExecutor.clearActivity();
|
||||
mInstallTracker.detach();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setFullscreen() {
|
||||
return false;
|
||||
|
||||
@@ -322,24 +322,24 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
}
|
||||
MinecraftAccount minecraftAccount = PojavProfile.getCurrentProfileContent(this, null);
|
||||
Logger.appendToLog("--------- beginning with launcher debug");
|
||||
printLauncherInfo(versionId);
|
||||
printLauncherInfo(versionId, Tools.isValidString(minecraftProfile.javaArgs) ? minecraftProfile.javaArgs : LauncherPreferences.PREF_CUSTOM_JAVA_ARGS);
|
||||
if (Tools.LOCAL_RENDERER.equals("vulkan_zink")) {
|
||||
checkVulkanZinkIsSupported();
|
||||
}
|
||||
JREUtils.redirectAndPrintJRELog();
|
||||
LauncherProfiles.update();
|
||||
LauncherProfiles.load();
|
||||
int requiredJavaVersion = 8;
|
||||
if(version.javaVersion != null) requiredJavaVersion = version.javaVersion.majorVersion;
|
||||
Tools.launchMinecraft(this, minecraftAccount, minecraftProfile, versionId, requiredJavaVersion);
|
||||
}
|
||||
|
||||
private void printLauncherInfo(String gameVersion) {
|
||||
private void printLauncherInfo(String gameVersion, String javaArguments) {
|
||||
Logger.appendToLog("Info: Launcher version: " + BuildConfig.VERSION_NAME);
|
||||
Logger.appendToLog("Info: Architecture: " + Architecture.archAsString(Tools.DEVICE_ARCHITECTURE));
|
||||
Logger.appendToLog("Info: Device model: " + Build.MANUFACTURER + " " +Build.MODEL);
|
||||
Logger.appendToLog("Info: API version: " + Build.VERSION.SDK_INT);
|
||||
Logger.appendToLog("Info: Selected Minecraft version: " + gameVersion);
|
||||
Logger.appendToLog("Info: Custom Java arguments: \"" + LauncherPreferences.PREF_CUSTOM_JAVA_ARGS + "\"");
|
||||
Logger.appendToLog("Info: Custom Java arguments: \"" + javaArguments + "\"");
|
||||
}
|
||||
|
||||
private void checkVulkanZinkIsSupported() {
|
||||
@@ -430,12 +430,12 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe
|
||||
sb.setMax(275);
|
||||
tmpMouseSpeed = (int) ((LauncherPreferences.PREF_MOUSESPEED*100));
|
||||
sb.setProgress(tmpMouseSpeed-25);
|
||||
tv.setText(getString(R.string.percent_format, tmpGyroSensitivity));
|
||||
tv.setText(getString(R.string.percent_format, tmpMouseSpeed));
|
||||
sb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
|
||||
tmpMouseSpeed = i+25;
|
||||
tv.setText(getString(R.string.percent_format, tmpGyroSensitivity));
|
||||
tv.setText(getString(R.string.percent_format, tmpMouseSpeed));
|
||||
}
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@@ -446,9 +446,11 @@ public class MinecraftGLSurface extends View implements GrabListener {
|
||||
CallbackBridge.mouseX += (e.getX()* mScaleFactor);
|
||||
CallbackBridge.mouseY += (e.getY()* mScaleFactor);
|
||||
|
||||
// Position is updated by many events, hence it is send regardless of the event value
|
||||
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
|
||||
|
||||
switch (e.getActionMasked()) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
|
||||
return true;
|
||||
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||
return sendMouseButtonUnconverted(e.getActionButton(), true);
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import net.kdt.pojavlaunch.contextexecutor.ContextExecutor;
|
||||
import net.kdt.pojavlaunch.tasks.AsyncAssetManager;
|
||||
import net.kdt.pojavlaunch.utils.*;
|
||||
|
||||
@@ -27,6 +28,7 @@ public class PojavApplication extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
ContextExecutor.setApplication(this);
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, th) -> {
|
||||
boolean storagePermAllowed = (Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT >= 29 ||
|
||||
ActivityCompat.checkSelfPermission(PojavApplication.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) && Tools.checkStorageRoot(PojavApplication.this);
|
||||
@@ -78,8 +80,14 @@ public class PojavApplication extends Application {
|
||||
startActivity(ferrorIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
ContextExecutor.clearApplication();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(LocaleUtils.setLocale(base));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package net.kdt.pojavlaunch;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask;
|
||||
import net.kdt.pojavlaunch.value.NotificationConstants;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class ShowErrorActivity extends Activity {
|
||||
|
||||
private static final String ERROR_ACTIVITY_REMOTE_TASK = "remoteTask";
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent intent = getIntent();
|
||||
if(intent == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
RemoteErrorTask remoteErrorTask = (RemoteErrorTask) intent.getSerializableExtra(ERROR_ACTIVITY_REMOTE_TASK);
|
||||
if(remoteErrorTask == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
remoteErrorTask.executeWithActivity(this);
|
||||
}
|
||||
|
||||
|
||||
public static class RemoteErrorTask implements ContextExecutorTask, Serializable {
|
||||
private final Throwable mThrowable;
|
||||
private final String mRolledMsg;
|
||||
|
||||
public RemoteErrorTask(Throwable mThrowable, String mRolledMsg) {
|
||||
this.mThrowable = mThrowable;
|
||||
this.mRolledMsg = mRolledMsg;
|
||||
}
|
||||
@Override
|
||||
public void executeWithActivity(Activity activity) {
|
||||
Tools.showError(activity, mRolledMsg, mThrowable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeWithApplication(Context context) {
|
||||
sendNotification(context, this);
|
||||
}
|
||||
}
|
||||
private static void sendNotification(Context context, RemoteErrorTask remoteErrorTask) {
|
||||
|
||||
Intent showErrorIntent = new Intent(context, ShowErrorActivity.class);
|
||||
showErrorIntent.putExtra(ERROR_ACTIVITY_REMOTE_TASK, remoteErrorTask);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, NotificationConstants.PENDINGINTENT_CODE_SHOW_ERROR, showErrorIntent,
|
||||
Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, context.getString(R.string.notif_channel_id))
|
||||
.setContentTitle(context.getString(R.string.notif_error_occured))
|
||||
.setContentText(context.getString(R.string.notif_error_occured_desc))
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.setContentIntent(pendingIntent);
|
||||
notificationManager.notify(NotificationConstants.NOTIFICATION_ID_SHOW_ERROR, notificationBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -45,6 +46,7 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import net.kdt.pojavlaunch.contextexecutor.ContextExecutor;
|
||||
import net.kdt.pojavlaunch.multirt.MultiRTUtils;
|
||||
import net.kdt.pojavlaunch.multirt.Runtime;
|
||||
import net.kdt.pojavlaunch.plugins.FFmpegPlugin;
|
||||
@@ -55,6 +57,7 @@ import net.kdt.pojavlaunch.utils.JSONUtils;
|
||||
import net.kdt.pojavlaunch.utils.OldVersionsUtils;
|
||||
import net.kdt.pojavlaunch.value.DependentLibrary;
|
||||
import net.kdt.pojavlaunch.value.MinecraftAccount;
|
||||
import net.kdt.pojavlaunch.value.MinecraftLibraryArtifact;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
|
||||
|
||||
@@ -79,6 +82,7 @@ import java.util.Map;
|
||||
|
||||
@SuppressWarnings("IOStreamConstructor")
|
||||
public final class Tools {
|
||||
public static final float BYTE_TO_MB = 1024 * 1024;
|
||||
public static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||
public static String APP_NAME = "null";
|
||||
|
||||
@@ -173,7 +177,7 @@ public final class Tools {
|
||||
}
|
||||
Runtime runtime = MultiRTUtils.forceReread(Tools.pickRuntime(minecraftProfile, versionJavaRequirement));
|
||||
JMinecraftVersionList.Version versionInfo = Tools.getVersionInfo(versionId);
|
||||
LauncherProfiles.update();
|
||||
LauncherProfiles.load();
|
||||
File gamedir = Tools.getGameDirPath(minecraftProfile);
|
||||
|
||||
|
||||
@@ -592,6 +596,23 @@ public final class Tools {
|
||||
}
|
||||
}
|
||||
|
||||
public static void showErrorRemote(Throwable e) {
|
||||
showErrorRemote(null, e);
|
||||
}
|
||||
public static void showErrorRemote(Context context, int rolledMessage, Throwable e) {
|
||||
showErrorRemote(context.getString(rolledMessage), e);
|
||||
}
|
||||
public static void showErrorRemote(String rolledMessage, Throwable e) {
|
||||
// I WILL embrace layer violations because Android's concept of layers is STUPID
|
||||
// We live in the same process anyway, why make it any more harder with this needless
|
||||
// abstraction?
|
||||
|
||||
// Add your Context-related rage here
|
||||
ContextExecutor.execute(new ShowErrorActivity.RemoteErrorTask(e, rolledMessage));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void dialogOnUiThread(final Activity activity, final CharSequence title, final CharSequence message) {
|
||||
activity.runOnUiThread(()->dialog(activity, title, message));
|
||||
}
|
||||
@@ -618,6 +639,53 @@ public final class Tools {
|
||||
}
|
||||
return true; // allow if none match
|
||||
}
|
||||
|
||||
private static void preProcessLibraries(DependentLibrary[] libraries) {
|
||||
for (int i = 0; i < libraries.length; i++) {
|
||||
DependentLibrary libItem = libraries[i];
|
||||
String[] version = libItem.name.split(":")[2].split("\\.");
|
||||
if (libItem.name.startsWith("net.java.dev.jna:jna:")) {
|
||||
// Special handling for LabyMod 1.8.9, Forge 1.12.2(?) and oshi
|
||||
// we have libjnidispatch 5.13.0 in jniLibs directory
|
||||
if (Integer.parseInt(version[0]) >= 5 && Integer.parseInt(version[1]) >= 13) continue;
|
||||
Log.d(APP_NAME, "Library " + libItem.name + " has been changed to version 5.13.0");
|
||||
createLibraryInfo(libItem);
|
||||
libItem.name = "net.java.dev.jna:jna:5.13.0";
|
||||
libItem.downloads.artifact.path = "net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar";
|
||||
libItem.downloads.artifact.sha1 = "1200e7ebeedbe0d10062093f32925a912020e747";
|
||||
libItem.downloads.artifact.url = "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar";
|
||||
} else if (libItem.name.startsWith("com.github.oshi:oshi-core:")) {
|
||||
//if (Integer.parseInt(version[0]) >= 6 && Integer.parseInt(version[1]) >= 3) return;
|
||||
// FIXME: ensure compatibility
|
||||
|
||||
if (Integer.parseInt(version[0]) != 6 || Integer.parseInt(version[1]) != 2) continue;
|
||||
Log.d(APP_NAME, "Library " + libItem.name + " has been changed to version 6.3.0");
|
||||
createLibraryInfo(libItem);
|
||||
libItem.name = "com.github.oshi:oshi-core:6.3.0";
|
||||
libItem.downloads.artifact.path = "com/github/oshi/oshi-core/6.3.0/oshi-core-6.3.0.jar";
|
||||
libItem.downloads.artifact.sha1 = "9e98cf55be371cafdb9c70c35d04ec2a8c2b42ac";
|
||||
libItem.downloads.artifact.url = "https://repo1.maven.org/maven2/com/github/oshi/oshi-core/6.3.0/oshi-core-6.3.0.jar";
|
||||
} else if (libItem.name.startsWith("org.ow2.asm:asm-all:")) {
|
||||
// Early versions of the ASM library get repalced with 5.0.4 because Pojav's LWJGL is compiled for
|
||||
// Java 8, which is not supported by old ASM versions. Mod loaders like Forge, which depend on this
|
||||
// library, often include lwjgl in their class transformations, which causes errors with old ASM versions.
|
||||
if(Integer.parseInt(version[0]) >= 5) continue;
|
||||
Log.d(APP_NAME, "Library " + libItem.name + " has been changed to version 5.0.4");
|
||||
createLibraryInfo(libItem);
|
||||
libItem.name = "org.ow2.asm:asm-all:5.0.4";
|
||||
libItem.url = null;
|
||||
libItem.downloads.artifact.path = "org/ow2/asm/asm-all/5.0.4/asm-all-5.0.4.jar";
|
||||
libItem.downloads.artifact.sha1 = "e6244859997b3d4237a552669279780876228909";
|
||||
libItem.downloads.artifact.url = "https://repo1.maven.org/maven2/org/ow2/asm/asm-all/5.0.4/asm-all-5.0.4.jar";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createLibraryInfo(DependentLibrary library) {
|
||||
if(library.downloads == null || library.downloads.artifact == null)
|
||||
library.downloads = new DependentLibrary.LibraryDownloads(new MinecraftLibraryArtifact());
|
||||
}
|
||||
|
||||
public static String[] generateLibClasspath(JMinecraftVersionList.Version info) {
|
||||
List<String> libDir = new ArrayList<>();
|
||||
for (DependentLibrary libItem: info.libraries) {
|
||||
@@ -636,7 +704,7 @@ public final class Tools {
|
||||
try {
|
||||
JMinecraftVersionList.Version customVer = Tools.GLOBAL_GSON.fromJson(read(DIR_HOME_VERSION + "/" + versionName + "/" + versionName + ".json"), JMinecraftVersionList.Version.class);
|
||||
if (skipInheriting || customVer.inheritsFrom == null || customVer.inheritsFrom.equals(customVer.id)) {
|
||||
return customVer;
|
||||
preProcessLibraries(customVer.libraries);
|
||||
} else {
|
||||
JMinecraftVersionList.Version inheritsVer;
|
||||
//If it won't download, just search for it
|
||||
@@ -674,6 +742,7 @@ public final class Tools {
|
||||
}
|
||||
} finally {
|
||||
inheritsVer.libraries = libList.toArray(new DependentLibrary[0]);
|
||||
preProcessLibraries(inheritsVer.libraries);
|
||||
}
|
||||
|
||||
// Inheriting Minecraft 1.13+ with append custom args
|
||||
@@ -711,8 +780,14 @@ public final class Tools {
|
||||
inheritsVer.arguments.game = totalArgList.toArray(new Object[0]);
|
||||
}
|
||||
|
||||
return inheritsVer;
|
||||
customVer = inheritsVer;
|
||||
}
|
||||
|
||||
// LabyMod 4 sets version instead of majorVersion
|
||||
if (customVer.javaVersion != null && customVer.javaVersion.majorVersion == 0) {
|
||||
customVer.javaVersion.majorVersion = customVer.javaVersion.version;
|
||||
}
|
||||
return customVer;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -810,7 +885,7 @@ public final class Tools {
|
||||
|
||||
public static int getDisplayFriendlyRes(int displaySideRes, float scaling){
|
||||
displaySideRes *= scaling;
|
||||
if(displaySideRes % 2 != 0) displaySideRes ++;
|
||||
if(displaySideRes % 2 != 0) displaySideRes --;
|
||||
return displaySideRes;
|
||||
}
|
||||
|
||||
@@ -993,4 +1068,12 @@ public final class Tools {
|
||||
Intent sendIntent = Intent.createChooser(shareIntent, "latestlog.txt");
|
||||
context.startActivity(sendIntent);
|
||||
}
|
||||
|
||||
/** Mesure the textview height, given its current parameters */
|
||||
public static int mesureTextviewHeight(TextView t) {
|
||||
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(t.getWidth(), View.MeasureSpec.AT_MOST);
|
||||
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
t.measure(widthMeasureSpec, heightMeasureSpec);
|
||||
return t.getMeasuredHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package net.kdt.pojavlaunch.contextexecutor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class ContextExecutor {
|
||||
private static WeakReference<Application> sApplication;
|
||||
private static WeakReference<Activity> sActivity;
|
||||
|
||||
|
||||
/**
|
||||
* Schedules a ContextExecutorTask to be executed. For more info on tasks, please read
|
||||
* ContextExecutorTask.java
|
||||
* @param contextExecutorTask the task to be executed
|
||||
*/
|
||||
public static void execute(ContextExecutorTask contextExecutorTask) {
|
||||
Tools.runOnUiThread(()->executeOnUiThread(contextExecutorTask));
|
||||
}
|
||||
|
||||
private static void executeOnUiThread(ContextExecutorTask contextExecutorTask) {
|
||||
Activity activity = getWeakReference(sActivity);
|
||||
if(activity != null) {
|
||||
contextExecutorTask.executeWithActivity(activity);
|
||||
return;
|
||||
}
|
||||
Application application = getWeakReference(sApplication);
|
||||
if(application != null) {
|
||||
contextExecutorTask.executeWithApplication(application);
|
||||
}else {
|
||||
throw new RuntimeException("ContextExecutor.execute() called before Application.onCreate!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Activity that this ContextExecutor will use for executing tasks
|
||||
* @param activity the activity to be used
|
||||
*/
|
||||
public static void setActivity(Activity activity) {
|
||||
sActivity = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the Activity previously set, so thet ContextExecutor won't use it to execute tasks.
|
||||
*/
|
||||
public static void clearActivity() {
|
||||
if(sActivity != null)
|
||||
sActivity.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Application that will be used to execute tasks if the Activity won't be available.
|
||||
* @param application the application to use as the fallback
|
||||
*/
|
||||
public static void setApplication(Application application) {
|
||||
sApplication = new WeakReference<>(application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the Application previously set, so that ContextExecutor will notify the user of a critical error
|
||||
* that is executing code after the application is ended by the system.
|
||||
*/
|
||||
public static void clearApplication() {
|
||||
if(sApplication != null)
|
||||
sApplication.clear();
|
||||
}
|
||||
|
||||
private static <T> T getWeakReference(WeakReference<T> weakReference) {
|
||||
if(weakReference == null) return null;
|
||||
return weakReference.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.kdt.pojavlaunch.contextexecutor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A ContextExecutorTask is a task that can dynamically change its behaviour, based on the context
|
||||
* used for its execution. This can be used to implement for ex. error/finish notifications from
|
||||
* background threads that may live with the Service after the activity that started them died.
|
||||
*/
|
||||
public interface ContextExecutorTask {
|
||||
/**
|
||||
* ContextExecutor will execute this function first if a foreground Activity that was attached to the
|
||||
* ContextExecutor is available.
|
||||
* @param activity the activity
|
||||
*/
|
||||
void executeWithActivity(Activity activity);
|
||||
|
||||
/**
|
||||
* ContextExecutor will execute this function if a foreground Activity is not available, but the app
|
||||
* is still running.
|
||||
* @param context the application context
|
||||
*/
|
||||
void executeWithApplication(Context context);
|
||||
}
|
||||
@@ -32,21 +32,19 @@ public class ControlData {
|
||||
public static final int SPECIALBTN_SCROLLUP = -7;
|
||||
public static final int SPECIALBTN_SCROLLDOWN = -8;
|
||||
public static final int SPECIALBTN_MENU = -9;
|
||||
|
||||
|
||||
private static ControlData[] SPECIAL_BUTTONS;
|
||||
private static List<String> SPECIAL_BUTTON_NAME_ARRAY;
|
||||
|
||||
// Internal usage only
|
||||
public boolean isHideable;
|
||||
|
||||
private static WeakReference<ExpressionBuilder> builder = new WeakReference<>(null);
|
||||
private static WeakReference<ArrayMap<String , String>> conversionMap = new WeakReference<>(null);
|
||||
private static WeakReference<ArrayMap<String, String>> conversionMap = new WeakReference<>(null);
|
||||
|
||||
static {
|
||||
buildExpressionBuilder();
|
||||
buildConversionMap();
|
||||
}
|
||||
|
||||
|
||||
// Internal usage only
|
||||
public boolean isHideable;
|
||||
/**
|
||||
* Both fields below are dynamic position data, auto updates
|
||||
* X and Y position, unlike the original one which uses fixed
|
||||
@@ -56,60 +54,29 @@ public class ControlData {
|
||||
*/
|
||||
public String dynamicX, dynamicY;
|
||||
public boolean isDynamicBtn, isToggle, passThruEnabled;
|
||||
|
||||
public static ControlData[] getSpecialButtons(){
|
||||
if (SPECIAL_BUTTONS == null) {
|
||||
SPECIAL_BUTTONS = new ControlData[]{
|
||||
new ControlData("Keyboard", new int[]{SPECIALBTN_KEYBOARD}, "${margin} * 3 + ${width} * 2", "${margin}", false),
|
||||
new ControlData("GUI", new int[]{SPECIALBTN_TOGGLECTRL}, "${margin}", "${bottom} - ${margin}"),
|
||||
new ControlData("PRI", new int[]{SPECIALBTN_MOUSEPRI}, "${margin}", "${screen_height} - ${margin} * 3 - ${height} * 3"),
|
||||
new ControlData("SEC", new int[]{SPECIALBTN_MOUSESEC}, "${margin} * 3 + ${width} * 2", "${screen_height} - ${margin} * 3 - ${height} * 3"),
|
||||
new ControlData("Mouse", new int[]{SPECIALBTN_VIRTUALMOUSE}, "${right}", "${margin}", false),
|
||||
|
||||
new ControlData("MID", new int[]{SPECIALBTN_MOUSEMID}, "${margin}", "${margin}"),
|
||||
new ControlData("SCROLLUP", new int[]{SPECIALBTN_SCROLLUP}, "${margin}", "${margin}"),
|
||||
new ControlData("SCROLLDOWN", new int[]{SPECIALBTN_SCROLLDOWN}, "${margin}", "${margin}"),
|
||||
new ControlData("MENU", new int[]{SPECIALBTN_MENU}, "${margin}", "${margin}")
|
||||
};
|
||||
}
|
||||
|
||||
return SPECIAL_BUTTONS;
|
||||
}
|
||||
|
||||
public static List<String> buildSpecialButtonArray() {
|
||||
if (SPECIAL_BUTTON_NAME_ARRAY == null) {
|
||||
List<String> nameList = new ArrayList<>();
|
||||
for (ControlData btn : getSpecialButtons()) {
|
||||
nameList.add("SPECIAL_" + btn.name);
|
||||
}
|
||||
SPECIAL_BUTTON_NAME_ARRAY = nameList;
|
||||
Collections.reverse(SPECIAL_BUTTON_NAME_ARRAY);
|
||||
}
|
||||
|
||||
return SPECIAL_BUTTON_NAME_ARRAY;
|
||||
}
|
||||
|
||||
public String name;
|
||||
private float width; //Dp instead of Px now
|
||||
private float height; //Dp instead of Px now
|
||||
public int[] keycodes; //Should store up to 4 keys
|
||||
public float opacity; //Alpha value from 0 to 1;
|
||||
public int bgColor;
|
||||
public int strokeColor;
|
||||
public int strokeWidth; //0-100%
|
||||
public float strokeWidth; // Dp instead of % now
|
||||
public float cornerRadius; //0-100%
|
||||
public boolean isSwipeable;
|
||||
public boolean displayInGame;
|
||||
public boolean displayInMenu;
|
||||
private float width; //Dp instead of Px now
|
||||
private float height; //Dp instead of Px now
|
||||
|
||||
public ControlData() {
|
||||
this("button");
|
||||
}
|
||||
|
||||
public ControlData(String name){
|
||||
this(name, new int[] {});
|
||||
public ControlData(String name) {
|
||||
this(name, new int[]{});
|
||||
}
|
||||
|
||||
public ControlData(String name, int[] keycodes) {
|
||||
this(name, keycodes, Tools.currentDisplayMetrics.widthPixels/2f, Tools.currentDisplayMetrics.heightPixels/2f);
|
||||
this(name, keycodes, Tools.currentDisplayMetrics.widthPixels / 2f, Tools.currentDisplayMetrics.heightPixels / 2f);
|
||||
}
|
||||
|
||||
public ControlData(String name, int[] keycodes, float x, float y) {
|
||||
@@ -141,11 +108,11 @@ public class ControlData {
|
||||
this(name, keycodes, dynamicX, dynamicY, isSquare ? 50 : 80, isSquare ? 50 : 30, false);
|
||||
}
|
||||
|
||||
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle){
|
||||
this(name, keycodes, dynamicX, dynamicY, width, height, isToggle, 1,0x4D000000, 0xFFFFFFFF,0,0);
|
||||
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle) {
|
||||
this(name, keycodes, dynamicX, dynamicY, width, height, isToggle, 1, 0x4D000000, 0xFFFFFFFF, 0, 0, true, true);
|
||||
}
|
||||
|
||||
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle, float opacity, int bgColor, int strokeColor, int strokeWidth, float cornerRadius) {
|
||||
public ControlData(String name, int[] keycodes, String dynamicX, String dynamicY, float width, float height, boolean isToggle, float opacity, int bgColor, int strokeColor, float strokeWidth, float cornerRadius, boolean displayInGame, boolean displayInMenu) {
|
||||
this.name = name;
|
||||
this.keycodes = inflateKeycodeArray(keycodes);
|
||||
this.dynamicX = dynamicX;
|
||||
@@ -159,10 +126,12 @@ public class ControlData {
|
||||
this.strokeColor = strokeColor;
|
||||
this.strokeWidth = strokeWidth;
|
||||
this.cornerRadius = cornerRadius;
|
||||
this.displayInGame = displayInGame;
|
||||
this.displayInMenu = displayInMenu;
|
||||
}
|
||||
|
||||
//Deep copy constructor
|
||||
public ControlData(ControlData controlData){
|
||||
public ControlData(ControlData controlData) {
|
||||
this(
|
||||
controlData.name,
|
||||
controlData.keycodes,
|
||||
@@ -175,17 +144,42 @@ public class ControlData {
|
||||
controlData.bgColor,
|
||||
controlData.strokeColor,
|
||||
controlData.strokeWidth,
|
||||
controlData.cornerRadius
|
||||
controlData.cornerRadius,
|
||||
controlData.displayInGame,
|
||||
controlData.displayInMenu
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public float insertDynamicPos(String dynamicPos) {
|
||||
// Insert value to ${variable}
|
||||
String insertedPos = JSONUtils.insertSingleJSONValue(dynamicPos, fillConversionMap());
|
||||
|
||||
// Calculate, because the dynamic position contains some math equations
|
||||
return calculate(insertedPos);
|
||||
public static ControlData[] getSpecialButtons() {
|
||||
if (SPECIAL_BUTTONS == null) {
|
||||
SPECIAL_BUTTONS = new ControlData[]{
|
||||
new ControlData("Keyboard", new int[]{SPECIALBTN_KEYBOARD}, "${margin} * 3 + ${width} * 2", "${margin}", false),
|
||||
new ControlData("GUI", new int[]{SPECIALBTN_TOGGLECTRL}, "${margin}", "${bottom} - ${margin}"),
|
||||
new ControlData("PRI", new int[]{SPECIALBTN_MOUSEPRI}, "${margin}", "${screen_height} - ${margin} * 3 - ${height} * 3"),
|
||||
new ControlData("SEC", new int[]{SPECIALBTN_MOUSESEC}, "${margin} * 3 + ${width} * 2", "${screen_height} - ${margin} * 3 - ${height} * 3"),
|
||||
new ControlData("Mouse", new int[]{SPECIALBTN_VIRTUALMOUSE}, "${right}", "${margin}", false),
|
||||
|
||||
new ControlData("MID", new int[]{SPECIALBTN_MOUSEMID}, "${margin}", "${margin}"),
|
||||
new ControlData("SCROLLUP", new int[]{SPECIALBTN_SCROLLUP}, "${margin}", "${margin}"),
|
||||
new ControlData("SCROLLDOWN", new int[]{SPECIALBTN_SCROLLDOWN}, "${margin}", "${margin}"),
|
||||
new ControlData("MENU", new int[]{SPECIALBTN_MENU}, "${margin}", "${margin}")
|
||||
};
|
||||
}
|
||||
|
||||
return SPECIAL_BUTTONS;
|
||||
}
|
||||
|
||||
public static List<String> buildSpecialButtonArray() {
|
||||
if (SPECIAL_BUTTON_NAME_ARRAY == null) {
|
||||
List<String> nameList = new ArrayList<>();
|
||||
for (ControlData btn : getSpecialButtons()) {
|
||||
nameList.add("SPECIAL_" + btn.name);
|
||||
}
|
||||
SPECIAL_BUTTON_NAME_ARRAY = nameList;
|
||||
Collections.reverse(SPECIAL_BUTTON_NAME_ARRAY);
|
||||
}
|
||||
|
||||
return SPECIAL_BUTTON_NAME_ARRAY;
|
||||
}
|
||||
|
||||
private static float calculate(String math) {
|
||||
@@ -193,44 +187,16 @@ public class ControlData {
|
||||
return (float) builder.get().build().evaluate();
|
||||
}
|
||||
|
||||
private static int[] inflateKeycodeArray(int[] keycodes){
|
||||
private static int[] inflateKeycodeArray(int[] keycodes) {
|
||||
int[] inflatedArray = new int[]{GLFW_KEY_UNKNOWN, GLFW_KEY_UNKNOWN, GLFW_KEY_UNKNOWN, GLFW_KEY_UNKNOWN};
|
||||
System.arraycopy(keycodes, 0, inflatedArray, 0, keycodes.length);
|
||||
return inflatedArray;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean containsKeycode(int keycodeToCheck){
|
||||
for(int keycode : keycodes)
|
||||
if(keycodeToCheck == keycode)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Getters || setters (with conversion for ease of use)
|
||||
public float getWidth() {
|
||||
return Tools.dpToPx(width);
|
||||
}
|
||||
|
||||
public float getHeight(){
|
||||
return Tools.dpToPx(height);
|
||||
}
|
||||
|
||||
|
||||
public void setWidth(float widthInPx){
|
||||
width = Tools.pxToDp(widthInPx);
|
||||
}
|
||||
|
||||
public void setHeight(float heightInPx){
|
||||
height = Tools.pxToDp(heightInPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder, keep a weak reference to it to use it with all views on first inflation
|
||||
*/
|
||||
private static void buildExpressionBuilder(){
|
||||
private static void buildExpressionBuilder() {
|
||||
ExpressionBuilder expressionBuilder = new ExpressionBuilder("1 + 1")
|
||||
.function(new Function("dp", 1) {
|
||||
@Override
|
||||
@@ -249,10 +215,11 @@ public class ControlData {
|
||||
|
||||
/**
|
||||
* wrapper for the WeakReference to the expressionField.
|
||||
*
|
||||
* @param stringExpression the expression to set.
|
||||
*/
|
||||
private static void setExpression(String stringExpression){
|
||||
if(builder.get() == null) buildExpressionBuilder();
|
||||
private static void setExpression(String stringExpression) {
|
||||
if (builder.get() == null) buildExpressionBuilder();
|
||||
builder.get().expression(stringExpression);
|
||||
}
|
||||
|
||||
@@ -269,7 +236,7 @@ public class ControlData {
|
||||
keyValueMap.put("bottom", "DUMMY_BOTTOM");
|
||||
keyValueMap.put("width", "DUMMY_WIDTH");
|
||||
keyValueMap.put("height", "DUMMY_HEIGHT");
|
||||
keyValueMap.put("screen_width", "DUMMY_DATA" );
|
||||
keyValueMap.put("screen_width", "DUMMY_DATA");
|
||||
keyValueMap.put("screen_height", "DUMMY_DATA");
|
||||
keyValueMap.put("margin", Integer.toString((int) Tools.dpToPx(2)));
|
||||
keyValueMap.put("preferred_scale", "DUMMY_DATA");
|
||||
@@ -277,14 +244,49 @@ public class ControlData {
|
||||
conversionMap = new WeakReference<>(keyValueMap);
|
||||
}
|
||||
|
||||
public float insertDynamicPos(String dynamicPos) {
|
||||
// Insert value to ${variable}
|
||||
String insertedPos = JSONUtils.insertSingleJSONValue(dynamicPos, fillConversionMap());
|
||||
|
||||
// Calculate, because the dynamic position contains some math equations
|
||||
return calculate(insertedPos);
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean containsKeycode(int keycodeToCheck) {
|
||||
for (int keycode : keycodes)
|
||||
if (keycodeToCheck == keycode)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Getters || setters (with conversion for ease of use)
|
||||
public float getWidth() {
|
||||
return Tools.dpToPx(width);
|
||||
}
|
||||
|
||||
public void setWidth(float widthInPx) {
|
||||
width = Tools.pxToDp(widthInPx);
|
||||
}
|
||||
|
||||
public float getHeight() {
|
||||
return Tools.dpToPx(height);
|
||||
}
|
||||
|
||||
public void setHeight(float heightInPx) {
|
||||
height = Tools.pxToDp(heightInPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the conversionMap with controlData dependent values.
|
||||
* The returned valueMap should NOT be kept in memory.
|
||||
*
|
||||
* @return the valueMap to use.
|
||||
*/
|
||||
private Map<String, String> fillConversionMap(){
|
||||
private Map<String, String> fillConversionMap() {
|
||||
ArrayMap<String, String> valueMap = conversionMap.get();
|
||||
if (valueMap == null){
|
||||
if (valueMap == null) {
|
||||
buildConversionMap();
|
||||
valueMap = conversionMap.get();
|
||||
}
|
||||
@@ -293,8 +295,8 @@ public class ControlData {
|
||||
valueMap.put("bottom", Float.toString(CallbackBridge.physicalHeight - getHeight()));
|
||||
valueMap.put("width", Float.toString(getWidth()));
|
||||
valueMap.put("height", Float.toString(getHeight()));
|
||||
valueMap.put("screen_width",Integer.toString(CallbackBridge.physicalWidth));
|
||||
valueMap.put("screen_height",Integer.toString(CallbackBridge.physicalHeight));
|
||||
valueMap.put("screen_width", Integer.toString(CallbackBridge.physicalWidth));
|
||||
valueMap.put("screen_height", Integer.toString(CallbackBridge.physicalHeight));
|
||||
valueMap.put("preferred_scale", Float.toString(LauncherPreferences.PREF_BUTTONSIZE));
|
||||
|
||||
return valueMap;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.kdt.pojavlaunch.customcontrols;
|
||||
|
||||
public class ControlJoystickData extends ControlData {
|
||||
|
||||
/* Whether the joystick can stay forward */
|
||||
public boolean forwardLock = false;
|
||||
|
||||
public ControlJoystickData(){
|
||||
super();
|
||||
}
|
||||
|
||||
public ControlJoystickData(ControlData properties) {
|
||||
super(properties);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package net.kdt.pojavlaunch.customcontrols;
|
||||
|
||||
import static android.content.Context.INPUT_METHOD_SERVICE;
|
||||
import static net.kdt.pojavlaunch.MainActivity.mControlLayout;
|
||||
import static net.kdt.pojavlaunch.Tools.currentDisplayMetrics;
|
||||
|
||||
import static org.lwjgl.glfw.CallbackBridge.isGrabbing;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
@@ -27,6 +30,7 @@ import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.customcontrols.buttons.ControlButton;
|
||||
import net.kdt.pojavlaunch.customcontrols.buttons.ControlDrawer;
|
||||
import net.kdt.pojavlaunch.customcontrols.buttons.ControlInterface;
|
||||
import net.kdt.pojavlaunch.customcontrols.buttons.ControlJoystick;
|
||||
import net.kdt.pojavlaunch.customcontrols.buttons.ControlSubButton;
|
||||
import net.kdt.pojavlaunch.customcontrols.handleview.ActionRow;
|
||||
import net.kdt.pojavlaunch.customcontrols.handleview.ControlHandleView;
|
||||
@@ -107,6 +111,12 @@ public class ControlLayout extends FrameLayout {
|
||||
if(mModifiable) drawer.areButtonsVisible = true;
|
||||
}
|
||||
|
||||
// Joystick(s)
|
||||
for(ControlJoystickData joystick : mLayout.mJoystickDataList){
|
||||
addJoystickView(joystick);
|
||||
}
|
||||
|
||||
|
||||
mLayout.scaledAt = LauncherPreferences.PREF_BUTTONSIZE;
|
||||
|
||||
setModified(false);
|
||||
@@ -177,15 +187,26 @@ public class ControlLayout extends FrameLayout {
|
||||
view.setFocusable(false);
|
||||
view.setFocusableInTouchMode(false);
|
||||
}else{
|
||||
view.setVisible(drawer.areButtonsVisible);
|
||||
view.setVisible(true);
|
||||
}
|
||||
|
||||
drawer.addButton(view);
|
||||
addView(view);
|
||||
drawer.addButton(view);
|
||||
|
||||
|
||||
setModified(true);
|
||||
}
|
||||
|
||||
// JOYSTICK BUTTON
|
||||
public void addJoystickButton(ControlJoystickData data){
|
||||
mLayout.mJoystickDataList.add(data);
|
||||
addJoystickView(data);
|
||||
}
|
||||
|
||||
private void addJoystickView(ControlJoystickData data){
|
||||
addView(new ControlJoystick(this, data));
|
||||
}
|
||||
|
||||
|
||||
private void removeAllButtons() {
|
||||
for(ControlInterface button : getButtonChildren()){
|
||||
@@ -220,7 +241,7 @@ public class ControlLayout extends FrameLayout {
|
||||
|
||||
mControlVisible = isVisible;
|
||||
for(ControlInterface button : getButtonChildren()){
|
||||
button.setVisible(isVisible);
|
||||
button.setVisible(((button.getProperties().displayInGame && isGrabbing()) || (button.getProperties().displayInMenu && !isGrabbing())) && isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +250,12 @@ public class ControlLayout extends FrameLayout {
|
||||
removeEditWindow();
|
||||
}
|
||||
mModifiable = isModifiable;
|
||||
if(isModifiable){
|
||||
// In edit mode, all controls have to be shown
|
||||
for(ControlInterface button : getButtonChildren()){
|
||||
button.setVisible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getModifiable(){
|
||||
@@ -556,4 +583,8 @@ public class ControlLayout extends FrameLayout {
|
||||
builder.setNegativeButton(R.string.global_no, (d,w)->{});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public boolean areControlVisible(){
|
||||
return mControlVisible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,18 @@ public class CustomControls {
|
||||
public float scaledAt;
|
||||
public List<ControlData> mControlDataList;
|
||||
public List<ControlDrawerData> mDrawerDataList;
|
||||
public List<ControlJoystickData> mJoystickDataList;
|
||||
public CustomControls() {
|
||||
this(new ArrayList<>(), new ArrayList<>());
|
||||
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public CustomControls(List<ControlData> mControlDataList, List<ControlDrawerData> mDrawerDataList) {
|
||||
public CustomControls(List<ControlData> mControlDataList, List<ControlDrawerData> mDrawerDataList, List<ControlJoystickData> mJoystickDataList) {
|
||||
this.mControlDataList = mControlDataList;
|
||||
this.mDrawerDataList = mDrawerDataList;
|
||||
this.scaledAt = 100f;
|
||||
this.mJoystickDataList = mJoystickDataList;
|
||||
this.scaledAt = 100f;
|
||||
}
|
||||
|
||||
// Generate default control
|
||||
@@ -51,14 +53,14 @@ public class CustomControls {
|
||||
this.mControlDataList.add(shiftData);
|
||||
this.mControlDataList.add(new ControlData(ctx, R.string.control_jump, new int[]{LwjglGlfwKeycode.GLFW_KEY_SPACE}, "${right} - ${margin} * 2 - ${width}", "${bottom} - ${margin} * 2 - ${height}", true));
|
||||
|
||||
//The default controls are conform to the V2
|
||||
version = 4;
|
||||
//The default controls are conform to the V3
|
||||
version = 6;
|
||||
}
|
||||
|
||||
|
||||
public void save(String path) throws IOException {
|
||||
//Current version is the V2.5 so the version as to be marked as 4 !
|
||||
version = 4;
|
||||
//Current version is the V3.0 so the version as to be marked as 6 !
|
||||
version = 6;
|
||||
|
||||
Tools.write(path, Tools.GLOBAL_GSON.toJson(this));
|
||||
}
|
||||
|
||||
@@ -20,105 +20,159 @@ public class LayoutConverter {
|
||||
try {
|
||||
JSONObject layoutJobj = new JSONObject(jsonLayoutData);
|
||||
|
||||
if(!layoutJobj.has("version")) { //v1 layout
|
||||
if (!layoutJobj.has("version")) { //v1 layout
|
||||
CustomControls layout = LayoutConverter.convertV1Layout(layoutJobj);
|
||||
layout.save(jsonPath);
|
||||
return layout;
|
||||
}else if (layoutJobj.getInt("version") == 2) {
|
||||
} else if (layoutJobj.getInt("version") == 2) {
|
||||
CustomControls layout = LayoutConverter.convertV2Layout(layoutJobj);
|
||||
layout.save(jsonPath);
|
||||
return layout;
|
||||
}else if (layoutJobj.getInt("version") == 3 || layoutJobj.getInt("version") == 4) {
|
||||
}else if (layoutJobj.getInt("version") >= 3 && layoutJobj.getInt("version") <= 5) {
|
||||
return LayoutConverter.convertV3_4Layout(layoutJobj);
|
||||
} else if (layoutJobj.getInt("version") == 6) {
|
||||
return Tools.GLOBAL_GSON.fromJson(jsonLayoutData, CustomControls.class);
|
||||
}else{
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}catch (JSONException e) {
|
||||
throw new JsonSyntaxException("Failed to load",e);
|
||||
} catch (JSONException e) {
|
||||
throw new JsonSyntaxException("Failed to load", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalize the layout to v6 from v3/4: The stroke width is no longer dependant on the button size
|
||||
*/
|
||||
public static CustomControls convertV3_4Layout(JSONObject oldLayoutJson) {
|
||||
CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
|
||||
convertStrokeWidth(layout);
|
||||
layout.version = 6;
|
||||
return layout;
|
||||
}
|
||||
|
||||
|
||||
public static CustomControls convertV2Layout(JSONObject oldLayoutJson) throws JSONException {
|
||||
CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
|
||||
JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList");
|
||||
layout.mControlDataList = new ArrayList<>(layoutMainArray.length());
|
||||
for(int i = 0; i < layoutMainArray.length(); i++) {
|
||||
for (int i = 0; i < layoutMainArray.length(); i++) {
|
||||
JSONObject button = layoutMainArray.getJSONObject(i);
|
||||
ControlData n_button = Tools.GLOBAL_GSON.fromJson(button.toString(), ControlData.class);
|
||||
if(!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
|
||||
if (!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
|
||||
double buttonC = button.getDouble("x");
|
||||
double ratio = buttonC/CallbackBridge.physicalWidth;
|
||||
double ratio = buttonC / CallbackBridge.physicalWidth;
|
||||
n_button.dynamicX = ratio + " * ${screen_width}";
|
||||
}
|
||||
if(!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
|
||||
if (!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
|
||||
double buttonC = button.getDouble("y");
|
||||
double ratio = buttonC/CallbackBridge.physicalHeight;
|
||||
double ratio = buttonC / CallbackBridge.physicalHeight;
|
||||
n_button.dynamicY = ratio + " * ${screen_height}";
|
||||
}
|
||||
layout.mControlDataList.add(n_button);
|
||||
}
|
||||
JSONArray layoutDrawerArray = oldLayoutJson.getJSONArray("mDrawerDataList");
|
||||
layout.mDrawerDataList = new ArrayList<>();
|
||||
for(int i = 0; i < layoutDrawerArray.length(); i++) {
|
||||
for (int i = 0; i < layoutDrawerArray.length(); i++) {
|
||||
JSONObject button = layoutDrawerArray.getJSONObject(i);
|
||||
JSONObject buttonProperties = button.getJSONObject("properties");
|
||||
ControlDrawerData n_button = Tools.GLOBAL_GSON.fromJson(button.toString(), ControlDrawerData.class);
|
||||
if(!Tools.isValidString(n_button.properties.dynamicX) && buttonProperties.has("x")) {
|
||||
if (!Tools.isValidString(n_button.properties.dynamicX) && buttonProperties.has("x")) {
|
||||
double buttonC = buttonProperties.getDouble("x");
|
||||
double ratio = buttonC/CallbackBridge.physicalWidth;
|
||||
double ratio = buttonC / CallbackBridge.physicalWidth;
|
||||
n_button.properties.dynamicX = ratio + " * ${screen_width}";
|
||||
}
|
||||
if(!Tools.isValidString(n_button.properties.dynamicY) && buttonProperties.has("y")) {
|
||||
if (!Tools.isValidString(n_button.properties.dynamicY) && buttonProperties.has("y")) {
|
||||
double buttonC = buttonProperties.getDouble("y");
|
||||
double ratio = buttonC/CallbackBridge.physicalHeight;
|
||||
double ratio = buttonC / CallbackBridge.physicalHeight;
|
||||
n_button.properties.dynamicY = ratio + " * ${screen_height}";
|
||||
}
|
||||
layout.mDrawerDataList.add(n_button);
|
||||
}
|
||||
convertStrokeWidth(layout);
|
||||
|
||||
layout.version = 3;
|
||||
return layout;
|
||||
}
|
||||
|
||||
public static CustomControls convertV1Layout(JSONObject oldLayoutJson) throws JSONException {
|
||||
CustomControls empty = new CustomControls();
|
||||
JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList");
|
||||
for(int i = 0; i < layoutMainArray.length(); i++) {
|
||||
for (int i = 0; i < layoutMainArray.length(); i++) {
|
||||
JSONObject button = layoutMainArray.getJSONObject(i);
|
||||
ControlData n_button = new ControlData();
|
||||
int[] keycodes = new int[] {LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
|
||||
int[] keycodes = new int[]{LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
|
||||
LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
|
||||
LwjglGlfwKeycode.GLFW_KEY_UNKNOWN,
|
||||
LwjglGlfwKeycode.GLFW_KEY_UNKNOWN};
|
||||
n_button.isDynamicBtn = button.getBoolean("isDynamicBtn");
|
||||
n_button.dynamicX = button.getString("dynamicX");
|
||||
n_button.dynamicY = button.getString("dynamicY");
|
||||
if(!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
|
||||
if (!Tools.isValidString(n_button.dynamicX) && button.has("x")) {
|
||||
double buttonC = button.getDouble("x");
|
||||
double ratio = buttonC/CallbackBridge.physicalWidth;
|
||||
double ratio = buttonC / CallbackBridge.physicalWidth;
|
||||
n_button.dynamicX = ratio + " * ${screen_width}";
|
||||
}
|
||||
if(!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
|
||||
if (!Tools.isValidString(n_button.dynamicY) && button.has("y")) {
|
||||
double buttonC = button.getDouble("y");
|
||||
double ratio = buttonC/CallbackBridge.physicalHeight;
|
||||
double ratio = buttonC / CallbackBridge.physicalHeight;
|
||||
n_button.dynamicY = ratio + " * ${screen_height}";
|
||||
}
|
||||
n_button.name = button.getString("name");
|
||||
n_button.opacity = ((float)((button.getInt("transparency")-100)*-1))/100f;
|
||||
n_button.opacity = ((float) ((button.getInt("transparency") - 100) * -1)) / 100f;
|
||||
n_button.passThruEnabled = button.getBoolean("passThruEnabled");
|
||||
n_button.isToggle = button.getBoolean("isToggle");
|
||||
n_button.setHeight(button.getInt("height"));
|
||||
n_button.setWidth(button.getInt("width"));
|
||||
n_button.bgColor = 0x4d000000;
|
||||
n_button.strokeWidth = 0;
|
||||
if(button.getBoolean("isRound")) { n_button.cornerRadius = 35f; }
|
||||
if (button.getBoolean("isRound")) {
|
||||
n_button.cornerRadius = 35f;
|
||||
}
|
||||
int next_idx = 0;
|
||||
if(button.getBoolean("holdShift")) { keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT; next_idx++; }
|
||||
if(button.getBoolean("holdCtrl")) { keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL; next_idx++; }
|
||||
if(button.getBoolean("holdAlt")) { keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_ALT; next_idx++; }
|
||||
if (button.getBoolean("holdShift")) {
|
||||
keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_SHIFT;
|
||||
next_idx++;
|
||||
}
|
||||
if (button.getBoolean("holdCtrl")) {
|
||||
keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL;
|
||||
next_idx++;
|
||||
}
|
||||
if (button.getBoolean("holdAlt")) {
|
||||
keycodes[next_idx] = LwjglGlfwKeycode.GLFW_KEY_LEFT_ALT;
|
||||
next_idx++;
|
||||
}
|
||||
keycodes[next_idx] = button.getInt("keycode");
|
||||
n_button.keycodes = keycodes;
|
||||
empty.mControlDataList.add(n_button);
|
||||
}
|
||||
empty.scaledAt = (float)oldLayoutJson.getDouble("scaledAt");
|
||||
empty.scaledAt = (float) oldLayoutJson.getDouble("scaledAt");
|
||||
empty.version = 3;
|
||||
return empty;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert the layout stroke width to the V5 form
|
||||
*/
|
||||
private static void convertStrokeWidth(CustomControls layout) {
|
||||
for (ControlData data : layout.mControlDataList) {
|
||||
data.strokeWidth = Tools.pxToDp(computeStrokeWidth(data.strokeWidth, data.getWidth(), data.getHeight()));
|
||||
}
|
||||
|
||||
for (ControlDrawerData data : layout.mDrawerDataList) {
|
||||
data.properties.strokeWidth = Tools.pxToDp(computeStrokeWidth(data.properties.strokeWidth, data.properties.getWidth(), data.properties.getHeight()));
|
||||
for (ControlData subButtonData : data.buttonProperties) {
|
||||
subButtonData.strokeWidth = Tools.pxToDp(computeStrokeWidth(subButtonData.strokeWidth, data.properties.getWidth(), data.properties.getWidth()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a size percentage into a px size, used by older layout versions
|
||||
*/
|
||||
static int computeStrokeWidth(float widthInPercent, float width, float height) {
|
||||
float maxSize = Math.max(width, height);
|
||||
return (int) ((maxSize / 2) * (widthInPercent / 100));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ public class ControlButton extends TextView implements ControlInterface {
|
||||
protected ControlData mProperties;
|
||||
private final ControlLayout mControlLayout;
|
||||
|
||||
/* Cache value from the ControlData radius for drawing purposes */
|
||||
private float mComputedRadius;
|
||||
|
||||
protected boolean mIsToggled = false;
|
||||
protected boolean mIsPointerOutOfBounds = false;
|
||||
|
||||
@@ -42,6 +45,7 @@ public class ControlButton extends TextView implements ControlInterface {
|
||||
setTextColor(Color.WHITE);
|
||||
setPadding(4, 4, 4, 4);
|
||||
setTextSize(14); // Nullify the default size setting
|
||||
setOutlineProvider(null); // Disable shadow casting, removing one drawing pass
|
||||
|
||||
//setOnLongClickListener(this);
|
||||
|
||||
@@ -61,6 +65,7 @@ public class ControlButton extends TextView implements ControlInterface {
|
||||
public void setProperties(ControlData properties, boolean changePos) {
|
||||
mProperties = properties;
|
||||
ControlInterface.super.setProperties(properties, changePos);
|
||||
mComputedRadius = ControlInterface.super.computeCornerRadius(mProperties.cornerRadius);
|
||||
|
||||
if (mProperties.isToggle) {
|
||||
//For the toggle layer
|
||||
@@ -76,16 +81,11 @@ public class ControlButton extends TextView implements ControlInterface {
|
||||
setText(properties.name);
|
||||
}
|
||||
|
||||
public void setVisible(boolean isVisible){
|
||||
if(mProperties.isHideable)
|
||||
setVisibility(isVisible ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (mIsToggled || (!mProperties.isToggle && isActivated()))
|
||||
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), mProperties.cornerRadius, mProperties.cornerRadius, mRectPaint);
|
||||
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), mComputedRadius, mComputedRadius, mRectPaint);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ package net.kdt.pojavlaunch.customcontrols.buttons;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlData;
|
||||
@@ -40,18 +42,19 @@ public class ControlDrawer extends ControlButton {
|
||||
|
||||
public void addButton(ControlSubButton button){
|
||||
buttons.add(button);
|
||||
setControlButtonVisibility(button, areButtonsVisible);
|
||||
syncButtons();
|
||||
setControlButtonVisibility(button, areButtonsVisible);
|
||||
}
|
||||
|
||||
private void setControlButtonVisibility(ControlButton button, boolean isVisible){
|
||||
button.setVisible(isVisible);
|
||||
button.getControlView().setVisibility(isVisible ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
private void switchButtonVisibility(){
|
||||
areButtonsVisible = !areButtonsVisible;
|
||||
int visibility = areButtonsVisible ? VISIBLE : GONE;
|
||||
for(ControlButton button : buttons){
|
||||
button.setVisible(areButtonsVisible);
|
||||
button.getControlView().setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +91,7 @@ public class ControlDrawer extends ControlButton {
|
||||
|
||||
|
||||
private void resizeButtons(){
|
||||
if (buttons == null) return;
|
||||
if (buttons == null || drawerData.orientation == ControlDrawerData.Orientation.FREE) return;
|
||||
for(ControlSubButton subButton : buttons){
|
||||
subButton.mProperties.setWidth(mProperties.getWidth());
|
||||
subButton.mProperties.setHeight(mProperties.getHeight());
|
||||
@@ -124,8 +127,13 @@ public class ControlDrawer extends ControlButton {
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean isVisible) {
|
||||
//TODO replicate changes to his children ?
|
||||
setVisibility(isVisible ? VISIBLE : GONE);
|
||||
int visibility = isVisible ? VISIBLE : GONE;
|
||||
setVisibility(visibility);
|
||||
if(visibility == GONE || areButtonsVisible) {
|
||||
for(ControlSubButton button : buttons){
|
||||
button.getControlView().setVisibility(isVisible ? VISIBLE : (!mProperties.isHideable && getVisibility() == GONE) ? VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package net.kdt.pojavlaunch.customcontrols.buttons;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_BUTTONSIZE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.math.MathUtils;
|
||||
|
||||
import net.kdt.pojavlaunch.GrabListener;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
|
||||
@@ -24,34 +29,54 @@ import org.lwjgl.glfw.CallbackBridge;
|
||||
* Most of the injected behavior is editing behavior,
|
||||
* sending keys has to be implemented by sub classes.
|
||||
*/
|
||||
public interface ControlInterface extends View.OnLongClickListener {
|
||||
public interface ControlInterface extends View.OnLongClickListener, GrabListener {
|
||||
|
||||
View getControlView();
|
||||
|
||||
ControlData getProperties();
|
||||
|
||||
/** Remove the button presence from the CustomControl object
|
||||
default void setProperties(ControlData properties) {
|
||||
setProperties(properties, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the button presence from the CustomControl object
|
||||
* You need to use {getControlParent()} for this.
|
||||
*/
|
||||
void removeButton();
|
||||
|
||||
/** Duplicate the data of the button and add a view with the duplicated data
|
||||
/**
|
||||
* Duplicate the data of the button and add a view with the duplicated data
|
||||
* Relies on the ControlLayout for the implementation.
|
||||
*/
|
||||
void cloneButton();
|
||||
|
||||
void setVisible(boolean isVisible);
|
||||
default void setVisible(boolean isVisible) {
|
||||
if(getProperties().isHideable)
|
||||
getControlView().setVisibility(isVisible ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
void sendKeyPresses(boolean isDown);
|
||||
|
||||
/** Load the values and hide non useful forms */
|
||||
/**
|
||||
* Load the values and hide non useful forms
|
||||
*/
|
||||
void loadEditValues(EditControlPopup editControlPopup);
|
||||
|
||||
@Override
|
||||
default void onGrabState(boolean isGrabbing) {
|
||||
if (getControlLayoutParent() != null && getControlLayoutParent().getModifiable()) return; // Disable when edited
|
||||
setVisible(((getProperties().displayInGame && isGrabbing) || (getProperties().displayInMenu && !isGrabbing)) && getControlLayoutParent().areControlVisible());
|
||||
}
|
||||
|
||||
default ControlLayout getControlLayoutParent(){
|
||||
default ControlLayout getControlLayoutParent() {
|
||||
return (ControlLayout) getControlView().getParent();
|
||||
}
|
||||
|
||||
/** Apply conversion steps for when the view is created */
|
||||
default ControlData preProcessProperties(ControlData properties, ControlLayout layout){
|
||||
/**
|
||||
* Apply conversion steps for when the view is created
|
||||
*/
|
||||
default ControlData preProcessProperties(ControlData properties, ControlLayout layout) {
|
||||
//Size
|
||||
properties.setWidth(properties.getWidth() / layout.getLayoutScale() * PREF_BUTTONSIZE);
|
||||
properties.setHeight(properties.getHeight() / layout.getLayoutScale() * PREF_BUTTONSIZE);
|
||||
@@ -66,10 +91,6 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
setProperties(getProperties());
|
||||
}
|
||||
|
||||
default void setProperties(ControlData properties) {
|
||||
setProperties(properties, true);
|
||||
}
|
||||
|
||||
/* This function should be overridden to store the properties */
|
||||
@CallSuper
|
||||
default void setProperties(ControlData properties, boolean changePos) {
|
||||
@@ -80,19 +101,22 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
|
||||
// Recycle layout params
|
||||
ViewGroup.LayoutParams params = getControlView().getLayoutParams();
|
||||
if(params == null) params = new FrameLayout.LayoutParams((int) properties.getWidth(), (int) properties.getHeight());
|
||||
if (params == null)
|
||||
params = new FrameLayout.LayoutParams((int) properties.getWidth(), (int) properties.getHeight());
|
||||
params.width = (int) properties.getWidth();
|
||||
params.height = (int) properties.getHeight();
|
||||
getControlView().setLayoutParams(params);
|
||||
}
|
||||
|
||||
/** Apply the background according to properties */
|
||||
default void setBackground(){
|
||||
GradientDrawable gd = getControlView().getBackground() instanceof GradientDrawable
|
||||
/**
|
||||
* Apply the background according to properties
|
||||
*/
|
||||
default void setBackground() {
|
||||
GradientDrawable gd = getControlView().getBackground() instanceof GradientDrawable
|
||||
? (GradientDrawable) getControlView().getBackground()
|
||||
: new GradientDrawable();
|
||||
gd.setColor(getProperties().bgColor);
|
||||
gd.setStroke(computeStrokeWidth(getProperties().strokeWidth), getProperties().strokeColor);
|
||||
gd.setStroke((int) Tools.dpToPx(getProperties().strokeWidth), getProperties().strokeColor);
|
||||
gd.setCornerRadius(computeCornerRadius(getProperties().cornerRadius));
|
||||
|
||||
getControlView().setBackground(gd);
|
||||
@@ -100,50 +124,56 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
|
||||
/**
|
||||
* Apply the dynamic equation on the x axis.
|
||||
*
|
||||
* @param dynamicX The equation to compute the position from
|
||||
*/
|
||||
default void setDynamicX(String dynamicX){
|
||||
default void setDynamicX(String dynamicX) {
|
||||
getProperties().dynamicX = dynamicX;
|
||||
getControlView().setX(getProperties().insertDynamicPos(dynamicX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the dynamic equation on the y axis.
|
||||
*
|
||||
* @param dynamicY The equation to compute the position from
|
||||
*/
|
||||
default void setDynamicY(String dynamicY){
|
||||
default void setDynamicY(String dynamicY) {
|
||||
getProperties().dynamicY = dynamicY;
|
||||
getControlView().setY(getProperties().insertDynamicPos(dynamicY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a dynamic equation from an absolute position, used to scale properly across devices
|
||||
*
|
||||
* @param x The absolute position on the horizontal axis
|
||||
* @return The equation as a String
|
||||
*/
|
||||
default String generateDynamicX(float x){
|
||||
if(x + (getProperties().getWidth()/2f) > CallbackBridge.physicalWidth/2f){
|
||||
default String generateDynamicX(float x) {
|
||||
if (x + (getProperties().getWidth() / 2f) > CallbackBridge.physicalWidth / 2f) {
|
||||
return (x + getProperties().getWidth()) / CallbackBridge.physicalWidth + " * ${screen_width} - ${width}";
|
||||
}else{
|
||||
} else {
|
||||
return x / CallbackBridge.physicalWidth + " * ${screen_width}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a dynamic equation from an absolute position, used to scale properly across devices
|
||||
*
|
||||
* @param y The absolute position on the vertical axis
|
||||
* @return The equation as a String
|
||||
*/
|
||||
default String generateDynamicY(float y){
|
||||
if(y + (getProperties().getHeight()/2f) > CallbackBridge.physicalHeight/2f){
|
||||
return (y + getProperties().getHeight()) / CallbackBridge.physicalHeight + " * ${screen_height} - ${height}";
|
||||
}else{
|
||||
default String generateDynamicY(float y) {
|
||||
if (y + (getProperties().getHeight() / 2f) > CallbackBridge.physicalHeight / 2f) {
|
||||
return (y + getProperties().getHeight()) / CallbackBridge.physicalHeight + " * ${screen_height} - ${height}";
|
||||
} else {
|
||||
return y / CallbackBridge.physicalHeight + " * ${screen_height}";
|
||||
}
|
||||
}
|
||||
|
||||
/** Regenerate and apply coordinates with supposedly modified properties */
|
||||
default void regenerateDynamicCoordinates(){
|
||||
/**
|
||||
* Regenerate and apply coordinates with supposedly modified properties
|
||||
*/
|
||||
default void regenerateDynamicCoordinates() {
|
||||
getProperties().dynamicX = generateDynamicX(getControlView().getX());
|
||||
getProperties().dynamicY = generateDynamicY(getControlView().getY());
|
||||
updateProperties();
|
||||
@@ -152,30 +182,28 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
/**
|
||||
* Do a pre-conversion of an equation using values from a button,
|
||||
* so the variables can be used for another button
|
||||
*
|
||||
* <p>
|
||||
* Internal use only.
|
||||
*
|
||||
* @param equation The dynamic position as a String
|
||||
* @param button The button to get the values from.
|
||||
* @param button The button to get the values from.
|
||||
* @return The pre-processed equation as a String.
|
||||
*/
|
||||
default String applySize(String equation, ControlInterface button){
|
||||
default String applySize(String equation, ControlInterface button) {
|
||||
return equation
|
||||
.replace("${right}", "(${screen_width} - ${width})")
|
||||
.replace("${bottom}","(${screen_height} - ${height})")
|
||||
.replace("${bottom}", "(${screen_height} - ${height})")
|
||||
.replace("${height}", "(px(" + Tools.pxToDp(button.getProperties().getHeight()) + ") /" + PREF_BUTTONSIZE + " * ${preferred_scale})")
|
||||
.replace("${width}", "(px(" + Tools.pxToDp(button.getProperties().getWidth()) + ") / " + PREF_BUTTONSIZE + " * ${preferred_scale})");
|
||||
}
|
||||
|
||||
/** Convert a size percentage into a px size */
|
||||
default int computeStrokeWidth(float widthInPercent){
|
||||
float maxSize = Math.max(getProperties().getWidth(), getProperties().getHeight());
|
||||
return (int)((maxSize/2) * (widthInPercent/100));
|
||||
}
|
||||
|
||||
/** Convert a corner radius percentage into a px corner radius */
|
||||
default float computeCornerRadius(float radiusInPercent){
|
||||
/**
|
||||
* Convert a corner radius percentage into a px corner radius
|
||||
*/
|
||||
default float computeCornerRadius(float radiusInPercent) {
|
||||
float minSize = Math.min(getProperties().getWidth(), getProperties().getHeight());
|
||||
return (minSize/2) * (radiusInPercent/100);
|
||||
return (minSize / 2) * (radiusInPercent / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,10 +213,10 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
* @return whether or not the button
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
default boolean canSnap(ControlInterface button){
|
||||
default boolean canSnap(ControlInterface button) {
|
||||
float MIN_DISTANCE = Tools.dpToPx(8);
|
||||
|
||||
if(button == this) return false;
|
||||
if (button == this) return false;
|
||||
return !(net.kdt.pojavlaunch.utils.MathUtils.dist(
|
||||
button.getControlView().getX() + button.getControlView().getWidth() / 2f,
|
||||
button.getControlView().getY() + button.getControlView().getHeight() / 2f,
|
||||
@@ -202,13 +230,13 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
* Try to snap, then align to neighboring buttons, given the provided coordinates.
|
||||
* The new position is automatically applied to the View,
|
||||
* regardless of if the View snapped or not.
|
||||
*
|
||||
* <p>
|
||||
* The new position is always dynamic, thus replacing previous dynamic positions
|
||||
*
|
||||
* @param x Coordinate on the x axis
|
||||
* @param y Coordinate on the y axis
|
||||
*/
|
||||
default void snapAndAlign(float x, float y){
|
||||
default void snapAndAlign(float x, float y) {
|
||||
float MIN_DISTANCE = Tools.dpToPx(8);
|
||||
String dynamicX = generateDynamicX(x);
|
||||
String dynamicY = generateDynamicY(y);
|
||||
@@ -216,9 +244,9 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
getControlView().setX(x);
|
||||
getControlView().setY(y);
|
||||
|
||||
for(ControlInterface button : ((ControlLayout) getControlView().getParent()).getButtonChildren()){
|
||||
for (ControlInterface button : ((ControlLayout) getControlView().getParent()).getButtonChildren()) {
|
||||
//Step 1: Filter unwanted buttons
|
||||
if(!canSnap(button)) continue;
|
||||
if (!canSnap(button)) continue;
|
||||
|
||||
//Step 2: Get Coordinates
|
||||
float button_top = button.getControlView().getY();
|
||||
@@ -232,28 +260,28 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
float right = getControlView().getX() + getControlView().getWidth();
|
||||
|
||||
//Step 3: For each axis, we try to snap to the nearest
|
||||
if(Math.abs(top - button_bottom) < MIN_DISTANCE){ // Bottom snap
|
||||
dynamicY = applySize(button.getProperties().dynamicY, button) + applySize(" + ${height}", button) + " + ${margin}" ;
|
||||
}else if(Math.abs(button_top - bottom) < MIN_DISTANCE){ //Top snap
|
||||
if (Math.abs(top - button_bottom) < MIN_DISTANCE) { // Bottom snap
|
||||
dynamicY = applySize(button.getProperties().dynamicY, button) + applySize(" + ${height}", button) + " + ${margin}";
|
||||
} else if (Math.abs(button_top - bottom) < MIN_DISTANCE) { //Top snap
|
||||
dynamicY = applySize(button.getProperties().dynamicY, button) + " - ${height} - ${margin}";
|
||||
}
|
||||
if(!dynamicY.equals(generateDynamicY(getControlView().getY()))){ //If we snapped
|
||||
if(Math.abs(button_left - left) < MIN_DISTANCE){ //Left align snap
|
||||
if (!dynamicY.equals(generateDynamicY(getControlView().getY()))) { //If we snapped
|
||||
if (Math.abs(button_left - left) < MIN_DISTANCE) { //Left align snap
|
||||
dynamicX = applySize(button.getProperties().dynamicX, button);
|
||||
}else if(Math.abs(button_right - right) < MIN_DISTANCE){ //Right align snap
|
||||
} else if (Math.abs(button_right - right) < MIN_DISTANCE) { //Right align snap
|
||||
dynamicX = applySize(button.getProperties().dynamicX, button) + applySize(" + ${width}", button) + " - ${width}";
|
||||
}
|
||||
}
|
||||
|
||||
if(Math.abs(button_left - right) < MIN_DISTANCE){ //Left snap
|
||||
if (Math.abs(button_left - right) < MIN_DISTANCE) { //Left snap
|
||||
dynamicX = applySize(button.getProperties().dynamicX, button) + " - ${width} - ${margin}";
|
||||
}else if(Math.abs(left - button_right) < MIN_DISTANCE){ //Right snap
|
||||
} else if (Math.abs(left - button_right) < MIN_DISTANCE) { //Right snap
|
||||
dynamicX = applySize(button.getProperties().dynamicX, button) + applySize(" + ${width}", button) + " + ${margin}";
|
||||
}
|
||||
if(!dynamicX.equals(generateDynamicX(getControlView().getX()))){ //If we snapped
|
||||
if(Math.abs(button_top - top) < MIN_DISTANCE){ //Top align snap
|
||||
if (!dynamicX.equals(generateDynamicX(getControlView().getX()))) { //If we snapped
|
||||
if (Math.abs(button_top - top) < MIN_DISTANCE) { //Top align snap
|
||||
dynamicY = applySize(button.getProperties().dynamicY, button);
|
||||
}else if(Math.abs(button_bottom - bottom) < MIN_DISTANCE){ //Bottom align snap
|
||||
} else if (Math.abs(button_bottom - bottom) < MIN_DISTANCE) { //Bottom align snap
|
||||
dynamicY = applySize(button.getProperties().dynamicY, button) + applySize(" + ${height}", button) + " - ${height}";
|
||||
}
|
||||
}
|
||||
@@ -264,19 +292,50 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
setDynamicY(dynamicY);
|
||||
}
|
||||
|
||||
/** Wrapper for multiple injections at once */
|
||||
default void injectBehaviors(){
|
||||
/**
|
||||
* Wrapper for multiple injections at once
|
||||
*/
|
||||
default void injectBehaviors() {
|
||||
injectProperties();
|
||||
injectTouchEventBehavior();
|
||||
injectLayoutParamBehavior();
|
||||
injectGrabListenerBehavior();
|
||||
}
|
||||
|
||||
default void injectProperties(){
|
||||
/**
|
||||
* Inject the grab listener, remove it when the view is gone
|
||||
*/
|
||||
default void injectGrabListenerBehavior() {
|
||||
if (getControlView() == null) {
|
||||
Log.e(ControlInterface.class.toString(), "Failed to inject grab listener behavior !");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
getControlView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull View v) {
|
||||
CallbackBridge.addGrabListener(ControlInterface.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull View v) {
|
||||
getControlView().removeOnAttachStateChangeListener(this);
|
||||
CallbackBridge.removeGrabListener(ControlInterface.this);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
default void injectProperties() {
|
||||
getControlView().post(() -> getControlView().setTranslationZ(10));
|
||||
}
|
||||
|
||||
/** Inject a touch listener on the view to make editing controls straight forward */
|
||||
default void injectTouchEventBehavior(){
|
||||
/**
|
||||
* Inject a touch listener on the view to make editing controls straight forward
|
||||
*/
|
||||
default void injectTouchEventBehavior() {
|
||||
getControlView().setOnTouchListener(new View.OnTouchListener() {
|
||||
private boolean mCanTriggerLongClick = true;
|
||||
private float downX, downY;
|
||||
@@ -285,7 +344,7 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent event) {
|
||||
if(!getControlLayoutParent().getModifiable()){
|
||||
if (!getControlLayoutParent().getModifiable()) {
|
||||
// Basically, editing behavior is forced while in game behavior is specific
|
||||
view.onTouchEvent(event);
|
||||
return true;
|
||||
@@ -309,7 +368,7 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if(Math.abs(event.getRawX() - downRawX) > 8 || Math.abs(event.getRawY() - downRawY) > 8)
|
||||
if (Math.abs(event.getRawX() - downRawX) > 8 || Math.abs(event.getRawY() - downRawY) > 8)
|
||||
mCanTriggerLongClick = false;
|
||||
getControlLayoutParent().adaptPanelPosition();
|
||||
|
||||
@@ -327,17 +386,17 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
});
|
||||
}
|
||||
|
||||
default void injectLayoutParamBehavior(){
|
||||
default void injectLayoutParamBehavior() {
|
||||
getControlView().addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
getProperties().setWidth(right-left);
|
||||
getProperties().setHeight(bottom-top);
|
||||
getProperties().setWidth(right - left);
|
||||
getProperties().setHeight(bottom - top);
|
||||
setBackground();
|
||||
|
||||
// Re-calculate position
|
||||
if(!getProperties().isDynamicBtn){
|
||||
if (!getProperties().isDynamicBtn) {
|
||||
getControlView().setX(getControlView().getX());
|
||||
getControlView().setY(getControlView().getY());
|
||||
}else {
|
||||
} else {
|
||||
getControlView().setX(getProperties().insertDynamicPos(getProperties().dynamicX));
|
||||
getControlView().setY(getProperties().insertDynamicPos(getProperties().dynamicY));
|
||||
}
|
||||
@@ -345,7 +404,7 @@ public interface ControlInterface extends View.OnLongClickListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean onLongClick(View v){
|
||||
default boolean onLongClick(View v) {
|
||||
if (getControlLayoutParent().getModifiable()) {
|
||||
getControlLayoutParent().editControlButton(this);
|
||||
getControlLayoutParent().mActionRow.setFollowedButton(this);
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package net.kdt.pojavlaunch.customcontrols.buttons;
|
||||
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_EAST;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NONE;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NORTH;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NORTH_EAST;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_NORTH_WEST;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_SOUTH;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_SOUTH_EAST;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_SOUTH_WEST;
|
||||
import static net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick.DIRECTION_WEST;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.View;
|
||||
|
||||
import net.kdt.pojavlaunch.LwjglGlfwKeycode;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlJoystickData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
|
||||
import net.kdt.pojavlaunch.customcontrols.gamepad.GamepadJoystick;
|
||||
import net.kdt.pojavlaunch.customcontrols.handleview.EditControlPopup;
|
||||
|
||||
import org.lwjgl.glfw.CallbackBridge;
|
||||
|
||||
import io.github.controlwear.virtual.joystick.android.JoystickView;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class ControlJoystick extends JoystickView implements ControlInterface {
|
||||
public final static int DIRECTION_FORWARD_LOCK = 8;
|
||||
// Directions keycode
|
||||
private final int[] mDirectionForwardLock = new int[]{LwjglGlfwKeycode.GLFW_KEY_LEFT_CONTROL};
|
||||
private final int[] mDirectionForward = new int[]{LwjglGlfwKeycode.GLFW_KEY_W};
|
||||
private final int[] mDirectionRight = new int[]{LwjglGlfwKeycode.GLFW_KEY_D};
|
||||
private final int[] mDirectionBackward = new int[]{LwjglGlfwKeycode.GLFW_KEY_S};
|
||||
private final int[] mDirectionLeft = new int[]{LwjglGlfwKeycode.GLFW_KEY_A};
|
||||
private ControlJoystickData mControlData;
|
||||
private int mLastDirectionInt = GamepadJoystick.DIRECTION_NONE;
|
||||
private int mCurrentDirectionInt = GamepadJoystick.DIRECTION_NONE;
|
||||
public ControlJoystick(ControlLayout parent, ControlJoystickData data) {
|
||||
super(parent.getContext());
|
||||
init(data, parent);
|
||||
}
|
||||
|
||||
private static void sendInput(int[] keys, boolean isDown) {
|
||||
for (int key : keys) {
|
||||
CallbackBridge.sendKeyPress(key, CallbackBridge.getCurrentMods(), isDown);
|
||||
}
|
||||
}
|
||||
|
||||
private void init(ControlJoystickData data, ControlLayout layout) {
|
||||
mControlData = data;
|
||||
setProperties(preProcessProperties(data, layout));
|
||||
setDeadzone(35);
|
||||
setFixedCenter(false);
|
||||
setAutoReCenterButton(true);
|
||||
|
||||
injectBehaviors();
|
||||
|
||||
setOnMoveListener(new OnMoveListener() {
|
||||
@Override
|
||||
public void onMove(int angle, int strength) {
|
||||
mLastDirectionInt = mCurrentDirectionInt;
|
||||
mCurrentDirectionInt = getDirectionInt(angle, strength);
|
||||
|
||||
if (mLastDirectionInt != mCurrentDirectionInt) {
|
||||
sendDirectionalKeycode(mLastDirectionInt, false);
|
||||
sendDirectionalKeycode(mCurrentDirectionInt, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onForwardLock(boolean isLocked) {
|
||||
sendInput(mDirectionForwardLock, isLocked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getControlView() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ControlData getProperties() {
|
||||
return mControlData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperties(ControlData properties, boolean changePos) {
|
||||
mControlData = (ControlJoystickData) properties;
|
||||
mControlData.isHideable = true;
|
||||
ControlInterface.super.setProperties(properties, changePos);
|
||||
postDelayed(() -> setForwardLockDistance(mControlData.forwardLock ? (int) Tools.dpToPx(60) : 0), 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeButton() {
|
||||
getControlLayoutParent().getLayout().mJoystickDataList.remove(getProperties());
|
||||
getControlLayoutParent().removeView(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cloneButton() {
|
||||
ControlData data = new ControlJoystickData(getProperties());
|
||||
getControlLayoutParent().addJoystickButton((ControlJoystickData) data);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setBackground() {
|
||||
setBorderWidth((int) Tools.dpToPx(getProperties().strokeWidth));
|
||||
setBorderColor(getProperties().strokeColor);
|
||||
setBackgroundColor(getProperties().bgColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendKeyPresses(boolean isDown) {/*STUB since non swipeable*/ }
|
||||
|
||||
@Override
|
||||
public void loadEditValues(EditControlPopup editControlPopup) {
|
||||
editControlPopup.loadJoystickValues(mControlData);
|
||||
}
|
||||
|
||||
private int getDirectionInt(int angle, int intensity) {
|
||||
if (intensity == 0) return DIRECTION_NONE;
|
||||
return (int) (((angle + 22.5) / 45) % 8);
|
||||
}
|
||||
|
||||
private void sendDirectionalKeycode(int direction, boolean isDown) {
|
||||
switch (direction) {
|
||||
case DIRECTION_NORTH:
|
||||
sendInput(mDirectionForward, isDown);
|
||||
break;
|
||||
case DIRECTION_NORTH_EAST:
|
||||
sendInput(mDirectionForward, isDown);
|
||||
sendInput(mDirectionRight, isDown);
|
||||
break;
|
||||
case DIRECTION_EAST:
|
||||
sendInput(mDirectionRight, isDown);
|
||||
break;
|
||||
case DIRECTION_SOUTH_EAST:
|
||||
sendInput(mDirectionRight, isDown);
|
||||
sendInput(mDirectionBackward, isDown);
|
||||
break;
|
||||
case DIRECTION_SOUTH:
|
||||
sendInput(mDirectionBackward, isDown);
|
||||
break;
|
||||
case DIRECTION_SOUTH_WEST:
|
||||
sendInput(mDirectionBackward, isDown);
|
||||
sendInput(mDirectionLeft, isDown);
|
||||
break;
|
||||
case DIRECTION_WEST:
|
||||
sendInput(mDirectionLeft, isDown);
|
||||
break;
|
||||
case DIRECTION_NORTH_WEST:
|
||||
sendInput(mDirectionForward, isDown);
|
||||
sendInput(mDirectionLeft, isDown);
|
||||
break;
|
||||
case DIRECTION_FORWARD_LOCK:
|
||||
sendInput(mDirectionForwardLock, isDown);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlDrawerData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
|
||||
import net.kdt.pojavlaunch.customcontrols.handleview.EditControlPopup;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class ControlSubButton extends ControlButton {
|
||||
@@ -21,8 +22,10 @@ public class ControlSubButton extends ControlButton {
|
||||
}
|
||||
|
||||
private void filterProperties(){
|
||||
mProperties.setHeight(parentDrawer.getProperties().getHeight());
|
||||
mProperties.setWidth(parentDrawer.getProperties().getWidth());
|
||||
if (parentDrawer != null && parentDrawer.drawerData.orientation != ControlDrawerData.Orientation.FREE) {
|
||||
mProperties.setHeight(parentDrawer.getProperties().getHeight());
|
||||
mProperties.setWidth(parentDrawer.getProperties().getWidth());
|
||||
}
|
||||
mProperties.isDynamicBtn = false;
|
||||
|
||||
setProperties(mProperties, false);
|
||||
@@ -30,12 +33,18 @@ public class ControlSubButton extends ControlButton {
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean isVisible) {
|
||||
setVisibility(isVisible ? (parentDrawer.areButtonsVisible ? VISIBLE : GONE) : (!mProperties.isHideable && parentDrawer.getVisibility() == GONE) ? VISIBLE : View.GONE);
|
||||
// STUB, visibility handled by the ControlDrawer
|
||||
//setVisibility(isVisible ? VISIBLE : (!mProperties.isHideable && parentDrawer.getVisibility() == GONE) ? VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGrabState(boolean isGrabbing) {
|
||||
// STUB, visibility lifecycle handled by the ControlDrawer
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLayoutParams(ViewGroup.LayoutParams params) {
|
||||
if(parentDrawer != null){
|
||||
if(parentDrawer != null && parentDrawer.drawerData.orientation != ControlDrawerData.Orientation.FREE){
|
||||
params.width = (int)parentDrawer.mProperties.getWidth();
|
||||
params.height = (int)parentDrawer.mProperties.getHeight();
|
||||
}
|
||||
@@ -81,4 +90,9 @@ public class ControlSubButton extends ControlButton {
|
||||
super.snapAndAlign(x, y);
|
||||
// Else the button is forced into place
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadEditValues(EditControlPopup editControlPopup) {
|
||||
editControlPopup.loadSubButtonValues(getProperties(), parentDrawer.drawerData.orientation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
@@ -34,6 +36,7 @@ import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.colorselector.ColorSelector;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlDrawerData;
|
||||
import net.kdt.pojavlaunch.customcontrols.ControlJoystickData;
|
||||
import net.kdt.pojavlaunch.customcontrols.buttons.ControlDrawer;
|
||||
import net.kdt.pojavlaunch.customcontrols.buttons.ControlInterface;
|
||||
|
||||
@@ -43,54 +46,54 @@ import java.util.List;
|
||||
* Class providing a sort of popup on top of a Layout, allowing to edit a given ControlButton
|
||||
*/
|
||||
public class EditControlPopup {
|
||||
protected final Spinner[] mKeycodeSpinners = new Spinner[4];
|
||||
private final DefocusableScrollView mScrollView;
|
||||
private ConstraintLayout mRootView;
|
||||
private final ColorSelector mColorSelector;
|
||||
|
||||
private final ObjectAnimator mEditPopupAnimator;
|
||||
private final ObjectAnimator mColorEditorAnimator;
|
||||
private boolean mDisplaying = false;
|
||||
private boolean mDisplayingColor = false;
|
||||
public boolean internalChanges = false; // True when we programmatically change stuff.
|
||||
private ControlInterface mCurrentlyEditedButton;
|
||||
private final int mMargin;
|
||||
public boolean internalChanges = false; // True when we programmatically change stuff.
|
||||
private final View.OnLayoutChangeListener mLayoutChangedListener = new View.OnLayoutChangeListener() {
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
|
||||
internalChanges = true;
|
||||
int width = (int)(safeParseFloat(mWidthEditText.getText().toString()));
|
||||
int width = (int) (safeParseFloat(mWidthEditText.getText().toString()));
|
||||
|
||||
if(width >= 0 && Math.abs(right - width) > 1){
|
||||
if (width >= 0 && Math.abs(right - width) > 1) {
|
||||
mWidthEditText.setText(String.valueOf(right - left));
|
||||
}
|
||||
int height = (int)(safeParseFloat(mHeightEditText.getText().toString()));
|
||||
if(height >= 0 && Math.abs(bottom - height) > 1){
|
||||
int height = (int) (safeParseFloat(mHeightEditText.getText().toString()));
|
||||
if (height >= 0 && Math.abs(bottom - height) > 1) {
|
||||
mHeightEditText.setText(String.valueOf(bottom - top));
|
||||
}
|
||||
|
||||
internalChanges = false;
|
||||
}
|
||||
};
|
||||
|
||||
protected EditText mNameEditText, mWidthEditText, mHeightEditText;
|
||||
@SuppressLint("UseSwitchCompatOrMaterialCode")
|
||||
protected Switch mToggleSwitch, mPassthroughSwitch, mSwipeableSwitch;
|
||||
protected Switch mToggleSwitch, mPassthroughSwitch, mSwipeableSwitch, mForwardLockSwitch;
|
||||
protected Spinner mOrientationSpinner;
|
||||
protected final Spinner[] mKeycodeSpinners = new Spinner[4];
|
||||
protected TextView[] mKeycodeTextviews = new TextView[4];
|
||||
protected SeekBar mStrokeWidthSeekbar, mCornerRadiusSeekbar, mAlphaSeekbar;
|
||||
protected TextView mStrokePercentTextView, mCornerRadiusPercentTextView, mAlphaPercentTextView;
|
||||
protected TextView mSelectBackgroundColor, mSelectStrokeColor;
|
||||
protected ArrayAdapter<String> mAdapter;
|
||||
protected List<String> mSpecialArray;
|
||||
|
||||
protected CheckBox mDisplayInGameCheckbox, mDisplayInMenuCheckbox;
|
||||
private ConstraintLayout mRootView;
|
||||
private boolean mDisplaying = false;
|
||||
private boolean mDisplayingColor = false;
|
||||
private ControlInterface mCurrentlyEditedButton;
|
||||
// Decorative textviews
|
||||
private TextView mOrientationTextView, mMappingTextView, mNameTextView, mCornerRadiusTextView;
|
||||
private TextView mOrientationTextView, mMappingTextView, mNameTextView,
|
||||
mCornerRadiusTextView, mVisibilityTextView, mSizeTextview, mSizeXTextView;
|
||||
|
||||
|
||||
|
||||
public EditControlPopup(Context context, ViewGroup parent){
|
||||
public EditControlPopup(Context context, ViewGroup parent) {
|
||||
mScrollView = (DefocusableScrollView) LayoutInflater.from(context).inflate(R.layout.dialog_control_button_setting, parent, false);
|
||||
parent.addView(mScrollView);
|
||||
|
||||
@@ -101,8 +104,8 @@ public class EditControlPopup {
|
||||
mColorSelector.getRootView().setTranslationZ(11);
|
||||
mColorSelector.getRootView().setX(-context.getResources().getDimensionPixelOffset(R.dimen._280sdp));
|
||||
|
||||
mEditPopupAnimator = ObjectAnimator.ofFloat(mScrollView, "x", 0).setDuration(1000);
|
||||
mColorEditorAnimator = ObjectAnimator.ofFloat(mColorSelector.getRootView(), "x", 0).setDuration(1000);
|
||||
mEditPopupAnimator = ObjectAnimator.ofFloat(mScrollView, "x", 0).setDuration(600);
|
||||
mColorEditorAnimator = ObjectAnimator.ofFloat(mColorSelector.getRootView(), "x", 0).setDuration(600);
|
||||
Interpolator decelerate = new AccelerateDecelerateInterpolator();
|
||||
mEditPopupAnimator.setInterpolator(decelerate);
|
||||
mColorEditorAnimator.setInterpolator(decelerate);
|
||||
@@ -117,18 +120,23 @@ public class EditControlPopup {
|
||||
setupRealTimeListeners();
|
||||
}
|
||||
|
||||
public static void setPercentageText(TextView textView, int progress) {
|
||||
textView.setText(textView.getContext().getString(R.string.percent_format, progress));
|
||||
}
|
||||
|
||||
/** Slide the layout into the visible screen area */
|
||||
public void appear(boolean fromRight){
|
||||
/**
|
||||
* Slide the layout into the visible screen area
|
||||
*/
|
||||
public void appear(boolean fromRight) {
|
||||
disappearColor(); // When someone jumps from a button to another
|
||||
|
||||
if(fromRight){
|
||||
if(!mDisplaying || !isAtRight()){
|
||||
if (fromRight) {
|
||||
if (!mDisplaying || !isAtRight()) {
|
||||
mEditPopupAnimator.setFloatValues(currentDisplayMetrics.widthPixels, currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin);
|
||||
mEditPopupAnimator.start();
|
||||
}
|
||||
}else{
|
||||
if (!mDisplaying || isAtRight()){
|
||||
} else {
|
||||
if (!mDisplaying || isAtRight()) {
|
||||
mEditPopupAnimator.setFloatValues(-mScrollView.getWidth(), mMargin);
|
||||
mEditPopupAnimator.start();
|
||||
}
|
||||
@@ -137,12 +145,14 @@ public class EditControlPopup {
|
||||
mDisplaying = true;
|
||||
}
|
||||
|
||||
/** Slide out the layout */
|
||||
public void disappear(){
|
||||
if(!mDisplaying) return;
|
||||
/**
|
||||
* Slide out the layout
|
||||
*/
|
||||
public void disappear() {
|
||||
if (!mDisplaying) return;
|
||||
|
||||
mDisplaying = false;
|
||||
if(isAtRight())
|
||||
if (isAtRight())
|
||||
mEditPopupAnimator.setFloatValues(currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin, currentDisplayMetrics.widthPixels);
|
||||
else
|
||||
mEditPopupAnimator.setFloatValues(mMargin, -mScrollView.getWidth());
|
||||
@@ -150,30 +160,39 @@ public class EditControlPopup {
|
||||
mEditPopupAnimator.start();
|
||||
}
|
||||
|
||||
/** Slide the layout into the visible screen area */
|
||||
public void appearColor(boolean fromRight, int color){
|
||||
if(fromRight){
|
||||
if(!mDisplayingColor || !isAtRight()){
|
||||
/**
|
||||
* Slide the layout into the visible screen area
|
||||
*/
|
||||
public void appearColor(boolean fromRight, int color) {
|
||||
if (fromRight) {
|
||||
if (!mDisplayingColor || !isAtRight()) {
|
||||
mColorEditorAnimator.setFloatValues(currentDisplayMetrics.widthPixels, currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin);
|
||||
mColorEditorAnimator.start();
|
||||
}
|
||||
}else{
|
||||
if (!mDisplayingColor || isAtRight()){
|
||||
} else {
|
||||
if (!mDisplayingColor || isAtRight()) {
|
||||
mColorEditorAnimator.setFloatValues(-mScrollView.getWidth(), mMargin);
|
||||
mColorEditorAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the color selector to have the same size as the control settings
|
||||
ViewGroup.LayoutParams params = mColorSelector.getRootView().getLayoutParams();
|
||||
params.height = mScrollView.getHeight();
|
||||
mColorSelector.getRootView().setLayoutParams(params);
|
||||
|
||||
mDisplayingColor = true;
|
||||
mColorSelector.show(color == -1 ? Color.WHITE : color);
|
||||
}
|
||||
|
||||
/** Slide out the layout */
|
||||
public void disappearColor(){
|
||||
if(!mDisplayingColor) return;
|
||||
/**
|
||||
* Slide out the layout
|
||||
*/
|
||||
public void disappearColor() {
|
||||
if (!mDisplayingColor) return;
|
||||
|
||||
mDisplayingColor = false;
|
||||
if(isAtRight())
|
||||
if (isAtRight())
|
||||
mColorEditorAnimator.setFloatValues(currentDisplayMetrics.widthPixels - mScrollView.getWidth() - mMargin, currentDisplayMetrics.widthPixels);
|
||||
else
|
||||
mColorEditorAnimator.setFloatValues(mMargin, -mScrollView.getWidth());
|
||||
@@ -181,33 +200,37 @@ public class EditControlPopup {
|
||||
mColorEditorAnimator.start();
|
||||
}
|
||||
|
||||
/** Slide out the first visible layer.
|
||||
* @return True if the last layer is disappearing */
|
||||
public boolean disappearLayer(){
|
||||
if(mDisplayingColor){
|
||||
/**
|
||||
* Slide out the first visible layer.
|
||||
*
|
||||
* @return True if the last layer is disappearing
|
||||
*/
|
||||
public boolean disappearLayer() {
|
||||
if (mDisplayingColor) {
|
||||
disappearColor();
|
||||
return false;
|
||||
}else{
|
||||
} else {
|
||||
disappear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Switch the panels position if needed */
|
||||
public void adaptPanelPosition(){
|
||||
if(mDisplaying){
|
||||
boolean isAtRight = mCurrentlyEditedButton.getControlView().getX() + mCurrentlyEditedButton.getControlView().getWidth()/2f < currentDisplayMetrics.widthPixels/2f;
|
||||
/**
|
||||
* Switch the panels position if needed
|
||||
*/
|
||||
public void adaptPanelPosition() {
|
||||
if (mDisplaying) {
|
||||
boolean isAtRight = mCurrentlyEditedButton.getControlView().getX() + mCurrentlyEditedButton.getControlView().getWidth() / 2f < currentDisplayMetrics.widthPixels / 2f;
|
||||
appear(isAtRight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void destroy(){
|
||||
public void destroy() {
|
||||
((ViewGroup) mScrollView.getParent()).removeView(mColorSelector.getRootView());
|
||||
((ViewGroup) mScrollView.getParent()).removeView(mScrollView);
|
||||
}
|
||||
|
||||
private void loadAdapter(){
|
||||
private void loadAdapter() {
|
||||
//Initialize adapter for keycodes
|
||||
mAdapter = new ArrayAdapter<>(mRootView.getContext(), R.layout.item_centered_textview);
|
||||
mSpecialArray = ControlData.buildSpecialButtonArray();
|
||||
@@ -228,48 +251,50 @@ public class EditControlPopup {
|
||||
mOrientationSpinner.setAdapter(adapter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void setDefaultVisibilitySetting(){
|
||||
for(int i=0; i<mRootView.getChildCount(); ++i){
|
||||
private void setDefaultVisibilitySetting() {
|
||||
for (int i = 0; i < mRootView.getChildCount(); ++i) {
|
||||
mRootView.getChildAt(i).setVisibility(VISIBLE);
|
||||
}
|
||||
for(Spinner s : mKeycodeSpinners) {
|
||||
s.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAtRight(){
|
||||
return mScrollView.getX() > currentDisplayMetrics.widthPixels/2f;
|
||||
}
|
||||
|
||||
|
||||
public static void setPercentageText(TextView textView, int progress){
|
||||
textView.setText(textView.getContext().getString(R.string.percent_format, progress));
|
||||
private boolean isAtRight() {
|
||||
return mScrollView.getX() > currentDisplayMetrics.widthPixels / 2f;
|
||||
}
|
||||
|
||||
/* LOADING VALUES */
|
||||
|
||||
/** Load values for basic control data */
|
||||
public void loadValues(ControlData data){
|
||||
/**
|
||||
* Load values for basic control data
|
||||
*/
|
||||
public void loadValues(ControlData data) {
|
||||
setDefaultVisibilitySetting();
|
||||
mOrientationTextView.setVisibility(GONE);
|
||||
mOrientationSpinner.setVisibility(GONE);
|
||||
mForwardLockSwitch.setVisibility(GONE);
|
||||
|
||||
mNameEditText.setText(data.name);
|
||||
mWidthEditText.setText(String.valueOf(data.getWidth()));
|
||||
mHeightEditText.setText(String.valueOf(data.getHeight()));
|
||||
|
||||
mAlphaSeekbar.setProgress((int) (data.opacity*100));
|
||||
mStrokeWidthSeekbar.setProgress(data.strokeWidth);
|
||||
mAlphaSeekbar.setProgress((int) (data.opacity * 100));
|
||||
mStrokeWidthSeekbar.setProgress((int) data.strokeWidth * 10);
|
||||
mCornerRadiusSeekbar.setProgress((int) data.cornerRadius);
|
||||
|
||||
setPercentageText(mAlphaPercentTextView, (int) (data.opacity*100));
|
||||
setPercentageText(mStrokePercentTextView, data.strokeWidth);
|
||||
setPercentageText(mAlphaPercentTextView, (int) (data.opacity * 100));
|
||||
setPercentageText(mStrokePercentTextView, (int) data.strokeWidth * 10);
|
||||
setPercentageText(mCornerRadiusPercentTextView, (int) data.cornerRadius);
|
||||
|
||||
mToggleSwitch.setChecked(data.isToggle);
|
||||
mPassthroughSwitch.setChecked(data.passThruEnabled);
|
||||
mSwipeableSwitch.setChecked(data.isSwipeable);
|
||||
|
||||
for(int i = 0; i< data.keycodes.length; i++){
|
||||
mDisplayInGameCheckbox.setChecked(data.displayInGame);
|
||||
mDisplayInMenuCheckbox.setChecked(data.displayInMenu);
|
||||
|
||||
for (int i = 0; i < data.keycodes.length; i++) {
|
||||
if (data.keycodes[i] < 0) {
|
||||
mKeycodeSpinners[i].setSelection(data.keycodes[i] + mSpecialArray.size());
|
||||
} else {
|
||||
@@ -278,18 +303,20 @@ public class EditControlPopup {
|
||||
}
|
||||
}
|
||||
|
||||
/** Load values for extended control data */
|
||||
public void loadValues(ControlDrawerData data){
|
||||
/**
|
||||
* Load values for extended control data
|
||||
*/
|
||||
public void loadValues(ControlDrawerData data) {
|
||||
loadValues(data.properties);
|
||||
|
||||
mOrientationSpinner.setSelection(
|
||||
ControlDrawerData.orientationToInt(data.orientation));
|
||||
|
||||
mMappingTextView.setVisibility(GONE);
|
||||
mKeycodeSpinners[0].setVisibility(GONE);
|
||||
mKeycodeSpinners[1].setVisibility(GONE);
|
||||
mKeycodeSpinners[2].setVisibility(GONE);
|
||||
mKeycodeSpinners[3].setVisibility(GONE);
|
||||
for (int i = 0; i < mKeycodeSpinners.length; i++) {
|
||||
mKeycodeSpinners[i].setVisibility(GONE);
|
||||
mKeycodeTextviews[i].setVisibility(GONE);
|
||||
}
|
||||
|
||||
mOrientationTextView.setVisibility(VISIBLE);
|
||||
mOrientationSpinner.setVisibility(VISIBLE);
|
||||
@@ -299,15 +326,17 @@ public class EditControlPopup {
|
||||
mToggleSwitch.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/** Load values for the joystick */
|
||||
@SuppressWarnings("unused") public void loadJoystickValues(ControlData data){
|
||||
/**
|
||||
* Load values for the joystick
|
||||
*/
|
||||
public void loadJoystickValues(ControlJoystickData data) {
|
||||
loadValues(data);
|
||||
|
||||
mMappingTextView.setVisibility(GONE);
|
||||
mKeycodeSpinners[0].setVisibility(GONE);
|
||||
mKeycodeSpinners[1].setVisibility(GONE);
|
||||
mKeycodeSpinners[2].setVisibility(GONE);
|
||||
mKeycodeSpinners[3].setVisibility(GONE);
|
||||
for (int i = 0; i < mKeycodeSpinners.length; i++) {
|
||||
mKeycodeSpinners[i].setVisibility(GONE);
|
||||
mKeycodeTextviews[i].setVisibility(GONE);
|
||||
}
|
||||
|
||||
mNameTextView.setVisibility(GONE);
|
||||
mNameEditText.setVisibility(GONE);
|
||||
@@ -319,10 +348,33 @@ public class EditControlPopup {
|
||||
mSwipeableSwitch.setVisibility(View.GONE);
|
||||
mPassthroughSwitch.setVisibility(View.GONE);
|
||||
mToggleSwitch.setVisibility(View.GONE);
|
||||
|
||||
mForwardLockSwitch.setVisibility(VISIBLE);
|
||||
mForwardLockSwitch.setChecked(data.forwardLock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load values for sub buttons
|
||||
*/
|
||||
public void loadSubButtonValues(ControlData data, ControlDrawerData.Orientation drawerOrientation) {
|
||||
loadValues(data);
|
||||
|
||||
// Size linked to the parent drawer depending on the drawer settings
|
||||
if(drawerOrientation != ControlDrawerData.Orientation.FREE){
|
||||
mSizeTextview.setVisibility(GONE);
|
||||
mSizeXTextView.setVisibility(GONE);
|
||||
mWidthEditText.setVisibility(GONE);
|
||||
mHeightEditText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
// No conditional, already depends on the parent drawer visibility
|
||||
mVisibilityTextView.setVisibility(GONE);
|
||||
mDisplayInMenuCheckbox.setVisibility(GONE);
|
||||
mDisplayInGameCheckbox.setVisibility(GONE);
|
||||
}
|
||||
|
||||
|
||||
private void bindLayout(){
|
||||
private void bindLayout() {
|
||||
mRootView = mScrollView.findViewById(R.id.edit_layout);
|
||||
mNameEditText = mScrollView.findViewById(R.id.editName_editText);
|
||||
mWidthEditText = mScrollView.findViewById(R.id.editSize_editTextX);
|
||||
@@ -330,10 +382,15 @@ public class EditControlPopup {
|
||||
mToggleSwitch = mScrollView.findViewById(R.id.checkboxToggle);
|
||||
mPassthroughSwitch = mScrollView.findViewById(R.id.checkboxPassThrough);
|
||||
mSwipeableSwitch = mScrollView.findViewById(R.id.checkboxSwipeable);
|
||||
mForwardLockSwitch = mScrollView.findViewById(R.id.checkboxForwardLock);
|
||||
mKeycodeSpinners[0] = mScrollView.findViewById(R.id.editMapping_spinner_1);
|
||||
mKeycodeSpinners[1] = mScrollView.findViewById(R.id.editMapping_spinner_2);
|
||||
mKeycodeSpinners[2] = mScrollView.findViewById(R.id.editMapping_spinner_3);
|
||||
mKeycodeSpinners[3] = mScrollView.findViewById(R.id.editMapping_spinner_4);
|
||||
mKeycodeTextviews[0] = mScrollView.findViewById(R.id.mapping_1_textview);
|
||||
mKeycodeTextviews[1] = mScrollView.findViewById(R.id.mapping_2_textview);
|
||||
mKeycodeTextviews[2] = mScrollView.findViewById(R.id.mapping_3_textview);
|
||||
mKeycodeTextviews[3] = mScrollView.findViewById(R.id.mapping_4_textview);
|
||||
mOrientationSpinner = mScrollView.findViewById(R.id.editOrientation_spinner);
|
||||
mStrokeWidthSeekbar = mScrollView.findViewById(R.id.editStrokeWidth_seekbar);
|
||||
mCornerRadiusSeekbar = mScrollView.findViewById(R.id.editCornerRadius_seekbar);
|
||||
@@ -343,28 +400,36 @@ public class EditControlPopup {
|
||||
mStrokePercentTextView = mScrollView.findViewById(R.id.editStrokeWidth_textView_percent);
|
||||
mAlphaPercentTextView = mScrollView.findViewById(R.id.editButtonOpacity_textView_percent);
|
||||
mCornerRadiusPercentTextView = mScrollView.findViewById(R.id.editCornerRadius_textView_percent);
|
||||
mDisplayInGameCheckbox = mScrollView.findViewById(R.id.visibility_game_checkbox);
|
||||
mDisplayInMenuCheckbox = mScrollView.findViewById(R.id.visibility_menu_checkbox);
|
||||
|
||||
//Decorative stuff
|
||||
mMappingTextView = mScrollView.findViewById(R.id.editMapping_textView);
|
||||
mOrientationTextView = mScrollView.findViewById(R.id.editOrientation_textView);
|
||||
mNameTextView = mScrollView.findViewById(R.id.editName_textView);
|
||||
mCornerRadiusTextView = mScrollView.findViewById(R.id.editCornerRadius_textView);
|
||||
mVisibilityTextView = mScrollView.findViewById(R.id.visibility_textview);
|
||||
mSizeTextview = mScrollView.findViewById(R.id.editSize_textView);
|
||||
mSizeXTextView = mScrollView.findViewById(R.id.editSize_x_textView);
|
||||
}
|
||||
|
||||
/**
|
||||
* A long function linking all the displayed data on the popup and,
|
||||
* the currently edited mCurrentlyEditedButton
|
||||
*/
|
||||
public void setupRealTimeListeners(){
|
||||
public void setupRealTimeListeners() {
|
||||
mNameEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
|
||||
mCurrentlyEditedButton.getProperties().name = s.toString();
|
||||
|
||||
@@ -375,16 +440,19 @@ public class EditControlPopup {
|
||||
|
||||
mWidthEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
|
||||
float width = safeParseFloat(s.toString());
|
||||
if(width >= 0){
|
||||
if (width >= 0) {
|
||||
mCurrentlyEditedButton.getProperties().setWidth(width);
|
||||
mCurrentlyEditedButton.updateProperties();
|
||||
}
|
||||
@@ -393,16 +461,19 @@ public class EditControlPopup {
|
||||
|
||||
mHeightEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
|
||||
float height = safeParseFloat(s.toString());
|
||||
if(height >= 0){
|
||||
if (height >= 0) {
|
||||
mCurrentlyEditedButton.getProperties().setHeight(height);
|
||||
mCurrentlyEditedButton.updateProperties();
|
||||
}
|
||||
@@ -410,66 +481,83 @@ public class EditControlPopup {
|
||||
});
|
||||
|
||||
mSwipeableSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().isSwipeable = isChecked;
|
||||
});
|
||||
mToggleSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().isToggle = isChecked;
|
||||
});
|
||||
mPassthroughSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().passThruEnabled = isChecked;
|
||||
});
|
||||
mForwardLockSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (internalChanges) return;
|
||||
if(mCurrentlyEditedButton.getProperties() instanceof ControlJoystickData){
|
||||
((ControlJoystickData) mCurrentlyEditedButton.getProperties()).forwardLock = isChecked;
|
||||
}
|
||||
});
|
||||
|
||||
mAlphaSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if(internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().opacity = mAlphaSeekbar.getProgress()/100f;
|
||||
mCurrentlyEditedButton.getControlView().setAlpha(mAlphaSeekbar.getProgress()/100f);
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().opacity = mAlphaSeekbar.getProgress() / 100f;
|
||||
mCurrentlyEditedButton.getControlView().setAlpha(mAlphaSeekbar.getProgress() / 100f);
|
||||
setPercentageText(mAlphaPercentTextView, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
|
||||
mStrokeWidthSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if(internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().strokeWidth = mStrokeWidthSeekbar.getProgress();
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().strokeWidth = mStrokeWidthSeekbar.getProgress() / 10F;
|
||||
mCurrentlyEditedButton.setBackground();
|
||||
setPercentageText(mStrokePercentTextView, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
|
||||
mCornerRadiusSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if(internalChanges) return;
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().cornerRadius = mCornerRadiusSeekbar.getProgress();
|
||||
mCurrentlyEditedButton.setBackground();
|
||||
setPercentageText(mCornerRadiusPercentTextView, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
for(int i = 0; i< mKeycodeSpinners.length; ++i){
|
||||
for (int i = 0; i < mKeycodeSpinners.length; ++i) {
|
||||
int finalI = i;
|
||||
mKeycodeTextviews[i].setOnClickListener(v -> mKeycodeSpinners[finalI].performClick());
|
||||
|
||||
mKeycodeSpinners[i].setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
@@ -480,10 +568,12 @@ public class EditControlPopup {
|
||||
} else {
|
||||
mCurrentlyEditedButton.getProperties().keycodes[finalI] = EfficientAndroidLWJGLKeycode.getValueByIndex(mKeycodeSpinners[finalI].getSelectedItemPosition() - mSpecialArray.size());
|
||||
}
|
||||
mKeycodeTextviews[finalI].setText((String) mKeycodeSpinners[finalI].getSelectedItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -494,16 +584,26 @@ public class EditControlPopup {
|
||||
// Side note, spinner listeners are fired later than all the other ones.
|
||||
// Meaning the internalChanges bool is useless here.
|
||||
|
||||
if(mCurrentlyEditedButton instanceof ControlDrawer){
|
||||
((ControlDrawer)mCurrentlyEditedButton).drawerData.orientation = ControlDrawerData.intToOrientation(mOrientationSpinner.getSelectedItemPosition());
|
||||
((ControlDrawer)mCurrentlyEditedButton).syncButtons();
|
||||
if (mCurrentlyEditedButton instanceof ControlDrawer) {
|
||||
((ControlDrawer) mCurrentlyEditedButton).drawerData.orientation = ControlDrawerData.intToOrientation(mOrientationSpinner.getSelectedItemPosition());
|
||||
((ControlDrawer) mCurrentlyEditedButton).syncButtons();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
mDisplayInGameCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().displayInGame = isChecked;
|
||||
});
|
||||
|
||||
mDisplayInMenuCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (internalChanges) return;
|
||||
mCurrentlyEditedButton.getProperties().displayInMenu = isChecked;
|
||||
});
|
||||
|
||||
mSelectStrokeColor.setOnClickListener(v -> {
|
||||
mColorSelector.setAlphaEnabled(false);
|
||||
@@ -524,18 +624,18 @@ public class EditControlPopup {
|
||||
});
|
||||
}
|
||||
|
||||
private float safeParseFloat(String string){
|
||||
private float safeParseFloat(String string) {
|
||||
float out = -1; // -1
|
||||
try {
|
||||
out = Float.parseFloat(string);
|
||||
}catch (NumberFormatException e){
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e("EditControlPopup", e.toString());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public void setCurrentlyEditedButton(ControlInterface button){
|
||||
if(mCurrentlyEditedButton != null)
|
||||
public void setCurrentlyEditedButton(ControlInterface button) {
|
||||
if (mCurrentlyEditedButton != null)
|
||||
mCurrentlyEditedButton.getControlView().removeOnLayoutChangeListener(mLayoutChangedListener);
|
||||
mCurrentlyEditedButton = button;
|
||||
mCurrentlyEditedButton.getControlView().addOnLayoutChangeListener(mLayoutChangedListener);
|
||||
|
||||
@@ -1,195 +1,24 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Adapter;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.modloaders.FabricDownloadTask;
|
||||
import net.kdt.pojavlaunch.modloaders.FabricUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
|
||||
import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
|
||||
import net.kdt.pojavlaunch.profiles.VersionSelectorDialog;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
public class FabricInstallFragment extends FabriclikeInstallFragment {
|
||||
|
||||
public class FabricInstallFragment extends Fragment implements AdapterView.OnItemSelectedListener, ModloaderDownloadListener, Runnable {
|
||||
public static final String TAG = "FabricInstallTarget";
|
||||
public static final String TAG = "FabricInstallFragment";
|
||||
private static ModloaderListenerProxy sTaskProxy;
|
||||
private TextView mSelectedVersionLabel;
|
||||
private String mSelectedLoaderVersion;
|
||||
private Spinner mLoaderVersionSpinner;
|
||||
private String mSelectedGameVersion;
|
||||
private boolean mSelectedSnapshot;
|
||||
private ProgressBar mProgressBar;
|
||||
private File mDestinationDir;
|
||||
private Button mStartButton;
|
||||
private View mRetryView;
|
||||
|
||||
public FabricInstallFragment() {
|
||||
super(R.layout.fragment_fabric_install);
|
||||
super(FabriclikeUtils.FABRIC_UTILS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
this.mDestinationDir = new File(Tools.DIR_CACHE, "fabric-installer");
|
||||
protected ModloaderListenerProxy getListenerProxy() {
|
||||
return sTaskProxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mStartButton = view.findViewById(R.id.fabric_installer_start_button);
|
||||
mStartButton.setOnClickListener(this::onClickStart);
|
||||
mSelectedVersionLabel = view.findViewById(R.id.fabric_installer_version_select_label);
|
||||
view.findViewById(R.id.fabric_installer_game_version_change).setOnClickListener(this::onClickSelect);
|
||||
mLoaderVersionSpinner = view.findViewById(R.id.fabric_installer_loader_ver_spinner);
|
||||
mLoaderVersionSpinner.setOnItemSelectedListener(this);
|
||||
mProgressBar = view.findViewById(R.id.fabric_installer_progress_bar);
|
||||
mRetryView = view.findViewById(R.id.fabric_installer_retry_layout);
|
||||
view.findViewById(R.id.fabric_installer_retry_button).setOnClickListener(this::onClickRetry);
|
||||
if(sTaskProxy != null) {
|
||||
mStartButton.setEnabled(false);
|
||||
sTaskProxy.attachListener(this);
|
||||
}
|
||||
new Thread(this).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if(sTaskProxy != null) {
|
||||
sTaskProxy.detachListener();
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickStart(View v) {
|
||||
if(ProgressKeeper.hasOngoingTasks()) {
|
||||
Toast.makeText(v.getContext(), R.string.tasks_ongoing, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
sTaskProxy = new ModloaderListenerProxy();
|
||||
FabricDownloadTask fabricDownloadTask = new FabricDownloadTask(sTaskProxy, mDestinationDir);
|
||||
sTaskProxy.attachListener(this);
|
||||
mStartButton.setEnabled(false);
|
||||
new Thread(fabricDownloadTask).start();
|
||||
}
|
||||
|
||||
private void onClickSelect(View v) {
|
||||
VersionSelectorDialog.open(v.getContext(), true, (id, snapshot)->{
|
||||
mSelectedGameVersion = id;
|
||||
mSelectedVersionLabel.setText(mSelectedGameVersion);
|
||||
mSelectedSnapshot = snapshot;
|
||||
if(mSelectedLoaderVersion != null && sTaskProxy == null) mStartButton.setEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
private void onClickRetry(View v) {
|
||||
mLoaderVersionSpinner.setAdapter(null);
|
||||
mStartButton.setEnabled(false);
|
||||
mProgressBar.setVisibility(View.VISIBLE);
|
||||
mRetryView.setVisibility(View.GONE);
|
||||
new Thread(this).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
Adapter adapter = adapterView.getAdapter();
|
||||
mSelectedLoaderVersion = (String) adapter.getItem(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
mSelectedLoaderVersion = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadFinished(File downloadedFile) {
|
||||
Tools.runOnUiThread(()->{
|
||||
Context context = requireContext();
|
||||
sTaskProxy.detachListener();
|
||||
sTaskProxy = null;
|
||||
mStartButton.setEnabled(true);
|
||||
// This works because the due to the fact that we have transitioned here
|
||||
// without adding a transaction to the back stack, which caused the previous
|
||||
// transaction to be amended (i guess?? thats how the back stack dump looks like)
|
||||
// we can get back to the main fragment with just one back stack pop.
|
||||
// For some reason that amendment causes the transaction to lose its tag
|
||||
// so we cant use the tag here.
|
||||
getParentFragmentManager().popBackStackImmediate();
|
||||
Intent intent = new Intent(context, JavaGUILauncherActivity.class);
|
||||
FabricUtils.addAutoInstallArgs(intent, downloadedFile, mSelectedGameVersion, mSelectedLoaderVersion, mSelectedSnapshot, true);
|
||||
context.startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataNotAvailable() {
|
||||
Tools.runOnUiThread(()->{
|
||||
Context context = requireContext();
|
||||
sTaskProxy.detachListener();
|
||||
sTaskProxy = null;
|
||||
mStartButton.setEnabled(true);
|
||||
Tools.dialog(context,
|
||||
context.getString(R.string.global_error),
|
||||
context.getString(R.string.fabric_dl_cant_read_meta));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadError(Exception e) {
|
||||
Tools.runOnUiThread(()-> {
|
||||
Context context = requireContext();
|
||||
sTaskProxy.detachListener();
|
||||
sTaskProxy = null;
|
||||
mStartButton.setEnabled(true);
|
||||
Tools.showError(context, e);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
List<String> mLoaderVersions = FabricUtils.downloadLoaderVersionList(false);
|
||||
if (mLoaderVersions != null) {
|
||||
Tools.runOnUiThread(()->{
|
||||
Context context = getContext();
|
||||
if(context == null) return;
|
||||
ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(context, R.layout.support_simple_spinner_dropdown_item, mLoaderVersions);
|
||||
mLoaderVersionSpinner.setAdapter(arrayAdapter);
|
||||
mProgressBar.setVisibility(View.GONE);
|
||||
});
|
||||
}else{
|
||||
Tools.runOnUiThread(()-> {
|
||||
mRetryView.setVisibility(View.VISIBLE);
|
||||
mProgressBar.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
}catch (IOException e) {
|
||||
Tools.runOnUiThread(()-> {
|
||||
if(getContext() != null) Tools.showError(getContext(), e);
|
||||
mRetryView.setVisibility(View.VISIBLE);
|
||||
mProgressBar.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
protected void setListenerProxy(ModloaderListenerProxy listenerProxy) {
|
||||
sTaskProxy = listenerProxy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import net.kdt.pojavlaunch.PojavApplication;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.modloaders.FabriclikeDownloadTask;
|
||||
import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.FabricVersion;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.SelfReferencingFuture;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public abstract class FabriclikeInstallFragment extends Fragment implements ModloaderDownloadListener, CompoundButton.OnCheckedChangeListener {
|
||||
private final FabriclikeUtils mFabriclikeUtils;
|
||||
private Spinner mGameVersionSpinner;
|
||||
private FabricVersion[] mGameVersionArray;
|
||||
private Future<?> mGameVersionFuture;
|
||||
private String mSelectedGameVersion;
|
||||
private Spinner mLoaderVersionSpinner;
|
||||
private FabricVersion[] mLoaderVersionArray;
|
||||
private Future<?> mLoaderVersionFuture;
|
||||
private String mSelectedLoaderVersion;
|
||||
private ProgressBar mProgressBar;
|
||||
private Button mStartButton;
|
||||
private View mRetryView;
|
||||
private CheckBox mOnlyStableCheckbox;
|
||||
protected FabriclikeInstallFragment(FabriclikeUtils mFabriclikeUtils) {
|
||||
super(R.layout.fragment_fabric_install);
|
||||
this.mFabriclikeUtils = mFabriclikeUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mStartButton = view.findViewById(R.id.fabric_installer_start_button);
|
||||
mStartButton.setOnClickListener(this::onClickStart);
|
||||
mGameVersionSpinner = view.findViewById(R.id.fabric_installer_game_ver_spinner);
|
||||
mGameVersionSpinner.setOnItemSelectedListener(new GameVersionSelectedListener());
|
||||
mLoaderVersionSpinner = view.findViewById(R.id.fabric_installer_loader_ver_spinner);
|
||||
mLoaderVersionSpinner.setOnItemSelectedListener(new LoaderVersionSelectedListener());
|
||||
mProgressBar = view.findViewById(R.id.fabric_installer_progress_bar);
|
||||
mRetryView = view.findViewById(R.id.fabric_installer_retry_layout);
|
||||
mOnlyStableCheckbox = view.findViewById(R.id.fabric_installer_only_stable_checkbox);
|
||||
mOnlyStableCheckbox.setOnCheckedChangeListener(this);
|
||||
view.findViewById(R.id.fabric_installer_retry_button).setOnClickListener(this::onClickRetry);
|
||||
((TextView)view.findViewById(R.id.fabric_installer_label_loader_ver)).setText(getString(R.string.fabric_dl_loader_version, mFabriclikeUtils.getName()));
|
||||
ModloaderListenerProxy proxy = getListenerProxy();
|
||||
if(proxy != null) {
|
||||
mStartButton.setEnabled(false);
|
||||
proxy.attachListener(this);
|
||||
}
|
||||
updateGameVersions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
cancelFutureChecked(mGameVersionFuture);
|
||||
cancelFutureChecked(mLoaderVersionFuture);
|
||||
ModloaderListenerProxy proxy = getListenerProxy();
|
||||
if(proxy != null) {
|
||||
proxy.detachListener();
|
||||
}
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private void onClickStart(View v) {
|
||||
if(ProgressKeeper.hasOngoingTasks()) {
|
||||
Toast.makeText(v.getContext(), R.string.tasks_ongoing, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
ModloaderListenerProxy proxy = new ModloaderListenerProxy();
|
||||
FabriclikeDownloadTask fabricDownloadTask = new FabriclikeDownloadTask(proxy, mFabriclikeUtils,
|
||||
mSelectedGameVersion, mSelectedLoaderVersion, true);
|
||||
proxy.attachListener(this);
|
||||
setListenerProxy(proxy);
|
||||
mStartButton.setEnabled(false);
|
||||
new Thread(fabricDownloadTask).start();
|
||||
}
|
||||
|
||||
private void onClickRetry(View v) {
|
||||
mStartButton.setEnabled(false);
|
||||
mRetryView.setVisibility(View.GONE);
|
||||
mLoaderVersionSpinner.setAdapter(null);
|
||||
if(mGameVersionArray == null) {
|
||||
mGameVersionSpinner.setAdapter(null);
|
||||
updateGameVersions();
|
||||
return;
|
||||
}
|
||||
updateLoaderVersions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadFinished(File downloadedFile) {
|
||||
Tools.runOnUiThread(()->{
|
||||
|
||||
getListenerProxy().detachListener();
|
||||
setListenerProxy(null);
|
||||
mStartButton.setEnabled(true);
|
||||
// This works because the due to the fact that we have transitioned here
|
||||
// without adding a transaction to the back stack, which caused the previous
|
||||
// transaction to be amended (i guess?? thats how the back stack dump looks like)
|
||||
// we can get back to the main fragment with just one back stack pop.
|
||||
// For some reason that amendment causes the transaction to lose its tag
|
||||
// so we cant use the tag here.
|
||||
getParentFragmentManager().popBackStackImmediate();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataNotAvailable() {
|
||||
Tools.runOnUiThread(()->{
|
||||
Context context = requireContext();
|
||||
getListenerProxy().detachListener();
|
||||
setListenerProxy(null);
|
||||
mStartButton.setEnabled(true);
|
||||
Tools.dialog(context,
|
||||
context.getString(R.string.global_error),
|
||||
context.getString(R.string.fabric_dl_cant_read_meta, mFabriclikeUtils.getName()));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadError(Exception e) {
|
||||
Tools.runOnUiThread(()-> {
|
||||
Context context = requireContext();
|
||||
getListenerProxy().detachListener();
|
||||
setListenerProxy(null);
|
||||
mStartButton.setEnabled(true);
|
||||
Tools.showError(context, e);
|
||||
});
|
||||
}
|
||||
|
||||
private void cancelFutureChecked(Future<?> future) {
|
||||
if(future != null && !future.isCancelled()) future.cancel(true);
|
||||
}
|
||||
|
||||
private void startLoading() {
|
||||
mProgressBar.setVisibility(View.VISIBLE);
|
||||
mStartButton.setEnabled(false);
|
||||
}
|
||||
|
||||
private void stopLoading() {
|
||||
mProgressBar.setVisibility(View.GONE);
|
||||
// The "visibility on" is managed by the spinners
|
||||
}
|
||||
|
||||
private ArrayAdapter<FabricVersion> createAdapter(FabricVersion[] fabricVersions, boolean onlyStable) {
|
||||
ArrayList<FabricVersion> filteredVersions = new ArrayList<>(fabricVersions.length);
|
||||
for(FabricVersion fabricVersion : fabricVersions) {
|
||||
if(!onlyStable || fabricVersion.stable) filteredVersions.add(fabricVersion);
|
||||
}
|
||||
filteredVersions.trimToSize();
|
||||
return new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_dropdown_item, filteredVersions);
|
||||
}
|
||||
|
||||
private void onException(Future<?> myFuture, Exception e) {
|
||||
Tools.runOnUiThread(()->{
|
||||
if(myFuture.isCancelled()) return;
|
||||
stopLoading();
|
||||
if(e != null) Tools.showError(requireContext(), e);
|
||||
mRetryView.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
updateGameSpinner();
|
||||
updateLoaderSpinner();
|
||||
}
|
||||
|
||||
class LoaderVersionSelectedListener implements AdapterView.OnItemSelectedListener {
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
mSelectedLoaderVersion = ((FabricVersion) adapterView.getAdapter().getItem(i)).version;
|
||||
mStartButton.setEnabled(mSelectedGameVersion != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
mSelectedLoaderVersion = null;
|
||||
mStartButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
class LoadLoaderVersionsTask implements SelfReferencingFuture.FutureInterface {
|
||||
@Override
|
||||
public void run(Future<?> myFuture) {
|
||||
Log.i("LoadLoaderVersions", "Starting...");
|
||||
try {
|
||||
mLoaderVersionArray = mFabriclikeUtils.downloadLoaderVersions(mSelectedGameVersion);
|
||||
if(mLoaderVersionArray != null) onFinished(myFuture);
|
||||
else onException(myFuture, null);
|
||||
}catch (IOException e) {
|
||||
onException(myFuture, e);
|
||||
}
|
||||
}
|
||||
private void onFinished(Future<?> myFuture) {
|
||||
Tools.runOnUiThread(()->{
|
||||
if(myFuture.isCancelled()) return;
|
||||
stopLoading();
|
||||
updateLoaderSpinner();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLoaderVersions() {
|
||||
startLoading();
|
||||
mLoaderVersionFuture = new SelfReferencingFuture(new LoadLoaderVersionsTask()).startOnExecutor(PojavApplication.sExecutorService);
|
||||
}
|
||||
|
||||
private void updateLoaderSpinner() {
|
||||
mLoaderVersionSpinner.setAdapter(createAdapter(mLoaderVersionArray, mOnlyStableCheckbox.isChecked()));
|
||||
}
|
||||
|
||||
class GameVersionSelectedListener implements AdapterView.OnItemSelectedListener {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
mSelectedGameVersion = ((FabricVersion) adapterView.getAdapter().getItem(i)).version;
|
||||
cancelFutureChecked(mLoaderVersionFuture);
|
||||
updateLoaderVersions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
mSelectedGameVersion = null;
|
||||
if(mLoaderVersionFuture != null) mLoaderVersionFuture.cancel(true);
|
||||
adapterView.setAdapter(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LoadGameVersionsTask implements SelfReferencingFuture.FutureInterface {
|
||||
@Override
|
||||
public void run(Future<?> myFuture) {
|
||||
try {
|
||||
mGameVersionArray = mFabriclikeUtils.downloadGameVersions();
|
||||
if(mGameVersionArray != null) onFinished(myFuture);
|
||||
else onException(myFuture, null);
|
||||
}catch (IOException e) {
|
||||
onException(myFuture, e);
|
||||
}
|
||||
}
|
||||
private void onFinished(Future<?> myFuture) {
|
||||
Tools.runOnUiThread(()->{
|
||||
if(myFuture.isCancelled()) return;
|
||||
stopLoading();
|
||||
updateGameSpinner();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGameVersions() {
|
||||
startLoading();
|
||||
mGameVersionFuture = new SelfReferencingFuture(new LoadGameVersionsTask()).startOnExecutor(PojavApplication.sExecutorService);
|
||||
}
|
||||
|
||||
private void updateGameSpinner() {
|
||||
mGameVersionSpinner.setAdapter(createAdapter(mGameVersionArray, mOnlyStableCheckbox.isChecked()));
|
||||
}
|
||||
|
||||
protected abstract ModloaderListenerProxy getListenerProxy();
|
||||
protected abstract void setListenerProxy(ModloaderListenerProxy listenerProxy);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public class ForgeInstallFragment extends ModVersionListFragment<List<String>> {
|
||||
|
||||
@Override
|
||||
public Runnable createDownloadTask(Object selectedVersion, ModloaderListenerProxy listenerProxy) {
|
||||
return new ForgeDownloadTask(listenerProxy, (String) selectedVersion, new File(Tools.DIR_CACHE, "forge-installer.jar"));
|
||||
return new ForgeDownloadTask(listenerProxy, (String) selectedVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,16 +13,21 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.kdt.mcgui.mcVersionSpinner;
|
||||
|
||||
import net.kdt.pojavlaunch.CustomControlsActivity;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.extra.ExtraConstants;
|
||||
import net.kdt.pojavlaunch.extra.ExtraCore;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
|
||||
|
||||
public class MainMenuFragment extends Fragment {
|
||||
public static final String TAG = "MainMenuFragment";
|
||||
|
||||
private mcVersionSpinner mVersionSpinner;
|
||||
|
||||
public MainMenuFragment(){
|
||||
super(R.layout.fragment_launcher);
|
||||
}
|
||||
@@ -36,6 +41,7 @@ public class MainMenuFragment extends Fragment {
|
||||
|
||||
ImageButton mEditProfileButton = view.findViewById(R.id.edit_profile_button);
|
||||
Button mPlayButton = view.findViewById(R.id.play_button);
|
||||
mVersionSpinner = view.findViewById(R.id.mc_version_spinner);
|
||||
|
||||
mNewsButton.setOnClickListener(v -> Tools.openURL(requireActivity(), Tools.URL_HOME));
|
||||
mCustomControlButton.setOnClickListener(v -> startActivity(new Intent(requireContext(), CustomControlsActivity.class)));
|
||||
@@ -51,10 +57,17 @@ public class MainMenuFragment extends Fragment {
|
||||
mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext()));
|
||||
|
||||
mNewsButton.setOnLongClickListener((v)->{
|
||||
Tools.swapFragment(requireActivity(), FabricInstallFragment.class, FabricInstallFragment.TAG, true, null);
|
||||
Tools.swapFragment(requireActivity(), SearchModFragment.class, SearchModFragment.TAG, true, null);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mVersionSpinner.reloadProfiles();
|
||||
}
|
||||
|
||||
private void runInstallerWithConfirmation(boolean isCustomArgs) {
|
||||
if (ProgressKeeper.getTaskCount() == 0)
|
||||
Tools.installMod(requireActivity(), isCustomArgs);
|
||||
|
||||
@@ -58,10 +58,10 @@ public abstract class ModVersionListFragment<T> extends Fragment implements Runn
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
public void onStop() {
|
||||
ModloaderListenerProxy taskProxy = getTaskProxy();
|
||||
if(taskProxy != null) taskProxy.detachListener();
|
||||
super.onDestroyView();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -51,8 +51,7 @@ public class OptiFineInstallFragment extends ModVersionListFragment<OptiFineUtil
|
||||
|
||||
@Override
|
||||
public Runnable createDownloadTask(Object selectedVersion, ModloaderListenerProxy listenerProxy) {
|
||||
return new OptiFineDownloadTask((OptiFineUtils.OptiFineVersion) selectedVersion,
|
||||
new File(Tools.DIR_CACHE, "optifine-installer.jar"), listenerProxy);
|
||||
return new OptiFineDownloadTask((OptiFineUtils.OptiFineVersion) selectedVersion, listenerProxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,7 +31,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ProfileEditorFragment extends Fragment {
|
||||
public static final String TAG = "ProfileEditorFragment";
|
||||
@@ -75,7 +74,7 @@ public class ProfileEditorFragment extends Fragment {
|
||||
List<String> renderList = new ArrayList<>(5);
|
||||
Collections.addAll(renderList, getResources().getStringArray(R.array.renderer));
|
||||
renderList.add("Default");
|
||||
mDefaultRenderer.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, renderList));
|
||||
mDefaultRenderer.setAdapter(new ArrayAdapter<>(getContext(), R.layout.item_simple_list_1, renderList));
|
||||
|
||||
// Set up behaviors
|
||||
mSaveButton.setOnClickListener(v -> {
|
||||
@@ -84,9 +83,12 @@ public class ProfileEditorFragment extends Fragment {
|
||||
});
|
||||
|
||||
mDeleteButton.setOnClickListener(v -> {
|
||||
LauncherProfiles.mainProfileJson.profiles.remove(mProfileKey);
|
||||
LauncherProfiles.update();
|
||||
ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, DELETED_PROFILE);
|
||||
if(LauncherProfiles.mainProfileJson.profiles.size() > 1){
|
||||
LauncherProfiles.mainProfileJson.profiles.remove(mProfileKey);
|
||||
LauncherProfiles.write();
|
||||
ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, DELETED_PROFILE);
|
||||
}
|
||||
|
||||
Tools.removeCurrentFragment(requireActivity());
|
||||
});
|
||||
|
||||
@@ -160,11 +162,7 @@ public class ProfileEditorFragment extends Fragment {
|
||||
mProfileKey = profile;
|
||||
}else{
|
||||
minecraftProfile = MinecraftProfile.createTemplate();
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
while(LauncherProfiles.mainProfileJson.profiles.containsKey(uuid)) {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
}
|
||||
mProfileKey = uuid;
|
||||
mProfileKey = LauncherProfiles.getFreeProfileKey();
|
||||
}
|
||||
return minecraftProfile;
|
||||
}
|
||||
@@ -208,7 +206,7 @@ public class ProfileEditorFragment extends Fragment {
|
||||
|
||||
|
||||
LauncherProfiles.mainProfileJson.profiles.put(mProfileKey, mTempProfile);
|
||||
LauncherProfiles.update();
|
||||
LauncherProfiles.write();
|
||||
ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_SPINNER, mProfileKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,9 @@ public class ProfileTypeSelectFragment extends Fragment {
|
||||
Tools.swapFragment(requireActivity(), FabricInstallFragment.class, FabricInstallFragment.TAG, false, null));
|
||||
view.findViewById(R.id.modded_profile_forge).setOnClickListener((v)->
|
||||
Tools.swapFragment(requireActivity(), ForgeInstallFragment.class, ForgeInstallFragment.TAG, false, null));
|
||||
view.findViewById(R.id.modded_profile_modpack).setOnClickListener((v)->
|
||||
Tools.swapFragment(requireActivity(), SearchModFragment.class, SearchModFragment.TAG, false, null));
|
||||
view.findViewById(R.id.modded_profile_quilt).setOnClickListener((v)->
|
||||
Tools.swapFragment(requireActivity(), QuiltInstallFragment.class, QuiltInstallFragment.TAG, false, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
|
||||
|
||||
public class QuiltInstallFragment extends FabriclikeInstallFragment {
|
||||
|
||||
public static final String TAG = "QuiltInstallFragment";
|
||||
private static ModloaderListenerProxy sTaskProxy;
|
||||
|
||||
public QuiltInstallFragment() {
|
||||
super(FabriclikeUtils.QUILT_UTILS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ModloaderListenerProxy getListenerProxy() {
|
||||
return sTaskProxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setListenerProxy(ModloaderListenerProxy listenerProxy) {
|
||||
sTaskProxy = listenerProxy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package net.kdt.pojavlaunch.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.math.MathUtils;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
|
||||
import net.kdt.pojavlaunch.profiles.VersionSelectorDialog;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
|
||||
public class SearchModFragment extends Fragment implements ModItemAdapter.SearchResultCallback {
|
||||
|
||||
public static final String TAG = "SearchModFragment";
|
||||
private View mOverlay;
|
||||
private float mOverlayTopCache; // Padding cache reduce resource lookup
|
||||
|
||||
private final RecyclerView.OnScrollListener mOverlayPositionListener = new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
mOverlay.setY(MathUtils.clamp(mOverlay.getY() - dy, -mOverlay.getHeight(), mOverlayTopCache));
|
||||
}
|
||||
};
|
||||
|
||||
private EditText mSearchEditText;
|
||||
private ImageButton mFilterButton;
|
||||
private RecyclerView mRecyclerview;
|
||||
private ModItemAdapter mModItemAdapter;
|
||||
private ProgressBar mSearchProgressBar;
|
||||
private TextView mStatusTextView;
|
||||
private ColorStateList mDefaultTextColor;
|
||||
|
||||
private ModpackApi modpackApi;
|
||||
|
||||
private final SearchFilters mSearchFilters;
|
||||
|
||||
public SearchModFragment(){
|
||||
super(R.layout.fragment_mod_search);
|
||||
mSearchFilters = new SearchFilters();
|
||||
mSearchFilters.isModpack = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
modpackApi = new CommonApi(context.getString(R.string.curseforge_api_key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
// You can only access resources after attaching to current context
|
||||
mModItemAdapter = new ModItemAdapter(getResources(), modpackApi, this);
|
||||
ProgressKeeper.addTaskCountListener(mModItemAdapter);
|
||||
mOverlayTopCache = getResources().getDimension(R.dimen.fragment_padding_medium);
|
||||
|
||||
mOverlay = view.findViewById(R.id.search_mod_overlay);
|
||||
mSearchEditText = view.findViewById(R.id.search_mod_edittext);
|
||||
mSearchProgressBar = view.findViewById(R.id.search_mod_progressbar);
|
||||
mRecyclerview = view.findViewById(R.id.search_mod_list);
|
||||
mStatusTextView = view.findViewById(R.id.search_mod_status_text);
|
||||
mFilterButton = view.findViewById(R.id.search_mod_filter);
|
||||
|
||||
mDefaultTextColor = mStatusTextView.getTextColors();
|
||||
|
||||
mRecyclerview.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
mRecyclerview.setAdapter(mModItemAdapter);
|
||||
|
||||
mRecyclerview.addOnScrollListener(mOverlayPositionListener);
|
||||
|
||||
mSearchEditText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
mSearchProgressBar.setVisibility(View.VISIBLE);
|
||||
mSearchFilters.name = mSearchEditText.getText().toString();
|
||||
mModItemAdapter.performSearchQuery(mSearchFilters);
|
||||
return true;
|
||||
});
|
||||
|
||||
mOverlay.post(()->{
|
||||
int overlayHeight = mOverlay.getHeight();
|
||||
mRecyclerview.setPadding(mRecyclerview.getPaddingLeft(),
|
||||
mRecyclerview.getPaddingTop() + overlayHeight,
|
||||
mRecyclerview.getPaddingRight(),
|
||||
mRecyclerview.getPaddingBottom());
|
||||
});
|
||||
mFilterButton.setOnClickListener(v -> displayFilterDialog());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
ProgressKeeper.removeTaskCountListener(mModItemAdapter);
|
||||
mRecyclerview.removeOnScrollListener(mOverlayPositionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchFinished() {
|
||||
mSearchProgressBar.setVisibility(View.GONE);
|
||||
mStatusTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchError(int error) {
|
||||
mSearchProgressBar.setVisibility(View.GONE);
|
||||
mStatusTextView.setVisibility(View.VISIBLE);
|
||||
switch (error) {
|
||||
case ERROR_INTERNAL:
|
||||
mStatusTextView.setTextColor(Color.RED);
|
||||
mStatusTextView.setText(R.string.search_modpack_error);
|
||||
break;
|
||||
case ERROR_NO_RESULTS:
|
||||
mStatusTextView.setTextColor(mDefaultTextColor);
|
||||
mStatusTextView.setText(R.string.search_modpack_no_result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void displayFilterDialog() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(requireContext())
|
||||
.setView(R.layout.dialog_mod_filters)
|
||||
.create();
|
||||
|
||||
// setup the view behavior
|
||||
dialog.setOnShowListener(dialogInterface -> {
|
||||
TextView mSelectedVersion = dialog.findViewById(R.id.search_mod_selected_mc_version_textview);
|
||||
Button mSelectVersionButton = dialog.findViewById(R.id.search_mod_mc_version_button);
|
||||
Button mApplyButton = dialog.findViewById(R.id.search_mod_apply_filters);
|
||||
|
||||
assert mSelectVersionButton != null;
|
||||
assert mSelectedVersion != null;
|
||||
assert mApplyButton != null;
|
||||
|
||||
// Setup the expendable list behavior
|
||||
mSelectVersionButton.setOnClickListener(v -> VersionSelectorDialog.open(v.getContext(), true, (id, snapshot)-> mSelectedVersion.setText(id)));
|
||||
|
||||
// Apply visually all the current settings
|
||||
mSelectedVersion.setText(mSearchFilters.mcVersion);
|
||||
|
||||
// Apply the new settings
|
||||
mApplyButton.setOnClickListener(v -> {
|
||||
mSearchFilters.mcVersion = mSelectedVersion.getText().toString();
|
||||
dialogInterface.dismiss();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FabricDownloadTask implements Runnable, Tools.DownloaderFeedback{
|
||||
private final File mDestinationDir;
|
||||
private final File mDestinationFile;
|
||||
private final ModloaderDownloadListener mModloaderDownloadListener;
|
||||
|
||||
public FabricDownloadTask(ModloaderDownloadListener modloaderDownloadListener, File mDestinationDir) {
|
||||
this.mModloaderDownloadListener = modloaderDownloadListener;
|
||||
this.mDestinationDir = mDestinationDir;
|
||||
this.mDestinationFile = new File(mDestinationDir, "fabric-installer.jar");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.fabric_dl_progress);
|
||||
try {
|
||||
if(runCatching()) mModloaderDownloadListener.onDownloadFinished(mDestinationFile);
|
||||
}catch (IOException e) {
|
||||
mModloaderDownloadListener.onDownloadError(e);
|
||||
}
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1);
|
||||
}
|
||||
|
||||
private boolean runCatching() throws IOException {
|
||||
if(!mDestinationDir.exists() && !mDestinationDir.mkdirs()) throw new IOException("Failed to create cache directory");
|
||||
String[] urlAndVersion = FabricUtils.getInstallerUrlAndVersion();
|
||||
if(urlAndVersion == null) {
|
||||
mModloaderDownloadListener.onDataNotAvailable();
|
||||
return false;
|
||||
}
|
||||
File versionFile = new File(mDestinationDir, "fabric-installer-version");
|
||||
boolean shouldDownloadInstaller = true;
|
||||
if(urlAndVersion[1] != null && versionFile.canRead()) { // if we know the latest version that we have and the server has
|
||||
try {
|
||||
shouldDownloadInstaller = !urlAndVersion[1].equals(Tools.read(versionFile.getAbsolutePath()));
|
||||
}catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if(shouldDownloadInstaller) {
|
||||
if (urlAndVersion[0] != null) {
|
||||
byte[] buffer = new byte[8192];
|
||||
DownloadUtils.downloadFileMonitored(urlAndVersion[0], mDestinationFile, buffer, this);
|
||||
if(urlAndVersion[1] != null) {
|
||||
try {
|
||||
Tools.write(versionFile.getAbsolutePath(), urlAndVersion[1]);
|
||||
}catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
mModloaderDownloadListener.onDataNotAvailable();
|
||||
return false;
|
||||
}
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(int curr, int max) {
|
||||
int progress100 = (int)(((float)curr / (float)max)*100f);
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.fabric_dl_progress);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FabricUtils {
|
||||
private static final String FABRIC_INSTALLER_METADATA_URL = "https://meta.fabricmc.net/v2/versions/installer";
|
||||
private static final String FABRIC_LOADER_METADATA_URL = "https://meta.fabricmc.net/v2/versions/loader";
|
||||
public static List<String> downloadLoaderVersionList(boolean onlyStable) throws IOException {
|
||||
try {
|
||||
return DownloadUtils.downloadStringCached(FABRIC_LOADER_METADATA_URL,
|
||||
"fabric_loader_versions", (input)->{
|
||||
final List<String> loaderList = new ArrayList<>();
|
||||
try {
|
||||
enumerateMetadata(input, (object) -> {
|
||||
if (onlyStable && !object.getBoolean("stable")) return false;
|
||||
loaderList.add(object.getString("version"));
|
||||
return false;
|
||||
});
|
||||
}catch (JSONException e) {
|
||||
throw new DownloadUtils.ParseException(e);
|
||||
}
|
||||
return loaderList;
|
||||
});
|
||||
}catch (DownloadUtils.ParseException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getInstallerUrlAndVersion() throws IOException{
|
||||
String installerMetadata = DownloadUtils.downloadString(FABRIC_INSTALLER_METADATA_URL);
|
||||
try {
|
||||
return DownloadUtils.downloadStringCached(FABRIC_INSTALLER_METADATA_URL,
|
||||
"fabric_installer_versions", input -> {
|
||||
try {
|
||||
JSONObject selectedMetadata = enumerateMetadata(installerMetadata, (object) ->
|
||||
object.getBoolean("stable"));
|
||||
if (selectedMetadata == null) return null;
|
||||
return new String[]{selectedMetadata.getString("url"),
|
||||
selectedMetadata.getString("version")};
|
||||
} catch (JSONException e) {
|
||||
throw new DownloadUtils.ParseException(e);
|
||||
}
|
||||
});
|
||||
}catch (DownloadUtils.ParseException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void addAutoInstallArgs(Intent intent, File modInstalllerJar,
|
||||
String gameVersion, String loaderVersion,
|
||||
boolean isSnapshot, boolean createProfile) {
|
||||
intent.putExtra("javaArgs", "-jar " + modInstalllerJar.getAbsolutePath() + " client -dir "+ Tools.DIR_GAME_NEW
|
||||
+ " -mcversion "+gameVersion +" -loader "+loaderVersion +
|
||||
(isSnapshot ? " -snapshot" : "") +
|
||||
(createProfile ? "" : " -noprofile"));
|
||||
intent.putExtra("openLogOutput", true);
|
||||
|
||||
}
|
||||
|
||||
private static JSONObject enumerateMetadata(String inputMetadata, FabricMetaReader metaReader) throws JSONException{
|
||||
JSONArray fullMetadata = new JSONArray(inputMetadata);
|
||||
JSONObject metadataObject = null;
|
||||
for(int i = 0; i < fullMetadata.length(); i++) {
|
||||
metadataObject = fullMetadata.getJSONObject(i);
|
||||
if(metaReader.processMetadata(metadataObject)) return metadataObject;
|
||||
}
|
||||
return metadataObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class FabricVersion {
|
||||
public String version;
|
||||
public boolean stable;
|
||||
|
||||
public static class LoaderDescriptor extends FabricVersion {
|
||||
public FabricVersion loader;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return loader != null ? loader.toString() : "null";
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FabriclikeDownloadTask implements Runnable, Tools.DownloaderFeedback{
|
||||
private final ModloaderDownloadListener mModloaderDownloadListener;
|
||||
private final FabriclikeUtils mUtils;
|
||||
private final String mGameVersion;
|
||||
private final String mLoaderVersion;
|
||||
private final boolean mCreateProfile;
|
||||
public FabriclikeDownloadTask(ModloaderDownloadListener modloaderDownloadListener, FabriclikeUtils utils, String mGameVersion, String mLoaderVersion, boolean mCreateProfile) {
|
||||
this.mModloaderDownloadListener = modloaderDownloadListener;
|
||||
this.mUtils = utils;
|
||||
this.mGameVersion = mGameVersion;
|
||||
this.mLoaderVersion = mLoaderVersion;
|
||||
this.mCreateProfile = mCreateProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.fabric_dl_progress);
|
||||
try {
|
||||
if(runCatching()) mModloaderDownloadListener.onDownloadFinished(null);
|
||||
else mModloaderDownloadListener.onDataNotAvailable();
|
||||
}catch (IOException e) {
|
||||
mModloaderDownloadListener.onDownloadError(e);
|
||||
}
|
||||
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
|
||||
}
|
||||
|
||||
private boolean runCatching() throws IOException{
|
||||
String fabricJson = DownloadUtils.downloadString(mUtils.createJsonDownloadUrl(mGameVersion, mLoaderVersion));
|
||||
String versionId;
|
||||
try {
|
||||
JSONObject fabricJsonObject = new JSONObject(fabricJson);
|
||||
versionId = fabricJsonObject.getString("id");
|
||||
}catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
File versionJsonDir = new File(Tools.DIR_HOME_VERSION, versionId);
|
||||
File versionJsonFile = new File(versionJsonDir, versionId+".json");
|
||||
if(versionJsonDir.isFile()) throw new IOException("Version destination directory is a file!");
|
||||
if(!versionJsonDir.exists() && !versionJsonDir.mkdirs()) throw new IOException("Failed to create version directory");
|
||||
Tools.write(versionJsonFile.getAbsolutePath(), fabricJson);
|
||||
if(mCreateProfile) {
|
||||
LauncherProfiles.load();
|
||||
MinecraftProfile fabricProfile = new MinecraftProfile();
|
||||
fabricProfile.lastVersionId = versionId;
|
||||
fabricProfile.name = mUtils.getName();
|
||||
fabricProfile.icon = mUtils.getIconName();
|
||||
LauncherProfiles.insertMinecraftProfile(fabricProfile);
|
||||
LauncherProfiles.write();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(int curr, int max) {
|
||||
int progress100 = (int)(((float)curr / (float)max)*100f);
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.fabric_dl_progress, mUtils.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package net.kdt.pojavlaunch.modloaders;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
public class FabriclikeUtils {
|
||||
|
||||
public static final FabriclikeUtils FABRIC_UTILS = new FabriclikeUtils("https://meta.fabricmc.net/v2", "fabric", "Fabric", "fabric");
|
||||
public static final FabriclikeUtils QUILT_UTILS = new FabriclikeUtils("https://meta.quiltmc.org/v3", "quilt", "Quilt", "quilt");
|
||||
|
||||
private static final String LOADER_METADATA_URL = "%s/versions/loader/%s";
|
||||
private static final String GAME_METADATA_URL = "%s/versions/game";
|
||||
|
||||
private static final String JSON_DOWNLOAD_URL = "%s/versions/loader/%s/%s/profile/json";
|
||||
|
||||
private final String mApiUrl;
|
||||
private final String mCachePrefix;
|
||||
private final String mName;
|
||||
private final String mIconName;
|
||||
|
||||
private FabriclikeUtils(String mApiUrl, String cachePrefix, String mName, String iconName) {
|
||||
this.mApiUrl = mApiUrl;
|
||||
this.mCachePrefix = cachePrefix;
|
||||
this.mIconName = iconName;
|
||||
this.mName = mName;
|
||||
}
|
||||
|
||||
public FabricVersion[] downloadGameVersions() throws IOException{
|
||||
try {
|
||||
return DownloadUtils.downloadStringCached(String.format(GAME_METADATA_URL, mApiUrl), mCachePrefix+"_game_versions",
|
||||
FabriclikeUtils::deserializeRawVersions
|
||||
);
|
||||
}catch (DownloadUtils.ParseException ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
public FabricVersion[] downloadLoaderVersions(String gameVersion) throws IOException{
|
||||
try {
|
||||
String urlEncodedGameVersion = URLEncoder.encode(gameVersion, "UTF-8");
|
||||
return DownloadUtils.downloadStringCached(String.format(LOADER_METADATA_URL, mApiUrl, urlEncodedGameVersion),
|
||||
mCachePrefix+"_loader_versions."+urlEncodedGameVersion,
|
||||
(input)->{ try {
|
||||
return deserializeLoaderVersions(input);
|
||||
}catch (JSONException e) {
|
||||
throw new DownloadUtils.ParseException(e);
|
||||
}});
|
||||
|
||||
}catch (DownloadUtils.ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String createJsonDownloadUrl(String gameVersion, String loaderVersion) {
|
||||
try {
|
||||
gameVersion = URLEncoder.encode(gameVersion, "UTF-8");
|
||||
loaderVersion = URLEncoder.encode(loaderVersion, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return String.format(JSON_DOWNLOAD_URL, mApiUrl, gameVersion, loaderVersion);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
public String getIconName() {
|
||||
return mIconName;
|
||||
}
|
||||
|
||||
private static FabricVersion[] deserializeLoaderVersions(String input) throws JSONException {
|
||||
JSONArray jsonArray = new JSONArray(input);
|
||||
FabricVersion[] fabricVersions = new FabricVersion[jsonArray.length()];
|
||||
for(int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonObject = jsonArray.getJSONObject(i).getJSONObject("loader");
|
||||
FabricVersion fabricVersion = new FabricVersion();
|
||||
fabricVersion.version = jsonObject.getString("version");
|
||||
//Quilt has a skill issue and does not say which versions are stable or not
|
||||
if(jsonObject.has("stable")) {
|
||||
fabricVersion.stable = jsonObject.getBoolean("stable");
|
||||
} else {
|
||||
fabricVersion.stable = !fabricVersion.version.contains("beta");
|
||||
}
|
||||
fabricVersions[i] = fabricVersion;
|
||||
}
|
||||
return fabricVersions;
|
||||
}
|
||||
|
||||
private static FabricVersion[] deserializeRawVersions(String jsonArrayIn) throws DownloadUtils.ParseException {
|
||||
try {
|
||||
return Tools.GLOBAL_GSON.fromJson(jsonArrayIn, FabricVersion[].class);
|
||||
}catch (JsonSyntaxException e) {
|
||||
e.printStackTrace();
|
||||
throw new DownloadUtils.ParseException(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,39 +10,79 @@ import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class ForgeDownloadTask implements Runnable, Tools.DownloaderFeedback {
|
||||
private final String mForgeUrl;
|
||||
private final String mForgeVersion;
|
||||
private final File mDestinationFile;
|
||||
private String mDownloadUrl;
|
||||
private String mFullVersion;
|
||||
private String mLoaderVersion;
|
||||
private String mGameVersion;
|
||||
private final ModloaderDownloadListener mListener;
|
||||
public ForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion, File destinationFile) {
|
||||
public ForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion) {
|
||||
this.mListener = listener;
|
||||
this.mForgeUrl = ForgeUtils.getInstallerUrl(forgeVersion);
|
||||
this.mForgeVersion = forgeVersion;
|
||||
this.mDestinationFile = destinationFile;
|
||||
this.mDownloadUrl = ForgeUtils.getInstallerUrl(forgeVersion);
|
||||
this.mFullVersion = forgeVersion;
|
||||
}
|
||||
|
||||
public ForgeDownloadTask(ModloaderDownloadListener listener, String gameVersion, String loaderVersion) {
|
||||
this.mListener = listener;
|
||||
this.mLoaderVersion = loaderVersion;
|
||||
this.mGameVersion = gameVersion;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mForgeVersion);
|
||||
try {
|
||||
byte[] buffer = new byte[8192];
|
||||
DownloadUtils.downloadFileMonitored(mForgeUrl, mDestinationFile, buffer, this);
|
||||
mListener.onDownloadFinished(mDestinationFile);
|
||||
}catch (IOException e) {
|
||||
if(e instanceof FileNotFoundException) {
|
||||
mListener.onDataNotAvailable();
|
||||
}else{
|
||||
mListener.onDownloadError(e);
|
||||
}
|
||||
if(determineDownloadUrl()) {
|
||||
downloadForge();
|
||||
}
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1);
|
||||
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
|
||||
}
|
||||
|
||||
@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, mForgeVersion);
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mFullVersion);
|
||||
}
|
||||
|
||||
private void downloadForge() {
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mFullVersion);
|
||||
try {
|
||||
File destinationFile = new File(Tools.DIR_CACHE, "forge-installer.jar");
|
||||
byte[] buffer = new byte[8192];
|
||||
DownloadUtils.downloadFileMonitored(mDownloadUrl, destinationFile, buffer, this);
|
||||
mListener.onDownloadFinished(destinationFile);
|
||||
}catch (FileNotFoundException e) {
|
||||
mListener.onDataNotAvailable();
|
||||
} catch (IOException e) {
|
||||
mListener.onDownloadError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean determineDownloadUrl() {
|
||||
if(mDownloadUrl != null && mFullVersion != null) return true;
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_searching);
|
||||
try {
|
||||
if(!findVersion()) {
|
||||
mListener.onDataNotAvailable();
|
||||
return false;
|
||||
}
|
||||
}catch (IOException e) {
|
||||
mListener.onDownloadError(e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,4 +59,10 @@ public class ForgeUtils {
|
||||
" -jar "+modInstallerJar.getAbsolutePath());
|
||||
intent.putExtra("skipDetectMod", true);
|
||||
}
|
||||
public static void addAutoInstallArgs(Intent intent, File modInstallerJar, String modpackFixupId) {
|
||||
intent.putExtra("javaArgs", "-javaagent:"+ Tools.DIR_DATA+"/forge_installer/forge_installer.jar"
|
||||
+ "=\"" + modpackFixupId +"\"" +
|
||||
" -jar "+modInstallerJar.getAbsolutePath());
|
||||
intent.putExtra("skipDetectMod", true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback,
|
||||
private final Object mMinecraftDownloadLock = new Object();
|
||||
private Throwable mDownloaderThrowable;
|
||||
|
||||
public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, File mDestinationFile, ModloaderDownloadListener mListener) {
|
||||
public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, ModloaderDownloadListener mListener) {
|
||||
this.mOptiFineVersion = mOptiFineVersion;
|
||||
this.mDestinationFile = mDestinationFile;
|
||||
this.mDestinationFile = new File(Tools.DIR_CACHE, "optifine-installer.jar");
|
||||
this.mListener = mListener;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback,
|
||||
}catch (IOException e) {
|
||||
mListener.onDownloadError(e);
|
||||
}
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1);
|
||||
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
|
||||
}
|
||||
|
||||
public boolean runCatching() throws IOException {
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.kdt.SimpleArrayAdapter;
|
||||
|
||||
import net.kdt.pojavlaunch.PojavApplication;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ImageReceiver;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache;
|
||||
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 net.kdt.pojavlaunch.progresskeeper.TaskCountListener;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class ModItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements TaskCountListener {
|
||||
private static final ModItem[] MOD_ITEMS_EMPTY = new ModItem[0];
|
||||
private static final int VIEW_TYPE_MOD_ITEM = 0;
|
||||
private static final int VIEW_TYPE_LOADING = 1;
|
||||
|
||||
/* Used when versions haven't loaded yet, default text to reduce layout shifting */
|
||||
private final SimpleArrayAdapter<String> mLoadingAdapter = new SimpleArrayAdapter<>(Collections.singletonList("Loading"));
|
||||
/* This my seem horribly inefficient but it is in fact the most efficient way without effectively writing a weak collection from scratch */
|
||||
private final Set<ViewHolder> mViewHolderSet = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final ModIconCache mIconCache = new ModIconCache();
|
||||
private final SearchResultCallback mSearchResultCallback;
|
||||
private ModItem[] mModItems;
|
||||
private final ModpackApi mModpackApi;
|
||||
|
||||
/* Cache for ever so slightly rounding the image for the corner not to stick out of the layout */
|
||||
private final float mCornerDimensionCache;
|
||||
|
||||
private Future<?> mTaskInProgress;
|
||||
private SearchFilters mSearchFilters;
|
||||
private SearchResult mCurrentResult;
|
||||
private boolean mLastPage;
|
||||
private boolean mTasksRunning;
|
||||
|
||||
|
||||
public ModItemAdapter(Resources resources, ModpackApi api, SearchResultCallback callback) {
|
||||
mCornerDimensionCache = resources.getDimension(R.dimen._1sdp) / 250;
|
||||
mModpackApi = api;
|
||||
mModItems = new ModItem[]{};
|
||||
mSearchResultCallback = callback;
|
||||
}
|
||||
|
||||
public void performSearchQuery(SearchFilters searchFilters) {
|
||||
if(mTaskInProgress != null) {
|
||||
mTaskInProgress.cancel(true);
|
||||
mTaskInProgress = null;
|
||||
}
|
||||
this.mSearchFilters = searchFilters;
|
||||
this.mLastPage = false;
|
||||
mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, null))
|
||||
.startOnExecutor(PojavApplication.sExecutorService);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(viewGroup.getContext());
|
||||
View view;
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_MOD_ITEM:
|
||||
// Create a new view, which defines the UI of the list item
|
||||
view = layoutInflater.inflate(R.layout.view_mod, viewGroup, false);
|
||||
return new ViewHolder(view);
|
||||
case VIEW_TYPE_LOADING:
|
||||
// Create a new view, which is actually just the progress bar
|
||||
view = layoutInflater.inflate(R.layout.view_loading, viewGroup, false);
|
||||
return new LoadingViewHolder(view);
|
||||
default:
|
||||
throw new RuntimeException("Unimplemented view type!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
switch (getItemViewType(position)) {
|
||||
case VIEW_TYPE_MOD_ITEM:
|
||||
((ModItemAdapter.ViewHolder)holder).setStateLimited(mModItems[position]);
|
||||
break;
|
||||
case VIEW_TYPE_LOADING:
|
||||
loadMoreResults();
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unimplemented view type!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if(mLastPage || mModItems.length == 0) return mModItems.length;
|
||||
return mModItems.length+1;
|
||||
}
|
||||
|
||||
private void loadMoreResults() {
|
||||
if(mTaskInProgress != null) return;
|
||||
mTaskInProgress = new SelfReferencingFuture(new SearchApiTask(mSearchFilters, mCurrentResult))
|
||||
.startOnExecutor(PojavApplication.sExecutorService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if(position < mModItems.length) return VIEW_TYPE_MOD_ITEM;
|
||||
return VIEW_TYPE_LOADING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdateTaskCount(int taskCount) {
|
||||
Tools.runOnUiThread(()->{
|
||||
mTasksRunning = taskCount != 0;
|
||||
for(ViewHolder viewHolder : mViewHolderSet) {
|
||||
viewHolder.updateInstallButtonState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Basic viewholder with expension capabilities
|
||||
*/
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private ModDetail mModDetail = null;
|
||||
private ModItem mModItem = null;
|
||||
private final TextView mTitle, mDescription;
|
||||
private final ImageView mIconView, mSourceView;
|
||||
private View mExtendedLayout;
|
||||
private Spinner mExtendedSpinner;
|
||||
private Button mExtendedButton;
|
||||
private TextView mExtendedErrorTextView;
|
||||
private Future<?> mExtensionFuture;
|
||||
private Bitmap mThumbnailBitmap;
|
||||
private ImageReceiver mImageReceiver;
|
||||
private boolean mInstallEnabled;
|
||||
|
||||
/* Used to display available versions of the mod(pack) */
|
||||
private final SimpleArrayAdapter<String> mVersionAdapter = new SimpleArrayAdapter<>(null);
|
||||
|
||||
public ViewHolder(View view) {
|
||||
super(view);
|
||||
mViewHolderSet.add(this);
|
||||
view.setOnClickListener(v -> {
|
||||
if(!hasExtended()){
|
||||
// Inflate the ViewStub
|
||||
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(
|
||||
mExtendedButton.getContext().getApplicationContext(),
|
||||
mModDetail,
|
||||
mExtendedSpinner.getSelectedItemPosition()));
|
||||
mExtendedSpinner.setAdapter(mLoadingAdapter);
|
||||
} else {
|
||||
if(isExtended()) closeDetailedView();
|
||||
else openDetailedView();
|
||||
}
|
||||
|
||||
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(() -> {
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
|
||||
// Define click listener for the ViewHolder's View
|
||||
mTitle = view.findViewById(R.id.mod_title_textview);
|
||||
mDescription = view.findViewById(R.id.mod_body_textview);
|
||||
mIconView = view.findViewById(R.id.mod_thumbnail_imageview);
|
||||
mSourceView = view.findViewById(R.id.mod_source_imageview);
|
||||
}
|
||||
|
||||
/** Display basic info about the moditem */
|
||||
public void setStateLimited(ModItem item) {
|
||||
mModDetail = null;
|
||||
if(mThumbnailBitmap != null) {
|
||||
mIconView.setImageBitmap(null);
|
||||
mThumbnailBitmap.recycle();
|
||||
}
|
||||
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
|
||||
mImageReceiver = bm->{
|
||||
mImageReceiver = null;
|
||||
mThumbnailBitmap = bm;
|
||||
RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(mIconView.getResources(), bm);
|
||||
drawable.setCornerRadius(mCornerDimensionCache * bm.getHeight());
|
||||
mIconView.setImageDrawable(drawable);
|
||||
};
|
||||
mIconCache.getImage(mImageReceiver, mModItem.getIconCacheTag(), mModItem.imageUrl);
|
||||
mSourceView.setImageResource(getSourceDrawable(item.apiSource));
|
||||
mTitle.setText(item.title);
|
||||
mDescription.setText(item.description);
|
||||
|
||||
if(hasExtended()){
|
||||
closeDetailedView();
|
||||
}
|
||||
}
|
||||
|
||||
/** Display extended info/interaction about a modpack */
|
||||
private void setStateDetailed(ModDetail detailedItem) {
|
||||
if(detailedItem != null) {
|
||||
setInstallEnabled(true);
|
||||
mExtendedErrorTextView.setVisibility(View.GONE);
|
||||
mVersionAdapter.setObjects(Arrays.asList(detailedItem.versionNames));
|
||||
mExtendedSpinner.setAdapter(mVersionAdapter);
|
||||
} else {
|
||||
closeDetailedView();
|
||||
setInstallEnabled(false);
|
||||
mExtendedErrorTextView.setVisibility(View.VISIBLE);
|
||||
mExtendedSpinner.setAdapter(null);
|
||||
mVersionAdapter.setObjects(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void openDetailedView() {
|
||||
mExtendedLayout.setVisibility(View.VISIBLE);
|
||||
mDescription.setMaxLines(99);
|
||||
|
||||
// We need to align to the longer section
|
||||
int futureBottom = mDescription.getBottom() + Tools.mesureTextviewHeight(mDescription) - mDescription.getHeight();
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mExtendedLayout.getLayoutParams();
|
||||
params.topToBottom = futureBottom > mIconView.getBottom() ? R.id.mod_body_textview : R.id.mod_thumbnail_imageview;
|
||||
mExtendedLayout.setLayoutParams(params);
|
||||
}
|
||||
|
||||
private void closeDetailedView(){
|
||||
mExtendedLayout.setVisibility(View.GONE);
|
||||
mDescription.setMaxLines(3);
|
||||
}
|
||||
|
||||
private void setDetailedStateDefault() {
|
||||
setInstallEnabled(false);
|
||||
mExtendedSpinner.setAdapter(mLoadingAdapter);
|
||||
mExtendedErrorTextView.setVisibility(View.GONE);
|
||||
openDetailedView();
|
||||
}
|
||||
|
||||
private boolean hasExtended(){
|
||||
return mExtendedLayout != null;
|
||||
}
|
||||
|
||||
private boolean isExtended(){
|
||||
return hasExtended() && mExtendedLayout.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
private int getSourceDrawable(int apiSource) {
|
||||
switch (apiSource) {
|
||||
case Constants.SOURCE_CURSEFORGE:
|
||||
return R.drawable.ic_curseforge;
|
||||
case Constants.SOURCE_MODRINTH:
|
||||
return R.drawable.ic_modrinth;
|
||||
default:
|
||||
throw new RuntimeException("Unknown API source");
|
||||
}
|
||||
}
|
||||
|
||||
private void setInstallEnabled(boolean enabled) {
|
||||
mInstallEnabled = enabled;
|
||||
updateInstallButtonState();
|
||||
}
|
||||
|
||||
private void updateInstallButtonState() {
|
||||
if(mExtendedButton != null)
|
||||
mExtendedButton.setEnabled(mInstallEnabled && !mTasksRunning);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The view holder used to hold the progress bar at the end of the list
|
||||
*/
|
||||
private static class LoadingViewHolder extends RecyclerView.ViewHolder {
|
||||
public LoadingViewHolder(View view) {
|
||||
super(view);
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchApiTask implements SelfReferencingFuture.FutureInterface {
|
||||
private final SearchFilters mSearchFilters;
|
||||
private final SearchResult mPreviousResult;
|
||||
|
||||
private SearchApiTask(SearchFilters searchFilters, SearchResult previousResult) {
|
||||
this.mSearchFilters = searchFilters;
|
||||
this.mPreviousResult = previousResult;
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Override
|
||||
public void run(Future<?> myFuture) {
|
||||
SearchResult result = mModpackApi.searchMod(mSearchFilters, mPreviousResult);
|
||||
ModItem[] resultModItems = result != null ? result.results : null;
|
||||
if(resultModItems != null && resultModItems.length != 0 && mPreviousResult != null) {
|
||||
ModItem[] newModItems = new ModItem[resultModItems.length + mModItems.length];
|
||||
System.arraycopy(mModItems, 0, newModItems, 0, mModItems.length);
|
||||
System.arraycopy(resultModItems, 0, newModItems, mModItems.length, resultModItems.length);
|
||||
resultModItems = newModItems;
|
||||
}
|
||||
ModItem[] finalModItems = resultModItems;
|
||||
Tools.runOnUiThread(() -> {
|
||||
if(myFuture.isCancelled()) return;
|
||||
mTaskInProgress = null;
|
||||
if(finalModItems == null) {
|
||||
mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_INTERNAL);
|
||||
}else if(finalModItems.length == 0) {
|
||||
if(mPreviousResult != null) {
|
||||
mLastPage = true;
|
||||
notifyItemChanged(mModItems.length);
|
||||
mSearchResultCallback.onSearchFinished();
|
||||
return;
|
||||
}
|
||||
mSearchResultCallback.onSearchError(SearchResultCallback.ERROR_NO_RESULTS);
|
||||
}else{
|
||||
mSearchResultCallback.onSearchFinished();
|
||||
}
|
||||
mCurrentResult = result;
|
||||
if(finalModItems == null) {
|
||||
mModItems = MOD_ITEMS_EMPTY;
|
||||
notifyDataSetChanged();
|
||||
return;
|
||||
}
|
||||
if(mPreviousResult != null) {
|
||||
int prevLength = mModItems.length;
|
||||
mModItems = finalModItems;
|
||||
notifyItemChanged(prevLength);
|
||||
notifyItemRangeInserted(prevLength+1, mModItems.length);
|
||||
}else {
|
||||
mModItems = finalModItems;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public interface SearchResultCallback {
|
||||
int ERROR_INTERNAL = 0;
|
||||
int ERROR_NO_RESULTS = 1;
|
||||
void onSearchFinished();
|
||||
void onSearchError(int error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModLoader;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This class is meant to track the availability of a modloader that is ready to be installed (as a result of modpack installation)
|
||||
* It is needed because having all this logic spread over LauncherActivity would be clumsy, and I think that this is the best way to
|
||||
* ensure that the modloader installer will run, even if the user does not receive the notification or something else happens
|
||||
*/
|
||||
public class ModloaderInstallTracker implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final Activity mActivity;
|
||||
|
||||
/**
|
||||
* Create a ModInstallTracker object. This must be done in the Activity's onCreate method.
|
||||
* @param activity the host activity
|
||||
*/
|
||||
public ModloaderInstallTracker(Activity activity) {
|
||||
mActivity = activity;
|
||||
mSharedPreferences = getPreferences(activity);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the ModloaderInstallTracker to the current Activity. Must be done in the Activity's
|
||||
* onResume method
|
||||
*/
|
||||
public void attach() {
|
||||
mSharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
runCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach the ModloaderInstallTracker from the current Activity. Must be done in the Activity's
|
||||
* onPause method
|
||||
*/
|
||||
public void detach() {
|
||||
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String prefName) {
|
||||
if(!"modLoaderAvailable".equals(prefName)) return;
|
||||
runCheck();
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private void runCheck() {
|
||||
if(!mSharedPreferences.getBoolean("modLoaderAvailable", false)) return;
|
||||
SharedPreferences.Editor editor = mSharedPreferences.edit().putBoolean("modLoaderAvailable", false);
|
||||
if(!editor.commit()) editor.apply();
|
||||
ModLoader modLoader = deserializeModLoader(mSharedPreferences);
|
||||
File modInstallFile = deserializeInstallFile(mSharedPreferences);
|
||||
if(modLoader == null || modInstallFile == null) return;
|
||||
startModInstallation(modLoader, modInstallFile);
|
||||
}
|
||||
|
||||
private void startModInstallation(ModLoader modLoader, File modInstallFile) {
|
||||
Intent installIntent = modLoader.getInstallationIntent(mActivity, modInstallFile);
|
||||
mActivity.startActivity(installIntent);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPreferences(Context context) {
|
||||
return context.getSharedPreferences("modloader_info", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the data necessary to start a ModLoader installation for the tracker to start the installer
|
||||
* sometime.
|
||||
* @param context the Context
|
||||
* @param modLoader the ModLoader to store
|
||||
* @param modInstallFile the installer jar to store
|
||||
*/
|
||||
@SuppressLint("ApplySharedPref")
|
||||
public static void saveModLoader(Context context, ModLoader modLoader, File modInstallFile) {
|
||||
SharedPreferences.Editor editor = getPreferences(context).edit();
|
||||
editor.putInt("modLoaderType", modLoader.modLoaderType);
|
||||
editor.putString("modLoaderVersion", modLoader.modLoaderVersion);
|
||||
editor.putString("minecraftVersion", modLoader.minecraftVersion);
|
||||
editor.putString("modInstallerJar", modInstallFile.getAbsolutePath());
|
||||
editor.putBoolean("modLoaderAvailable", true);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private static ModLoader deserializeModLoader(SharedPreferences sharedPreferences) {
|
||||
if(!sharedPreferences.contains("modLoaderType") ||
|
||||
!sharedPreferences.contains("modLoaderVersion") ||
|
||||
!sharedPreferences.contains("minecraftVersion")) return null;
|
||||
return new ModLoader(sharedPreferences.getInt("modLoaderType", -1),
|
||||
sharedPreferences.getString("modLoaderVersion", ""),
|
||||
sharedPreferences.getString("minecraftVersion", ""));
|
||||
}
|
||||
|
||||
private static File deserializeInstallFile(SharedPreferences sharedPreferences) {
|
||||
if(!sharedPreferences.contains("modInstallerJar")) return null;
|
||||
return new File(sharedPreferences.getString("modInstallerJar", ""));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@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(additionalHeaders, baseUrl + "/" + endpoint, tClass);
|
||||
}
|
||||
|
||||
public <T> T get(String endpoint, HashMap<String, Object> query, Class<T> tClass) {
|
||||
return getFullUrl(additionalHeaders, baseUrl + "/" + endpoint, query, tClass);
|
||||
}
|
||||
|
||||
public <T> T post(String endpoint, T body, Class<T> 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(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);
|
||||
inputStream.close();
|
||||
conn.disconnect();
|
||||
return data;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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();
|
||||
byte[] input = body.getBytes(StandardCharsets.UTF_8);
|
||||
outputStream.write(input, 0, input.length);
|
||||
outputStream.close();
|
||||
|
||||
InputStream inputStream = conn.getInputStream();
|
||||
String data = Tools.read(inputStream);
|
||||
inputStream.close();
|
||||
|
||||
conn.disconnect();
|
||||
return data;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
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()) {
|
||||
String value = Objects.toString(query.get(param));
|
||||
params.append(urlEncodeUTF8(param))
|
||||
.append("=")
|
||||
.append(urlEncodeUTF8(value))
|
||||
.append("&");
|
||||
}
|
||||
return params.substring(0, params.length() - 1);
|
||||
}
|
||||
|
||||
public static <T> T getFullUrl(String url, Class<T> tClass) {
|
||||
return getFullUrl(null, url, tClass);
|
||||
}
|
||||
|
||||
public static <T> T getFullUrl(String url, HashMap<String, Object> query, Class<T> tClass) {
|
||||
return getFullUrl(null, url, query, tClass);
|
||||
}
|
||||
|
||||
public static <T> T postFullUrl(String url, T body, Class<T> tClass) {
|
||||
return postFullUrl(null, url, body, tClass);
|
||||
}
|
||||
|
||||
public static <T> T postFullUrl(String url, HashMap<String, Object> query, T body, Class<T> 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);
|
||||
}
|
||||
|
||||
private static String urlEncodeUTF8(String input) {
|
||||
try {
|
||||
return URLEncoder.encode(input, "UTF-8");
|
||||
}catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("UTF-8 is required");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.kdt.pojavlaunch.PojavApplication;
|
||||
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 java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Group all apis under the same umbrella, as another layer of abstraction
|
||||
*/
|
||||
public class CommonApi implements ModpackApi {
|
||||
|
||||
private final ModpackApi mCurseforgeApi;
|
||||
private final ModpackApi mModrinthApi;
|
||||
private final ModpackApi[] mModpackApis;
|
||||
|
||||
public CommonApi(String curseforgeApiKey) {
|
||||
mCurseforgeApi = new CurseforgeApi(curseforgeApiKey);
|
||||
mModrinthApi = new ModrinthApi();
|
||||
mModpackApis = new ModpackApi[]{mModrinthApi, mCurseforgeApi};
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) {
|
||||
CommonApiSearchResult commonApiSearchResult = (CommonApiSearchResult) previousPageResult;
|
||||
// If there are no previous page results, create a new array. Otherwise, use the one from the previous page
|
||||
SearchResult[] results = commonApiSearchResult == null ?
|
||||
new SearchResult[mModpackApis.length] : commonApiSearchResult.searchResults;
|
||||
|
||||
int totalSize = 0;
|
||||
int totalTotalSize = 0;
|
||||
|
||||
Future<?>[] futures = new Future<?>[mModpackApis.length];
|
||||
for(int i = 0; i < mModpackApis.length; i++) {
|
||||
// If there is an array and its length is zero, this means that we've exhausted the results for this
|
||||
// search query and we don't need to actually do the search
|
||||
if(results[i] != null && results[i].results.length == 0) continue;
|
||||
// If the previous page result is not null (aka the arrays aren't fresh)
|
||||
// and the previous result is null, it means that na error has occured on the previous
|
||||
// page. We lost contingency anyway, so don't bother requesting.
|
||||
if(previousPageResult != null && results[i] == null) continue;
|
||||
futures[i] = PojavApplication.sExecutorService.submit(new ApiDownloadTask(i, searchFilters,
|
||||
results[i]));
|
||||
}
|
||||
|
||||
if(Thread.interrupted()) {
|
||||
cancelAllFutures(futures);
|
||||
return null;
|
||||
}
|
||||
boolean hasSuccessful = false;
|
||||
// Count up all the results
|
||||
for(int i = 0; i < mModpackApis.length; i++) {
|
||||
Future<?> future = futures[i];
|
||||
if(future == null) continue;
|
||||
try {
|
||||
SearchResult searchResult = results[i] = (SearchResult) future.get();
|
||||
if(searchResult != null) hasSuccessful = true;
|
||||
else continue;
|
||||
totalSize += searchResult.results.length;
|
||||
totalTotalSize += searchResult.totalResultCount;
|
||||
}catch (Exception e) {
|
||||
cancelAllFutures(futures);
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if(!hasSuccessful) {
|
||||
return null;
|
||||
}
|
||||
// Then build an array with all the mods
|
||||
ArrayList<ModItem[]> filteredResults = new ArrayList<>(results.length);
|
||||
|
||||
// Sanitize returned values
|
||||
for(SearchResult result : results) {
|
||||
if(result == null) continue;
|
||||
ModItem[] searchResults = result.results;
|
||||
// If the length is zero, we don't need to perform needless copies
|
||||
if(searchResults.length == 0) continue;
|
||||
filteredResults.add(searchResults);
|
||||
}
|
||||
filteredResults.trimToSize();
|
||||
if(Thread.interrupted()) return null;
|
||||
|
||||
ModItem[] concatenatedItems = buildFusedResponse(filteredResults);
|
||||
if(Thread.interrupted()) return null;
|
||||
// Recycle or create new search result
|
||||
if(commonApiSearchResult == null) commonApiSearchResult = new CommonApiSearchResult();
|
||||
commonApiSearchResult.searchResults = results;
|
||||
commonApiSearchResult.totalResultCount = totalTotalSize;
|
||||
commonApiSearchResult.results = concatenatedItems;
|
||||
return commonApiSearchResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDetail getModDetails(ModItem item) {
|
||||
return getModpackApi(item.apiSource).getModDetails(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException {
|
||||
return getModpackApi(modDetail.apiSource).installMod(modDetail, selectedVersion);
|
||||
}
|
||||
|
||||
private @NonNull ModpackApi getModpackApi(int apiSource) {
|
||||
switch (apiSource) {
|
||||
case Constants.SOURCE_MODRINTH:
|
||||
return mModrinthApi;
|
||||
case Constants.SOURCE_CURSEFORGE:
|
||||
return mCurseforgeApi;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown API source: " + apiSource);
|
||||
}
|
||||
}
|
||||
|
||||
/** Fuse the arrays in a way that's fair for every endpoint */
|
||||
private ModItem[] buildFusedResponse(List<ModItem[]> modMatrix){
|
||||
int totalSize = 0;
|
||||
|
||||
// Calculate the total size of the merged array
|
||||
for (ModItem[] array : modMatrix) {
|
||||
totalSize += array.length;
|
||||
}
|
||||
|
||||
ModItem[] fusedItems = new ModItem[totalSize];
|
||||
|
||||
int mergedIndex = 0;
|
||||
int maxLength = 0;
|
||||
|
||||
// Find the maximum length of arrays
|
||||
for (ModItem[] array : modMatrix) {
|
||||
if (array.length > maxLength) {
|
||||
maxLength = array.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the merged array
|
||||
for (int i = 0; i < maxLength; i++) {
|
||||
for (ModItem[] matrix : modMatrix) {
|
||||
if (i < matrix.length) {
|
||||
fusedItems[mergedIndex] = matrix[i];
|
||||
mergedIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fusedItems;
|
||||
}
|
||||
|
||||
private void cancelAllFutures(Future<?>[] futures) {
|
||||
for(Future<?> future : futures) {
|
||||
if(future == null) continue;
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class ApiDownloadTask implements Callable<SearchResult> {
|
||||
private final int mModApi;
|
||||
private final SearchFilters mSearchFilters;
|
||||
private final SearchResult mPreviousPageResult;
|
||||
|
||||
private ApiDownloadTask(int modApi, SearchFilters searchFilters, SearchResult previousPageResult) {
|
||||
this.mModApi = modApi;
|
||||
this.mSearchFilters = searchFilters;
|
||||
this.mPreviousPageResult = previousPageResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult call() {
|
||||
return mModpackApis[mModApi].searchMod(mSearchFilters, mPreviousPageResult);
|
||||
}
|
||||
}
|
||||
|
||||
class CommonApiSearchResult extends SearchResult {
|
||||
SearchResult[] searchResults = new SearchResult[mModpackApis.length];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
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.CurseManifest;
|
||||
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.progresskeeper.ProgressKeeper;
|
||||
import net.kdt.pojavlaunch.utils.FileUtils;
|
||||
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_SORT_RELEVANCY = 1;
|
||||
private static final int CURSEFORGE_PAGINATION_SIZE = 50;
|
||||
private static final int CURSEFORGE_PAGINATION_END_REACHED = -1;
|
||||
private static final int CURSEFORGE_PAGINATION_ERROR = -2;
|
||||
|
||||
private final ApiHandler mApiHandler;
|
||||
public CurseforgeApi(String apiKey) {
|
||||
mApiHandler = new ApiHandler("https://api.curseforge.com/v1", apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) {
|
||||
CurseforgeSearchResult curseforgeSearchResult = (CurseforgeSearchResult) previousPageResult;
|
||||
|
||||
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);
|
||||
params.put("sortField", CURSEFORGE_SORT_RELEVANCY);
|
||||
params.put("sortOrder", "desc");
|
||||
if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty())
|
||||
params.put("gameVersion", searchFilters.mcVersion);
|
||||
if(previousPageResult != null)
|
||||
params.put("index", curseforgeSearchResult.previousOffset);
|
||||
|
||||
JsonObject response = mApiHandler.get("mods/search", params, JsonObject.class);
|
||||
if(response == null) return null;
|
||||
JsonArray dataArray = response.getAsJsonArray("data");
|
||||
if(dataArray == null) return null;
|
||||
JsonObject paginationInfo = response.getAsJsonObject("pagination");
|
||||
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);
|
||||
}
|
||||
if(curseforgeSearchResult == null) curseforgeSearchResult = new CurseforgeSearchResult();
|
||||
curseforgeSearchResult.results = modItemList.toArray(new ModItem[0]);
|
||||
curseforgeSearchResult.totalResultCount = paginationInfo.get("totalCount").getAsInt();
|
||||
curseforgeSearchResult.previousOffset += dataArray.size();
|
||||
return curseforgeSearchResult;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDetail getModDetails(ModItem item) {
|
||||
ArrayList<JsonObject> allModDetails = new ArrayList<>();
|
||||
int index = 0;
|
||||
while(index != CURSEFORGE_PAGINATION_END_REACHED &&
|
||||
index != CURSEFORGE_PAGINATION_ERROR) {
|
||||
index = getPaginatedDetails(allModDetails, index, item.id);
|
||||
}
|
||||
if(index == CURSEFORGE_PAGINATION_ERROR) 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 ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException{
|
||||
//TODO considering only modpacks for now
|
||||
return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installCurseforgeZip);
|
||||
}
|
||||
|
||||
|
||||
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 CURSEFORGE_PAGINATION_ERROR;
|
||||
JsonArray data = response.getAsJsonArray("data");
|
||||
if(data == null) return CURSEFORGE_PAGINATION_ERROR;
|
||||
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 CURSEFORGE_PAGINATION_END_REACHED; // we read the remainder! yay!
|
||||
}
|
||||
return index + data.size();
|
||||
}
|
||||
|
||||
private ModLoader 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);
|
||||
if(!verifyManifest(curseManifest)) {
|
||||
Log.i("CurseforgeApi","manifest verification failed");
|
||||
return null;
|
||||
}
|
||||
ModDownloader modDownloader = new ModDownloader(new File(instanceDestination,"mods"), true);
|
||||
int fileCount = curseManifest.files.length;
|
||||
for(int i = 0; i < fileCount; i++) {
|
||||
final CurseManifest.CurseFile curseFile = curseManifest.files[i];
|
||||
modDownloader.submitDownload(()->{
|
||||
String url = getDownloadUrl(curseFile.projectID, curseFile.fileID);
|
||||
if(url == null && curseFile.required)
|
||||
throw new IOException("Failed to obtain download URL for "+curseFile.projectID+" "+curseFile.fileID);
|
||||
else if(url == null) return null;
|
||||
return new ModDownloader.FileInfo(url, FileUtils.getFileName(url));
|
||||
});
|
||||
}
|
||||
modDownloader.awaitFinish((c,m)->
|
||||
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, (int) Math.max((float)c/m*100,0), R.string.modpack_download_downloading_mods_fc, c, m)
|
||||
);
|
||||
String overridesDir = "overrides";
|
||||
if(curseManifest.overrides != null) overridesDir = curseManifest.overrides;
|
||||
ZipUtils.zipExtract(modpackZipFile, overridesDir, instanceDestination);
|
||||
return createInfo(curseManifest.minecraft);
|
||||
}
|
||||
}
|
||||
|
||||
private ModLoader createInfo(CurseManifest.CurseMinecraft minecraft) {
|
||||
CurseManifest.CurseModLoader primaryModLoader = null;
|
||||
for(CurseManifest.CurseModLoader modLoader : minecraft.modLoaders) {
|
||||
if(modLoader.primary) {
|
||||
primaryModLoader = modLoader;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(primaryModLoader == null) primaryModLoader = minecraft.modLoaders[0];
|
||||
String modLoaderId = primaryModLoader.id;
|
||||
int dashIndex = modLoaderId.indexOf('-');
|
||||
String modLoaderName = modLoaderId.substring(0, dashIndex);
|
||||
String modLoaderVersion = modLoaderId.substring(dashIndex+1);
|
||||
Log.i("CurseforgeApi", modLoaderId + " " + modLoaderName + " "+modLoaderVersion);
|
||||
int modLoaderTypeInt;
|
||||
switch (modLoaderName) {
|
||||
case "forge":
|
||||
modLoaderTypeInt = ModLoader.MOD_LOADER_FORGE;
|
||||
break;
|
||||
case "fabric":
|
||||
modLoaderTypeInt = ModLoader.MOD_LOADER_FABRIC;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
//TODO: Quilt is also Forge? How does that work?
|
||||
}
|
||||
return new ModLoader(modLoaderTypeInt, modLoaderVersion, minecraft.version);
|
||||
}
|
||||
|
||||
private String getDownloadUrl(long projectID, long fileID) {
|
||||
// First try the official api endpoint
|
||||
JsonObject response = mApiHandler.get("mods/"+projectID+"/files/"+fileID+"/download-url", JsonObject.class);
|
||||
if (response != null && !response.get("data").isJsonNull())
|
||||
return response.get("data").getAsString();
|
||||
|
||||
// Otherwise, fallback to building an edge link
|
||||
JsonObject fallbackResponse = mApiHandler.get(String.format("mods/%s/files/%s", projectID, fileID), JsonObject.class);
|
||||
if (fallbackResponse != null && !fallbackResponse.get("data").isJsonNull()){
|
||||
JsonObject modData = fallbackResponse.get("data").getAsJsonObject();
|
||||
int id = modData.get("id").getAsInt();
|
||||
return String.format("https://edge.forgecdn.net/files/%s/%s/%s", id/1000, id % 1000, modData.get("fileName").getAsString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean verifyManifest(CurseManifest manifest) {
|
||||
if(!"minecraftModpack".equals(manifest.manifestType)) return false;
|
||||
if(manifest.manifestVersion != 1) return false;
|
||||
if(manifest.minecraft == null) return false;
|
||||
if(manifest.minecraft.version == null) return false;
|
||||
if(manifest.minecraft.modLoaders == null) return false;
|
||||
if(manifest.minecraft.modLoaders.length < 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
class CurseforgeSearchResult extends SearchResult {
|
||||
int previousOffset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class ModDownloader {
|
||||
private static final ThreadLocal<byte[]> sThreadLocalBuffer = new ThreadLocal<>();
|
||||
private final ThreadPoolExecutor mDownloadPool = new ThreadPoolExecutor(4,4,100, TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>());
|
||||
private final AtomicBoolean mTerminator = new AtomicBoolean(false);
|
||||
private final AtomicLong mDownloadSize = new AtomicLong(0);
|
||||
private final Object mExceptionSyncPoint = new Object();
|
||||
private final File mDestinationDirectory;
|
||||
private final boolean mUseFileCount;
|
||||
private IOException mFirstIOException;
|
||||
private long mTotalSize;
|
||||
|
||||
public ModDownloader(File destinationDirectory) {
|
||||
this(destinationDirectory, false);
|
||||
}
|
||||
|
||||
public ModDownloader(File destinationDirectory, boolean useFileCount) {
|
||||
this.mDownloadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
|
||||
this.mDestinationDirectory = destinationDirectory;
|
||||
this.mUseFileCount = useFileCount;
|
||||
}
|
||||
|
||||
public void submitDownload(int fileSize, String relativePath, String... url) {
|
||||
if(mUseFileCount) mTotalSize += 1;
|
||||
else mTotalSize += fileSize;
|
||||
mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath)));
|
||||
}
|
||||
|
||||
public void submitDownload(FileInfoProvider infoProvider) {
|
||||
if(!mUseFileCount) throw new RuntimeException("This method can only be used in a file-counting ModDownloader");
|
||||
mTotalSize += 1;
|
||||
mDownloadPool.execute(new FileInfoQueryTask(infoProvider));
|
||||
}
|
||||
|
||||
public void awaitFinish(Tools.DownloaderFeedback feedback) throws IOException {
|
||||
try {
|
||||
mDownloadPool.shutdown();
|
||||
while(!mDownloadPool.awaitTermination(20, TimeUnit.MILLISECONDS) && !mTerminator.get()) {
|
||||
feedback.updateProgress((int) mDownloadSize.get(), (int) mTotalSize);
|
||||
}
|
||||
if(mTerminator.get()) {
|
||||
mDownloadPool.shutdownNow();
|
||||
synchronized (mExceptionSyncPoint) {
|
||||
if(mFirstIOException == null) mExceptionSyncPoint.wait();
|
||||
throw mFirstIOException;
|
||||
}
|
||||
}
|
||||
}catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getThreadLocalBuffer() {
|
||||
byte[] buffer = sThreadLocalBuffer.get();
|
||||
if(buffer != null) return buffer;
|
||||
buffer = new byte[8192];
|
||||
sThreadLocalBuffer.set(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private void downloadFailed(IOException exception) {
|
||||
mTerminator.set(true);
|
||||
synchronized (mExceptionSyncPoint) {
|
||||
if(mFirstIOException == null) {
|
||||
mFirstIOException = exception;
|
||||
mExceptionSyncPoint.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileInfoQueryTask implements Runnable {
|
||||
private final FileInfoProvider mFileInfoProvider;
|
||||
public FileInfoQueryTask(FileInfoProvider fileInfoProvider) {
|
||||
this.mFileInfoProvider = fileInfoProvider;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
FileInfo fileInfo = mFileInfoProvider.getFileInfo();
|
||||
if(fileInfo == null) return;
|
||||
new DownloadTask(new String[]{fileInfo.url},
|
||||
new File(mDestinationDirectory, fileInfo.relativePath)).run();
|
||||
}catch (IOException e) {
|
||||
downloadFailed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadTask implements Runnable, Tools.DownloaderFeedback {
|
||||
private final String[] mDownloadUrls;
|
||||
private final File mDestination;
|
||||
private int last = 0;
|
||||
|
||||
public DownloadTask(String[] downloadurls,
|
||||
File downloadDestination) {
|
||||
this.mDownloadUrls = downloadurls;
|
||||
this.mDestination = downloadDestination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
IOException exception = null;
|
||||
for(String sourceUrl : mDownloadUrls) {
|
||||
try {
|
||||
exception = tryDownload(sourceUrl);
|
||||
if(exception == null) return;
|
||||
}catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(exception != null) {
|
||||
downloadFailed(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException tryDownload(String sourceUrl) throws InterruptedException {
|
||||
IOException exception = null;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
try {
|
||||
DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this);
|
||||
if(mUseFileCount) mDownloadSize.addAndGet(1);
|
||||
return null;
|
||||
} catch (InterruptedIOException e) {
|
||||
throw new InterruptedException();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
exception = e;
|
||||
}
|
||||
if(!mUseFileCount) {
|
||||
mDownloadSize.addAndGet(-last);
|
||||
last = 0;
|
||||
}
|
||||
}
|
||||
return exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(int curr, int max) {
|
||||
if(mUseFileCount) return;
|
||||
mDownloadSize.addAndGet(curr - last);
|
||||
last = curr;
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileInfo {
|
||||
public final String url;
|
||||
public final String relativePath;
|
||||
|
||||
public FileInfo(String url, String relativePath) {
|
||||
this.url = url;
|
||||
this.relativePath = relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
public interface FileInfoProvider {
|
||||
FileInfo getFileInfo() throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
|
||||
import net.kdt.pojavlaunch.modloaders.FabriclikeDownloadTask;
|
||||
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 java.io.File;
|
||||
|
||||
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 final int modLoaderType;
|
||||
public final String modLoaderVersion;
|
||||
public final String minecraftVersion;
|
||||
|
||||
public ModLoader(int modLoaderType, String modLoaderVersion, String minecraftVersion) {
|
||||
this.modLoaderType = modLoaderType;
|
||||
this.modLoaderVersion = modLoaderVersion;
|
||||
this.minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Version ID (the name of the mod loader in the versions/ folder)
|
||||
* @return the Version ID as a string
|
||||
*/
|
||||
public String getVersionId() {
|
||||
switch (modLoaderType) {
|
||||
case MOD_LOADER_FORGE:
|
||||
return minecraftVersion+"-forge-"+modLoaderVersion;
|
||||
case MOD_LOADER_FABRIC:
|
||||
return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion;
|
||||
case MOD_LOADER_QUILT:
|
||||
return "quilt-loader-"+modLoaderVersion+"-"+minecraftVersion;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Runnable that needs to run in order to download the mod loader.
|
||||
* The task will also install the mod loader if it does not require GUI installation
|
||||
* @param listener the listener that gets notified of the installation status
|
||||
* @return the task Runnable that needs to be ran
|
||||
*/
|
||||
public Runnable getDownloadTask(ModloaderDownloadListener listener) {
|
||||
switch (modLoaderType) {
|
||||
case MOD_LOADER_FORGE:
|
||||
return new ForgeDownloadTask(listener, minecraftVersion, modLoaderVersion);
|
||||
case MOD_LOADER_FABRIC:
|
||||
return createFabriclikeTask(listener, FabriclikeUtils.FABRIC_UTILS);
|
||||
case MOD_LOADER_QUILT:
|
||||
return createFabriclikeTask(listener, FabriclikeUtils.QUILT_UTILS);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Intent to start the graphical installation of the mod loader.
|
||||
* This method should only be ran after the download task of the specified mod loader finishes.
|
||||
* This method returns null if the mod loader does not require GUI installation
|
||||
* @param context the package resolving Context (can be the base context)
|
||||
* @param modInstallerJar the JAR file of the mod installer, provided by ModloaderDownloadListener after the installation
|
||||
* finishes.
|
||||
* @return the Intent which the launcher needs to start in order to install the mod loader
|
||||
*/
|
||||
public Intent getInstallationIntent(Context context, File modInstallerJar) {
|
||||
Intent baseIntent = new Intent(context, JavaGUILauncherActivity.class);
|
||||
switch (modLoaderType) {
|
||||
case MOD_LOADER_FORGE:
|
||||
ForgeUtils.addAutoInstallArgs(baseIntent, modInstallerJar, getVersionId());
|
||||
return baseIntent;
|
||||
case MOD_LOADER_QUILT:
|
||||
case MOD_LOADER_FABRIC:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the mod loader this object denotes requires GUI installation
|
||||
* @return true if mod loader requires GUI installation, false otherwise
|
||||
*/
|
||||
public boolean requiresGuiInstallation() {
|
||||
switch (modLoaderType) {
|
||||
case MOD_LOADER_FORGE:
|
||||
return true;
|
||||
case MOD_LOADER_FABRIC:
|
||||
case MOD_LOADER_QUILT:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private FabriclikeDownloadTask createFabriclikeTask(ModloaderDownloadListener modloaderDownloadListener, FabriclikeUtils utils) {
|
||||
return new FabriclikeDownloadTask(modloaderDownloadListener, utils, minecraftVersion, modLoaderVersion, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.PojavApplication;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
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 java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface ModpackApi {
|
||||
|
||||
/**
|
||||
* @param searchFilters Filters
|
||||
* @param previousPageResult The result from the previous page
|
||||
* @return the list of mod items from specified offset
|
||||
*/
|
||||
SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult);
|
||||
|
||||
/**
|
||||
* @param searchFilters Filters
|
||||
* @return A list of mod items
|
||||
*/
|
||||
default SearchResult searchMod(SearchFilters searchFilters) {
|
||||
return searchMod(searchFilters, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the mod details
|
||||
* @param item The moditem that was selected
|
||||
* @return Detailed data about a mod(pack)
|
||||
*/
|
||||
ModDetail getModDetails(ModItem item);
|
||||
|
||||
/**
|
||||
* Download and install the mod(pack)
|
||||
* @param modDetail The mod detail data
|
||||
* @param selectedVersion The selected version
|
||||
*/
|
||||
default void handleInstallation(Context context, ModDetail modDetail, int selectedVersion) {
|
||||
// Doing this here since when starting installation, the progress does not start immediately
|
||||
// which may lead to two concurrent installations (very bad)
|
||||
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.global_waiting);
|
||||
PojavApplication.sExecutorService.execute(() -> {
|
||||
try {
|
||||
ModLoader loaderInfo = installMod(modDetail, selectedVersion);
|
||||
if (loaderInfo == null) return;
|
||||
loaderInfo.getDownloadTask(new NotificationDownloadListener(context, loaderInfo)).run();
|
||||
}catch (IOException e) {
|
||||
Tools.showErrorRemote(context, R.string.modpack_install_download_failed, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the mod(pack).
|
||||
* May require the download of additional files.
|
||||
* May requires launching the installation of a modloader
|
||||
* @param modDetail The mod detail data
|
||||
* @param selectedVersion The selected version
|
||||
*/
|
||||
ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
|
||||
import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper;
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
|
||||
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ModpackInstaller {
|
||||
|
||||
public static ModLoader installModpack(ModDetail modDetail, int selectedVersion, InstallFunction installFunction) throws IOException{
|
||||
String versionUrl = modDetail.versionUrls[selectedVersion];
|
||||
String modpackName = modDetail.title.toLowerCase(Locale.ROOT).trim().replace(" ", "_" );
|
||||
|
||||
// Build a new minecraft instance, folder first
|
||||
|
||||
// Get the modpack file
|
||||
File modpackFile = new File(Tools.DIR_CACHE, modpackName + ".cf"); // Cache File
|
||||
ModLoader modLoaderInfo;
|
||||
try {
|
||||
byte[] downloadBuffer = new byte[8192];
|
||||
DownloadUtils.downloadFileMonitored(versionUrl, modpackFile, downloadBuffer,
|
||||
new DownloaderProgressWrapper(R.string.modpack_download_downloading_metadata,
|
||||
ProgressLayout.INSTALL_MODPACK));
|
||||
// Install the modpack
|
||||
modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName));
|
||||
|
||||
} finally {
|
||||
modpackFile.delete();
|
||||
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
|
||||
}
|
||||
if(modLoaderInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the instance
|
||||
MinecraftProfile profile = new MinecraftProfile();
|
||||
profile.gameDir = "./custom_instances/" + modpackName;
|
||||
profile.name = modDetail.title;
|
||||
profile.lastVersionId = modLoaderInfo.getVersionId();
|
||||
profile.icon = ModIconCache.getBase64Image(modDetail.getIconCacheTag());
|
||||
|
||||
|
||||
LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile);
|
||||
LauncherProfiles.write();
|
||||
|
||||
return modLoaderInfo;
|
||||
}
|
||||
|
||||
interface InstallFunction {
|
||||
ModLoader installModpack(File modpackFile, File instanceDestination) throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.kdt.mcgui.ProgressLayout;
|
||||
|
||||
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.ModrinthIndex;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
|
||||
import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper;
|
||||
import net.kdt.pojavlaunch.utils.ZipUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ModrinthApi implements ModpackApi{
|
||||
private final ApiHandler mApiHandler;
|
||||
public ModrinthApi(){
|
||||
mApiHandler = new ApiHandler("https://api.modrinth.com/v2");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult searchMod(SearchFilters searchFilters, SearchResult previousPageResult) {
|
||||
ModrinthSearchResult modrinthSearchResult = (ModrinthSearchResult) previousPageResult;
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
|
||||
// Build the facets filters
|
||||
StringBuilder facetString = new StringBuilder();
|
||||
facetString.append("[");
|
||||
facetString.append(String.format("[\"project_type:%s\"]", searchFilters.isModpack ? "modpack" : "mod"));
|
||||
if(searchFilters.mcVersion != null && !searchFilters.mcVersion.isEmpty())
|
||||
facetString.append(String.format(",[\"versions:%s\"]", searchFilters.mcVersion));
|
||||
facetString.append("]");
|
||||
params.put("facets", facetString.toString());
|
||||
params.put("query", searchFilters.name.replace(' ', '+'));
|
||||
params.put("limit", 50);
|
||||
params.put("index", "relevance");
|
||||
if(modrinthSearchResult != null)
|
||||
params.put("offset", modrinthSearchResult.previousOffset);
|
||||
|
||||
JsonObject response = mApiHandler.get("search", params, JsonObject.class);
|
||||
if(response == null) return null;
|
||||
JsonArray responseHits = response.getAsJsonArray("hits");
|
||||
if(responseHits == null) return null;
|
||||
|
||||
ModItem[] items = new ModItem[responseHits.size()];
|
||||
for(int i=0; i<responseHits.size(); ++i){
|
||||
JsonObject hit = responseHits.get(i).getAsJsonObject();
|
||||
items[i] = new ModItem(
|
||||
Constants.SOURCE_MODRINTH,
|
||||
hit.get("project_type").getAsString().equals("modpack"),
|
||||
hit.get("project_id").getAsString(),
|
||||
hit.get("title").getAsString(),
|
||||
hit.get("description").getAsString(),
|
||||
hit.get("icon_url").getAsString()
|
||||
);
|
||||
}
|
||||
if(modrinthSearchResult == null) modrinthSearchResult = new ModrinthSearchResult();
|
||||
modrinthSearchResult.previousOffset += responseHits.size();
|
||||
modrinthSearchResult.results = items;
|
||||
modrinthSearchResult.totalResultCount = response.get("total_hits").getAsInt();
|
||||
return modrinthSearchResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDetail getModDetails(ModItem item) {
|
||||
|
||||
JsonArray response = mApiHandler.get(String.format("project/%s/version", item.id), JsonArray.class);
|
||||
if(response == null) return null;
|
||||
System.out.println(response);
|
||||
String[] names = new String[response.size()];
|
||||
String[] mcNames = new String[response.size()];
|
||||
String[] urls = new String[response.size()];
|
||||
|
||||
for (int i=0; i<response.size(); ++i) {
|
||||
JsonObject version = response.get(i).getAsJsonObject();
|
||||
names[i] = version.get("name").getAsString();
|
||||
mcNames[i] = version.get("game_versions").getAsJsonArray().get(0).getAsString();
|
||||
urls[i] = version.get("files").getAsJsonArray().get(0).getAsJsonObject().get("url").getAsString();
|
||||
}
|
||||
|
||||
return new ModDetail(item, names, mcNames, urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException{
|
||||
//TODO considering only modpacks for now
|
||||
return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installMrpack);
|
||||
}
|
||||
|
||||
private static ModLoader createInfo(ModrinthIndex modrinthIndex) {
|
||||
if(modrinthIndex == null) return null;
|
||||
Map<String, String> dependencies = modrinthIndex.dependencies;
|
||||
String mcVersion = dependencies.get("minecraft");
|
||||
if(mcVersion == null) return null;
|
||||
String modLoaderVersion;
|
||||
if((modLoaderVersion = dependencies.get("forge")) != null) {
|
||||
return new ModLoader(ModLoader.MOD_LOADER_FORGE, modLoaderVersion, mcVersion);
|
||||
}
|
||||
if((modLoaderVersion = dependencies.get("fabric-loader")) != null) {
|
||||
return new ModLoader(ModLoader.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion);
|
||||
}
|
||||
if((modLoaderVersion = dependencies.get("quilt-loader")) != null) {
|
||||
return new ModLoader(ModLoader.MOD_LOADER_QUILT, modLoaderVersion, mcVersion);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ModLoader installMrpack(File mrpackFile, File instanceDestination) throws IOException {
|
||||
try (ZipFile modpackZipFile = new ZipFile(mrpackFile)){
|
||||
ModrinthIndex modrinthIndex = Tools.GLOBAL_GSON.fromJson(
|
||||
Tools.read(ZipUtils.getEntryStream(modpackZipFile, "modrinth.index.json")),
|
||||
ModrinthIndex.class);
|
||||
|
||||
ModDownloader modDownloader = new ModDownloader(instanceDestination);
|
||||
for(ModrinthIndex.ModrinthIndexFile indexFile : modrinthIndex.files) {
|
||||
modDownloader.submitDownload(indexFile.fileSize, indexFile.path, indexFile.downloads);
|
||||
}
|
||||
modDownloader.awaitFinish(new DownloaderProgressWrapper(R.string.modpack_download_downloading_mods, ProgressLayout.INSTALL_MODPACK));
|
||||
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.modpack_download_applying_overrides, 1, 2);
|
||||
ZipUtils.zipExtract(modpackZipFile, "overrides/", instanceDestination);
|
||||
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 50, R.string.modpack_download_applying_overrides, 2, 2);
|
||||
ZipUtils.zipExtract(modpackZipFile, "client-overrides/", instanceDestination);
|
||||
return createInfo(modrinthIndex);
|
||||
}
|
||||
}
|
||||
|
||||
class ModrinthSearchResult extends SearchResult {
|
||||
int previousOffset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.api;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import net.kdt.pojavlaunch.LauncherActivity;
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
|
||||
import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker;
|
||||
import net.kdt.pojavlaunch.value.NotificationConstants;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class NotificationDownloadListener implements ModloaderDownloadListener {
|
||||
|
||||
private final NotificationCompat.Builder mNotificationBuilder;
|
||||
private final NotificationManager mNotificationManager;
|
||||
private final Context mContext;
|
||||
private final ModLoader mModLoader;
|
||||
|
||||
public NotificationDownloadListener(Context context, ModLoader modLoader) {
|
||||
mModLoader = modLoader;
|
||||
mContext = context.getApplicationContext();
|
||||
mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mNotificationBuilder = new NotificationCompat.Builder(context, "channel_id")
|
||||
.setContentTitle(context.getString(R.string.modpack_install_notification_title))
|
||||
.setSmallIcon(R.drawable.notif_icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadFinished(File downloadedFile) {
|
||||
if(mModLoader.requiresGuiInstallation()) {
|
||||
ModloaderInstallTracker.saveModLoader(mContext, mModLoader, downloadedFile);
|
||||
Intent mainActivityIntent = new Intent(mContext, LauncherActivity.class);
|
||||
Tools.runOnUiThread(() -> sendIntentNotification(mainActivityIntent, R.string.modpack_install_notification_success));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataNotAvailable() {
|
||||
Tools.runOnUiThread(()->sendEmptyNotification(R.string.modpack_install_notification_data_not_available));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadError(Exception e) {
|
||||
Tools.showErrorRemote(mContext, R.string.modpack_install_modloader_download_failed, e);
|
||||
}
|
||||
|
||||
private void sendIntentNotification(Intent intent, int contentText) {
|
||||
PendingIntent pendingInstallIntent =
|
||||
PendingIntent.getActivity(mContext, NotificationConstants.PENDINGINTENT_CODE_DOWNLOAD_SERVICE,
|
||||
intent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
|
||||
mNotificationBuilder.setContentText(mContext.getText(contentText));
|
||||
mNotificationBuilder.setContentIntent(pendingInstallIntent);
|
||||
mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build());
|
||||
}
|
||||
|
||||
private void sendEmptyNotification(int contentText) {
|
||||
mNotificationBuilder.setContentText(mContext.getText(contentText));
|
||||
mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.imagecache;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import net.kdt.pojavlaunch.utils.DownloadUtils;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
class DownloadImageTask implements Runnable {
|
||||
private static final float BITMAP_FINAL_DIMENSION = 256f;
|
||||
private final ReadFromDiskTask mParentTask;
|
||||
private int mRetryCount;
|
||||
DownloadImageTask(ReadFromDiskTask parentTask) {
|
||||
this.mParentTask = parentTask;
|
||||
this.mRetryCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean wasSuccessful = false;
|
||||
while(mRetryCount < 5 && !(wasSuccessful = runCatching())) {
|
||||
mRetryCount++;
|
||||
}
|
||||
// restart the parent task to read the image and send it to the receiver
|
||||
// if it wasn't cancelled. If it was, then we just die here
|
||||
if(wasSuccessful && !mParentTask.taskCancelled())
|
||||
mParentTask.iconCache.cacheLoaderPool.execute(mParentTask);
|
||||
}
|
||||
|
||||
public boolean runCatching() {
|
||||
try {
|
||||
IconCacheJanitor.waitForJanitorToFinish();
|
||||
DownloadUtils.downloadFile(mParentTask.imageUrl, mParentTask.cacheFile);
|
||||
Bitmap bitmap = BitmapFactory.decodeFile(mParentTask.cacheFile.getAbsolutePath());
|
||||
if(bitmap == null) return false;
|
||||
int bitmapWidth = bitmap.getWidth(), bitmapHeight = bitmap.getHeight();
|
||||
if(bitmapWidth <= BITMAP_FINAL_DIMENSION && bitmapHeight <= BITMAP_FINAL_DIMENSION) {
|
||||
bitmap.recycle();
|
||||
return true;
|
||||
}
|
||||
float imageRescaleRatio = Math.min(BITMAP_FINAL_DIMENSION/bitmapWidth, BITMAP_FINAL_DIMENSION/bitmapHeight);
|
||||
Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap,
|
||||
(int)(bitmapWidth * imageRescaleRatio),
|
||||
(int)(bitmapHeight * imageRescaleRatio),
|
||||
true);
|
||||
bitmap.recycle();
|
||||
if(resizedBitmap == bitmap) return true;
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(mParentTask.cacheFile)) {
|
||||
resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fileOutputStream);
|
||||
} finally {
|
||||
resizedBitmap.recycle();
|
||||
}
|
||||
return true;
|
||||
}catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.imagecache;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import net.kdt.pojavlaunch.PojavApplication;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* This image is intended to keep the mod icon cache tidy (aka under 100 megabytes)
|
||||
*/
|
||||
public class IconCacheJanitor implements Runnable{
|
||||
public static final long CACHE_SIZE_LIMIT = 104857600; // The cache size limit, 100 megabytes
|
||||
public static final long CACHE_BRINGDOWN = 52428800; // The size to which the cache should be brought
|
||||
// in case of an overflow, 50 mb
|
||||
private static Future<?> sJanitorFuture;
|
||||
private static boolean sJanitorRan = false;
|
||||
private IconCacheJanitor() {
|
||||
// don't allow others to create this
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
File modIconCachePath = ModIconCache.getImageCachePath();
|
||||
if(!modIconCachePath.isDirectory() || !modIconCachePath.canRead()) return;
|
||||
File[] modIconFiles = modIconCachePath.listFiles();
|
||||
if(modIconFiles == null) return;
|
||||
ArrayList<File> writableModIconFiles = new ArrayList<>(modIconFiles.length);
|
||||
long directoryFileSize = 0;
|
||||
for(File modIconFile : modIconFiles) {
|
||||
if(!modIconFile.isFile() || !modIconFile.canRead()) continue;
|
||||
directoryFileSize += modIconFile.length();
|
||||
if(!modIconFile.canWrite()) continue;
|
||||
writableModIconFiles.add(modIconFile);
|
||||
}
|
||||
if(directoryFileSize < CACHE_SIZE_LIMIT) {
|
||||
Log.i("IconCacheJanitor", "Skipping cleanup because there's not enough to clean up");
|
||||
return;
|
||||
}
|
||||
Arrays.sort(modIconFiles,
|
||||
(x,y)-> Long.compare(y.lastModified(), x.lastModified())
|
||||
);
|
||||
int filesCleanedUp = 0;
|
||||
for(File modFile : writableModIconFiles) {
|
||||
if(directoryFileSize < CACHE_BRINGDOWN) break;
|
||||
long modFileSize = modFile.length();
|
||||
if(modFile.delete()) {
|
||||
directoryFileSize -= modFileSize;
|
||||
filesCleanedUp++;
|
||||
}
|
||||
}
|
||||
Log.i("IconCacheJanitor", "Cleaned up "+filesCleanedUp+ " files");
|
||||
synchronized (IconCacheJanitor.class) {
|
||||
sJanitorFuture = null;
|
||||
sJanitorRan = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the janitor task, unless there was one running already or one has ran already
|
||||
*/
|
||||
public static void runJanitor() {
|
||||
synchronized (IconCacheJanitor.class) {
|
||||
if (sJanitorFuture != null || sJanitorRan) return;
|
||||
sJanitorFuture = PojavApplication.sExecutorService.submit(new IconCacheJanitor());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the janitor task to finish, if there is one running already
|
||||
* Note that the thread waiting must not be interrupted.
|
||||
*/
|
||||
public static void waitForJanitorToFinish() {
|
||||
synchronized (IconCacheJanitor.class) {
|
||||
if (sJanitorFuture == null) return;
|
||||
try {
|
||||
sJanitorFuture.get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
throw new RuntimeException("Should not happen!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.imagecache;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
/**
|
||||
* ModIconCache will call your view back when the image becomes available with this interface
|
||||
*/
|
||||
public interface ImageReceiver {
|
||||
void onImageAvailable(Bitmap image);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.imagecache;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ModIconCache {
|
||||
ThreadPoolExecutor cacheLoaderPool = new ThreadPoolExecutor(10,
|
||||
10,
|
||||
1000,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>());
|
||||
File cachePath;
|
||||
private final List<WeakReference<ImageReceiver>> mCancelledReceivers = new ArrayList<>();
|
||||
public ModIconCache() {
|
||||
cachePath = getImageCachePath();
|
||||
if(!cachePath.exists() && !cachePath.isFile() && Tools.DIR_CACHE.canWrite()) {
|
||||
if(!cachePath.mkdirs())
|
||||
throw new RuntimeException("Failed to create icon cache directory");
|
||||
}
|
||||
|
||||
}
|
||||
static File getImageCachePath() {
|
||||
return new File(Tools.DIR_CACHE, "mod_icons");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an image for a mod with the associated tag and URL to download it in case if its not cached
|
||||
* @param imageReceiver the receiver interface that would get called when the image loads
|
||||
* @param imageTag the tag of the image to keep track of it
|
||||
* @param imageUrl the URL of the image in case if it's not cached
|
||||
*/
|
||||
public void getImage(ImageReceiver imageReceiver, String imageTag, String imageUrl) {
|
||||
cacheLoaderPool.execute(new ReadFromDiskTask(this, imageReceiver, imageTag, imageUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the image obtainment task requested with this receiver as "cancelled". This means that
|
||||
* this receiver will not be called back and that some tasks related to this image may be
|
||||
* prevented from happening or interrupted.
|
||||
* @param imageReceiver the receiver to cancel
|
||||
*/
|
||||
public void cancelImage(ImageReceiver imageReceiver) {
|
||||
synchronized (mCancelledReceivers) {
|
||||
mCancelledReceivers.add(new WeakReference<>(imageReceiver));
|
||||
}
|
||||
}
|
||||
|
||||
boolean checkCancelled(ImageReceiver imageReceiver) {
|
||||
boolean isCanceled = false;
|
||||
synchronized (mCancelledReceivers) {
|
||||
Iterator<WeakReference<ImageReceiver>> iterator = mCancelledReceivers.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
WeakReference<ImageReceiver> reference = iterator.next();
|
||||
if (reference.get() == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
if(reference.get() == imageReceiver) {
|
||||
isCanceled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(isCanceled) Log.i("IconCache", "checkCancelled("+imageReceiver.hashCode()+") == true");
|
||||
return isCanceled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base64-encoded version of a cached icon by its tag.
|
||||
* Note: this functions performs I/O operations, and should not be called on the UI
|
||||
* thread.
|
||||
* @param imageTag the icon tag
|
||||
* @return the base64 encoded image or null if not cached
|
||||
*/
|
||||
|
||||
public static String getBase64Image(String imageTag) {
|
||||
File imagePath = new File(Tools.DIR_CACHE, "mod_icons/"+imageTag+".ca");
|
||||
Log.i("IconCache", "Creating base64 version of icon "+imageTag);
|
||||
if(!imagePath.canRead() || !imagePath.isFile()) {
|
||||
Log.i("IconCache", "Icon does not exist");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
try(FileInputStream fileInputStream = new FileInputStream(imagePath)) {
|
||||
byte[] imageBytes = IOUtils.toByteArray(fileInputStream);
|
||||
// reencode to png? who cares! our profile icon cache is an omnivore!
|
||||
// if some other launcher parses this and dies it is not our problem :troll:
|
||||
return "data:image/png;base64,"+ Base64.encodeToString(imageBytes, Base64.DEFAULT);
|
||||
}
|
||||
}catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.imagecache;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class ReadFromDiskTask implements Runnable {
|
||||
final ModIconCache iconCache;
|
||||
final ImageReceiver imageReceiver;
|
||||
final File cacheFile;
|
||||
final String imageUrl;
|
||||
|
||||
ReadFromDiskTask(ModIconCache iconCache, ImageReceiver imageReceiver, String cacheTag, String imageUrl) {
|
||||
this.iconCache = iconCache;
|
||||
this.imageReceiver = imageReceiver;
|
||||
this.cacheFile = new File(iconCache.cachePath, cacheTag+".ca");
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public void runDownloadTask() {
|
||||
iconCache.cacheLoaderPool.execute(new DownloadImageTask(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if(cacheFile.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
if(cacheFile.canRead()) {
|
||||
IconCacheJanitor.waitForJanitorToFinish();
|
||||
Bitmap bitmap = BitmapFactory.decodeFile(cacheFile.getAbsolutePath());
|
||||
if(bitmap != null) {
|
||||
Tools.runOnUiThread(()->{
|
||||
if(taskCancelled()) {
|
||||
bitmap.recycle(); // do not leak the bitmap if the task got cancelled right at the end
|
||||
return;
|
||||
}
|
||||
imageReceiver.onImageAvailable(bitmap);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(iconCache.cachePath.canWrite() &&
|
||||
!taskCancelled()) { // don't run the download task if the task got canceled
|
||||
runDownloadTask();
|
||||
}
|
||||
}
|
||||
@SuppressWarnings("BooleanMethodAlwaysInverted")
|
||||
public boolean taskCancelled() {
|
||||
return iconCache.checkCancelled(imageReceiver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||
|
||||
public class Constants {
|
||||
private Constants(){}
|
||||
|
||||
/** Types of modpack apis */
|
||||
public static final int SOURCE_MODRINTH = 0x0;
|
||||
public static final int SOURCE_CURSEFORGE = 0x1;
|
||||
public static final int SOURCE_TECHNIC = 0x2;
|
||||
|
||||
/** Modrinth api, file environments */
|
||||
public static final String MODRINTH_FILE_ENV_REQUIRED = "required";
|
||||
public static final String MODRINTH_FILE_ENV_OPTIONAL = "optional";
|
||||
public static final String MODRINTH_FILE_ENV_UNSUPPORTED = "unsupported";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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 boolean required;
|
||||
}
|
||||
public static class CurseMinecraft {
|
||||
public String version;
|
||||
public CurseModLoader[] modLoaders;
|
||||
}
|
||||
public static class CurseModLoader {
|
||||
public String id;
|
||||
public boolean primary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ModDetail extends ModItem {
|
||||
/* A cheap way to map from the front facing name to the underlying id */
|
||||
public String[] versionNames;
|
||||
public String [] mcVersionNames;
|
||||
public String[] versionUrls;
|
||||
public ModDetail(ModItem item, String[] versionNames, String[] mcVersionNames, String[] versionUrls) {
|
||||
super(item.apiSource, item.isModpack, item.id, item.title, item.description, item.imageUrl);
|
||||
this.versionNames = versionNames;
|
||||
this.mcVersionNames = mcVersionNames;
|
||||
this.versionUrls = versionUrls;
|
||||
|
||||
// Add the mc version to the version model
|
||||
for (int i=0; i<versionNames.length; i++){
|
||||
if (!versionNames[i].contains(mcVersionNames[i]))
|
||||
versionNames[i] += " - " + mcVersionNames[i];
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModDetail{" +
|
||||
"versionNames=" + Arrays.toString(versionNames) +
|
||||
", mcVersionNames=" + Arrays.toString(mcVersionNames) +
|
||||
", versionIds=" + Arrays.toString(versionUrls) +
|
||||
", id='" + id + '\'' +
|
||||
", title='" + title + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", imageUrl='" + imageUrl + '\'' +
|
||||
", apiSource=" + apiSource +
|
||||
", isModpack=" + isModpack +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class ModItem extends ModSource {
|
||||
|
||||
public String id;
|
||||
public String title;
|
||||
public String description;
|
||||
public String imageUrl;
|
||||
|
||||
public ModItem(int apiSource, boolean isModpack, String id, String title, String description, String imageUrl) {
|
||||
this.apiSource = apiSource;
|
||||
this.isModpack = isModpack;
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModItem{" +
|
||||
"id='" + id + '\'' +
|
||||
", title='" + title + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", imageUrl='" + imageUrl + '\'' +
|
||||
", apiSource=" + apiSource +
|
||||
", isModpack=" + isModpack +
|
||||
'}';
|
||||
}
|
||||
|
||||
public String getIconCacheTag() {
|
||||
return apiSource+"_"+id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||
|
||||
public abstract class ModSource {
|
||||
public int apiSource;
|
||||
public boolean isModpack;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* POJO to represent the modrinth index inside mrpacks
|
||||
*/
|
||||
public class ModrinthIndex {
|
||||
|
||||
|
||||
public int formatVersion;
|
||||
public String game;
|
||||
public String versionId;
|
||||
public String name;
|
||||
public String summary;
|
||||
|
||||
public ModrinthIndexFile[] files;
|
||||
public Map<String, String> dependencies;
|
||||
|
||||
|
||||
public static class ModrinthIndexFile {
|
||||
public String path;
|
||||
public String[] downloads;
|
||||
public int fileSize;
|
||||
|
||||
public ModrinthIndexFileHashes hashes;
|
||||
|
||||
@Nullable public ModrinthIndexFileEnv env;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModrinthIndexFile{" +
|
||||
"path='" + path + '\'' +
|
||||
", downloads=" + Arrays.toString(downloads) +
|
||||
", fileSize=" + fileSize +
|
||||
", hashes=" + hashes +
|
||||
'}';
|
||||
}
|
||||
|
||||
public static class ModrinthIndexFileHashes {
|
||||
public String sha1;
|
||||
public String sha512;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModrinthIndexFileHashes{" +
|
||||
"sha1='" + sha1 + '\'' +
|
||||
", sha512='" + sha512 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class ModrinthIndexFileEnv {
|
||||
public String client;
|
||||
public String server;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModrinthIndexFileEnv{" +
|
||||
"client='" + client + '\'' +
|
||||
", server='" + server + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ModrinthIndex{" +
|
||||
"formatVersion=" + formatVersion +
|
||||
", game='" + game + '\'' +
|
||||
", versionId='" + versionId + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", summary='" + summary + '\'' +
|
||||
", files=" + Arrays.toString(files) +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Search filters, passed to APIs
|
||||
*/
|
||||
public class SearchFilters {
|
||||
public boolean isModpack;
|
||||
public String name;
|
||||
@Nullable public String mcVersion;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.kdt.pojavlaunch.modloaders.modpacks.models;
|
||||
|
||||
public class SearchResult {
|
||||
public int totalResultCount;
|
||||
public ModItem[] results;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class RTSpinnerAdapter implements SpinnerAdapter {
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = convertView != null?
|
||||
convertView:
|
||||
LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent,false);
|
||||
LayoutInflater.from(mContext).inflate(R.layout.item_simple_list_1, parent,false);
|
||||
|
||||
Runtime runtime = mRuntimes.get(position);
|
||||
if(position == mRuntimes.size() - 1 ){
|
||||
|
||||
@@ -31,15 +31,10 @@ public class ProfileAdapter extends BaseAdapter {
|
||||
private Map<String, MinecraftProfile> mProfiles;
|
||||
private final MinecraftProfile dummy = new MinecraftProfile();
|
||||
private List<String> mProfileList;
|
||||
private final ProfileAdapterExtra[] mExtraEntires;
|
||||
private ProfileAdapterExtra[] mExtraEntires;
|
||||
|
||||
public ProfileAdapter(Context context, ProfileAdapterExtra[] extraEntries) {
|
||||
ProfileIconCache.initDefault(context);
|
||||
LauncherProfiles.update();
|
||||
mProfiles = new HashMap<>(LauncherProfiles.mainProfileJson.profiles);
|
||||
if(extraEntries == null) mExtraEntires = new ProfileAdapterExtra[0];
|
||||
else mExtraEntires = extraEntries;
|
||||
mProfileList = new ArrayList<>(Arrays.asList(mProfiles.keySet().toArray(new String[0])));
|
||||
public ProfileAdapter(ProfileAdapterExtra[] extraEntries) {
|
||||
reloadProfiles(extraEntries);
|
||||
}
|
||||
/*
|
||||
* Gets how much profiles are loaded in the adapter right now
|
||||
@@ -67,6 +62,8 @@ public class ProfileAdapter extends BaseAdapter {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int resolveProfileIndex(String name) {
|
||||
return mProfileList.indexOf(name);
|
||||
}
|
||||
@@ -98,11 +95,7 @@ public class ProfileAdapter extends BaseAdapter {
|
||||
|
||||
MinecraftProfile minecraftProfile = mProfiles.get(nm);
|
||||
if(minecraftProfile == null) minecraftProfile = dummy;
|
||||
Drawable cachedIcon = ProfileIconCache.getCachedIcon(nm);
|
||||
|
||||
if(cachedIcon == null) {
|
||||
cachedIcon = ProfileIconCache.tryResolveIcon(v.getResources(), nm, minecraftProfile.icon);
|
||||
}
|
||||
Drawable cachedIcon = ProfileIconCache.fetchIcon(v.getResources(), nm, minecraftProfile.icon);
|
||||
extendedTextView.setCompoundDrawablesRelative(cachedIcon, null, extendedTextView.getCompoundsDrawables()[2], null);
|
||||
|
||||
if(Tools.isValidString(minecraftProfile.name))
|
||||
@@ -134,4 +127,19 @@ public class ProfileAdapter extends BaseAdapter {
|
||||
extendedTextView.setText(extra.name);
|
||||
extendedTextView.setBackgroundColor(Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
/** Reload profiles from the file */
|
||||
public void reloadProfiles(){
|
||||
LauncherProfiles.load();
|
||||
mProfiles = new HashMap<>(LauncherProfiles.mainProfileJson.profiles);
|
||||
mProfileList = new ArrayList<>(Arrays.asList(mProfiles.keySet().toArray(new String[0])));
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/** Reload profiles from the file, with additional extra entries */
|
||||
public void reloadProfiles(ProfileAdapterExtra[] extraEntries) {
|
||||
if(extraEntries == null) mExtraEntires = new ProfileAdapterExtra[0];
|
||||
else mExtraEntires = extraEntries;
|
||||
this.reloadProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,99 @@
|
||||
package net.kdt.pojavlaunch.profiles;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ProfileIconCache {
|
||||
private static final String BASE64_PNG_HEADER = "data:image/png;base64,";
|
||||
// Data header format: data:<mime>;<encoding>,<data>
|
||||
private static final String DATA_HEADER = "data:";
|
||||
private static final String FALLBACK_ICON_NAME = "default";
|
||||
private static final Map<String, Drawable> sIconCache = new HashMap<>();
|
||||
private static Drawable sDefaultIcon;
|
||||
private static final Map<String, Drawable> sStaticIconCache = new HashMap<>();
|
||||
|
||||
|
||||
public static void initDefault(Context context) {
|
||||
if(sDefaultIcon != null) return;
|
||||
sDefaultIcon = ResourcesCompat.getDrawable(context.getResources(), R.mipmap.ic_launcher_foreground, null);
|
||||
if(sDefaultIcon != null) sDefaultIcon.setBounds(0, 0, 10, 10);
|
||||
/**
|
||||
* Fetch an icon from the cache, or load it if it's not cached.
|
||||
* @param resources the Resources object, used for creating drawables
|
||||
* @param key the profile key
|
||||
* @param icon the profile icon data (stored in the icon field of MinecraftProfile)
|
||||
* @return an icon drawable
|
||||
*/
|
||||
public static @NonNull Drawable fetchIcon(Resources resources, @NonNull String key, @Nullable String icon) {
|
||||
Drawable cachedIcon = sIconCache.get(key);
|
||||
if(cachedIcon != null) return cachedIcon;
|
||||
if(icon != null && icon.startsWith(DATA_HEADER)) return fetchDataIcon(resources, key, icon);
|
||||
else return fetchStaticIcon(resources, key, icon);
|
||||
}
|
||||
|
||||
public static Drawable getCachedIcon(String key) {
|
||||
return sIconCache.get(key);
|
||||
private static Drawable fetchDataIcon(Resources resources, String key, @NonNull String icon) {
|
||||
Drawable dataIcon = readDataIcon(resources, icon);
|
||||
if(dataIcon == null) dataIcon = fetchFallbackIcon(resources);
|
||||
sIconCache.put(key, dataIcon);
|
||||
return dataIcon;
|
||||
}
|
||||
|
||||
public static Drawable submitIcon(Resources resources, String key, String base64) {
|
||||
byte[] pngBytes = Base64.decode(base64, Base64.DEFAULT);
|
||||
Drawable drawable = new BitmapDrawable(resources, BitmapFactory.decodeByteArray(pngBytes, 0, pngBytes.length));
|
||||
sIconCache.put(key, drawable);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public static Drawable tryResolveIcon(Resources resources, String profileName, String b64Icon) {
|
||||
Drawable icon;
|
||||
if (b64Icon != null && b64Icon.startsWith(BASE64_PNG_HEADER)) {
|
||||
icon = ProfileIconCache.submitIcon(resources, profileName, b64Icon.substring(BASE64_PNG_HEADER.length()));
|
||||
}else{
|
||||
Log.i("IconParser","Unsupported icon: "+b64Icon);
|
||||
icon = ProfileIconCache.pushDefaultIcon(profileName);
|
||||
private static Drawable fetchStaticIcon(Resources resources, String key, @Nullable String icon) {
|
||||
Drawable staticIcon = sStaticIconCache.get(icon);
|
||||
if(staticIcon == null) {
|
||||
if(icon != null) staticIcon = getStaticIcon(resources, icon);
|
||||
if(staticIcon == null) staticIcon = fetchFallbackIcon(resources);
|
||||
sStaticIconCache.put(icon, staticIcon);
|
||||
}
|
||||
return icon;
|
||||
sIconCache.put(key, staticIcon);
|
||||
return staticIcon;
|
||||
}
|
||||
|
||||
public static Drawable pushDefaultIcon(String key) {
|
||||
sIconCache.put(key, sDefaultIcon);
|
||||
private static @NonNull Drawable fetchFallbackIcon(Resources resources) {
|
||||
Drawable fallbackIcon = sStaticIconCache.get(FALLBACK_ICON_NAME);
|
||||
if(fallbackIcon == null) {
|
||||
fallbackIcon = Objects.requireNonNull(getStaticIcon(resources, FALLBACK_ICON_NAME));
|
||||
sStaticIconCache.put(FALLBACK_ICON_NAME, fallbackIcon);
|
||||
}
|
||||
return fallbackIcon;
|
||||
}
|
||||
|
||||
return sDefaultIcon;
|
||||
private static Drawable getStaticIcon(Resources resources, @NonNull String icon) {
|
||||
int staticIconResource = getStaticIconResource(icon);
|
||||
if(staticIconResource == -1) return null;
|
||||
return ResourcesCompat.getDrawable(resources, staticIconResource, null);
|
||||
}
|
||||
|
||||
private static int getStaticIconResource(String icon) {
|
||||
switch (icon) {
|
||||
case "default": return R.drawable.ic_pojav_full;
|
||||
case "fabric": return R.drawable.ic_fabric;
|
||||
case "quilt": return R.drawable.ic_quilt;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static Drawable readDataIcon(Resources resources, String icon) {
|
||||
byte[] iconData = extractIconData(icon);
|
||||
if(iconData == null) return null;
|
||||
Bitmap iconBitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
|
||||
if(iconBitmap == null) return null;
|
||||
return new BitmapDrawable(resources, iconBitmap);
|
||||
}
|
||||
|
||||
private static byte[] extractIconData(String inputString) {
|
||||
int firstSemicolon = inputString.indexOf(';');
|
||||
int commaAfterSemicolon = inputString.indexOf(',');
|
||||
if(firstSemicolon == -1 || commaAfterSemicolon == -1) return null;
|
||||
String dataEncoding = inputString.substring(firstSemicolon+1, commaAfterSemicolon);
|
||||
if(!dataEncoding.equals("base64")) return null;
|
||||
return Base64.decode(inputString.substring(commaAfterSemicolon+1), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.kdt.pojavlaunch.progresskeeper;
|
||||
|
||||
import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB;
|
||||
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
|
||||
public class DownloaderProgressWrapper implements Tools.DownloaderFeedback {
|
||||
|
||||
private final int mProgressString;
|
||||
private final String mProgressRecord;
|
||||
public String extraString = null;
|
||||
|
||||
/**
|
||||
* A simple wrapper to send the downloader progress to ProgressKeeper
|
||||
* @param progressString the string that will be used in the progress reporter
|
||||
* @param progressRecord the record for ProgressKeeper
|
||||
*/
|
||||
public DownloaderProgressWrapper(int progressString, String progressRecord) {
|
||||
this.mProgressString = progressString;
|
||||
this.mProgressRecord = progressRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(int curr, int max) {
|
||||
Object[] va;
|
||||
if(extraString != null) {
|
||||
va = new Object[3];
|
||||
va[0] = extraString;
|
||||
va[1] = curr/BYTE_TO_MB;
|
||||
va[2] = max/BYTE_TO_MB;
|
||||
}
|
||||
else {
|
||||
va = new Object[2];
|
||||
va[0] = curr/BYTE_TO_MB;
|
||||
va[1] = max/BYTE_TO_MB;
|
||||
}
|
||||
// the allocations are fine because thats how java implements variadic arguments in bytecode: an array of whatever
|
||||
ProgressKeeper.submitProgress(mProgressRecord, (int) Math.max((float)curr/max*100,0), mProgressString, va);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.value.NotificationConstants;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
@@ -38,14 +39,15 @@ public class GameService extends Service {
|
||||
}
|
||||
Intent killIntent = new Intent(getApplicationContext(), GameService.class);
|
||||
killIntent.putExtra("kill", true);
|
||||
PendingIntent pendingKillIntent = PendingIntent.getService(this, 0, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
PendingIntent pendingKillIntent = PendingIntent.getService(this, NotificationConstants.PENDINGINTENT_CODE_KILL_GAME_SERVICE
|
||||
, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "channel_id")
|
||||
.setContentTitle(getString(R.string.lazy_service_default_title))
|
||||
.setContentText(getString(R.string.notification_game_runs))
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent)
|
||||
.setSmallIcon(R.drawable.notif_icon)
|
||||
.setNotificationSilent();
|
||||
startForeground(2, notificationBuilder.build());
|
||||
startForeground(NotificationConstants.NOTIFICATION_ID_GAME_SERVICE, notificationBuilder.build());
|
||||
return START_NOT_STICKY; // non-sticky so android wont try restarting the game after the user uses the "Quit" button
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import net.kdt.pojavlaunch.R;
|
||||
import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
|
||||
import net.kdt.pojavlaunch.progresskeeper.TaskCountListener;
|
||||
import net.kdt.pojavlaunch.value.NotificationConstants;
|
||||
|
||||
/**
|
||||
* Lazy service which allows the process not to get killed.
|
||||
@@ -42,7 +43,8 @@ public class ProgressService extends Service implements TaskCountListener {
|
||||
notificationManagerCompat = NotificationManagerCompat.from(getApplicationContext());
|
||||
Intent killIntent = new Intent(getApplicationContext(), ProgressService.class);
|
||||
killIntent.putExtra("kill", true);
|
||||
PendingIntent pendingKillIntent = PendingIntent.getService(this, 0, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
PendingIntent pendingKillIntent = PendingIntent.getService(this, NotificationConstants.PENDINGINTENT_CODE_KILL_PROGRESS_SERVICE
|
||||
, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
mNotificationBuilder = new NotificationCompat.Builder(this, "channel_id")
|
||||
.setContentTitle(getString(R.string.lazy_service_default_title))
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent)
|
||||
@@ -62,7 +64,7 @@ public class ProgressService extends Service implements TaskCountListener {
|
||||
}
|
||||
Log.d("ProgressService", "Started!");
|
||||
mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, ProgressKeeper.getTaskCount()));
|
||||
startForeground(1, mNotificationBuilder.build());
|
||||
startForeground(NotificationConstants.NOTIFICATION_ID_PROGRESS_SERVICE, mNotificationBuilder.build());
|
||||
if(ProgressKeeper.getTaskCount() < 1) stopSelf();
|
||||
else ProgressKeeper.addTaskCountListener(this, false);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.kdt.pojavlaunch.tasks;
|
||||
|
||||
import static net.kdt.pojavlaunch.PojavApplication.sExecutorService;
|
||||
import static net.kdt.pojavlaunch.Tools.BYTE_TO_MB;
|
||||
import static net.kdt.pojavlaunch.utils.DownloadUtils.downloadFileMonitored;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -40,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AsyncMinecraftDownloader {
|
||||
private static final float BYTE_TO_MB = 1024 * 1024;
|
||||
|
||||
public static final String MINECRAFT_RES = "https://resources.download.minecraft.net/";
|
||||
|
||||
/* Allows each downloading thread to have its own RECYCLED buffer */
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
package net.kdt.pojavlaunch.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class FileUtils {
|
||||
public static boolean exists(String filePath){
|
||||
return new File(filePath).exists();
|
||||
}
|
||||
|
||||
public static String getFileName(String pathOrUrl) {
|
||||
int lastSlashIndex = pathOrUrl.lastIndexOf('/');
|
||||
if(lastSlashIndex == -1) return null;
|
||||
return pathOrUrl.substring(lastSlashIndex);
|
||||
}
|
||||
}
|
||||
@@ -285,6 +285,9 @@ public class JREUtils {
|
||||
purgeArg(userArgs,"-d32");
|
||||
purgeArg(userArgs,"-d64");
|
||||
purgeArg(userArgs, "-Xint");
|
||||
purgeArg(userArgs, "-XX:+UseTransparentHugePages");
|
||||
purgeArg(userArgs, "-XX:+UseLargePagesInMetaspace");
|
||||
purgeArg(userArgs, "-XX:+UseLargePages");
|
||||
purgeArg(userArgs, "-Dorg.lwjgl.opengl.libname");
|
||||
|
||||
//Add automatically generated args
|
||||
@@ -332,6 +335,7 @@ public class JREUtils {
|
||||
ArrayList<String> overridableArguments = new ArrayList<>(Arrays.asList(
|
||||
"-Djava.home=" + runtimeHome,
|
||||
"-Djava.io.tmpdir=" + Tools.DIR_CACHE.getAbsolutePath(),
|
||||
"-Djna.boot.library.path=" + NATIVE_LIB_DIR,
|
||||
"-Duser.home=" + Tools.DIR_GAME_HOME,
|
||||
"-Duser.language=" + System.getProperty("user.language"),
|
||||
"-Dos.name=Linux",
|
||||
@@ -340,11 +344,11 @@ public class JREUtils {
|
||||
"-Dpojav.path.private.account=" + Tools.DIR_ACCOUNT_NEW,
|
||||
"-Duser.timezone=" + TimeZone.getDefault().getID(),
|
||||
|
||||
"-Dorg.lwjgl.vulkan.libname=libvulkan.so",
|
||||
//LWJGL 3 DEBUG FLAGS
|
||||
//"-Dorg.lwjgl.util.Debug=true",
|
||||
//"-Dorg.lwjgl.util.DebugFunctions=true",
|
||||
//"-Dorg.lwjgl.util.DebugLoader=true",
|
||||
"-Dorg.lwjgl.util.NoChecks=true",
|
||||
// GLFW Stub width height
|
||||
"-Dglfwstub.windowWidth=" + Tools.getDisplayFriendlyRes(currentDisplayMetrics.widthPixels, LauncherPreferences.PREF_SCALE_FACTOR/100F),
|
||||
"-Dglfwstub.windowHeight=" + Tools.getDisplayFriendlyRes(currentDisplayMetrics.heightPixels, LauncherPreferences.PREF_SCALE_FACTOR/100F),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package net.kdt.pojavlaunch.utils;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class ZipUtils {
|
||||
/**
|
||||
* Gets an InputStream for a given ZIP entry, throwing an IOException if the ZIP entry does not
|
||||
* exist.
|
||||
* @param zipFile The ZipFile to get the entry from
|
||||
* @param entryPath The full path inside of the ZipFile
|
||||
* @return The InputStream provided by the ZipFile
|
||||
* @throws IOException if the entry was not found
|
||||
*/
|
||||
public static InputStream getEntryStream(ZipFile zipFile, String entryPath) throws IOException{
|
||||
ZipEntry entry = zipFile.getEntry(entryPath);
|
||||
if(entry == null) throw new IOException("No entry in ZIP file: "+entryPath);
|
||||
return zipFile.getInputStream(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all files in a ZipFile inside of a given directory to a given destination directory
|
||||
* How to specify dirName:
|
||||
* If you want to extract all files in the ZipFile, specify ""
|
||||
* If you want to extract a single directory, specify its full path followed by a trailing /
|
||||
* @param zipFile The ZipFile to extract files from
|
||||
* @param dirName The directory to extract the files from
|
||||
* @param destination The destination directory to extract the files into
|
||||
* @throws IOException if it was not possible to create a directory or file extraction failed
|
||||
*/
|
||||
public static void zipExtract(ZipFile zipFile, String dirName, File destination) throws IOException {
|
||||
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
|
||||
|
||||
int dirNameLen = dirName.length();
|
||||
while(zipEntries.hasMoreElements()) {
|
||||
ZipEntry zipEntry = zipEntries.nextElement();
|
||||
String entryName = zipEntry.getName();
|
||||
if(!entryName.startsWith(dirName) || zipEntry.isDirectory()) continue;
|
||||
File zipDestination = new File(destination, entryName.substring(dirNameLen));
|
||||
File parent = zipDestination.getParentFile();
|
||||
if(parent != null && !parent.exists())
|
||||
if(!parent.mkdirs()) throw new IOException("Failed to create "+parent.getAbsolutePath());
|
||||
try (InputStream inputStream = zipFile.getInputStream(zipEntry);
|
||||
OutputStream outputStream =
|
||||
new FileOutputStream(zipDestination)) {
|
||||
IOUtils.copy(inputStream, outputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.kdt.pojavlaunch.value;
|
||||
|
||||
public class NotificationConstants {
|
||||
public static final int NOTIFICATION_ID_PROGRESS_SERVICE = 1;
|
||||
public static final int NOTIFICATION_ID_GAME_SERVICE = 2;
|
||||
public static final int NOTIFICATION_ID_DOWNLOAD_LISTENER = 3;
|
||||
public static final int NOTIFICATION_ID_SHOW_ERROR = 4;
|
||||
public static final int PENDINGINTENT_CODE_KILL_PROGRESS_SERVICE = 1;
|
||||
public static final int PENDINGINTENT_CODE_KILL_GAME_SERVICE = 2;
|
||||
public static final int PENDINGINTENT_CODE_DOWNLOAD_SERVICE = 3;
|
||||
public static final int PENDINGINTENT_CODE_SHOW_ERROR = 4;
|
||||
}
|
||||
@@ -8,45 +8,77 @@ import net.kdt.pojavlaunch.Tools;
|
||||
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class LauncherProfiles {
|
||||
public static MinecraftLauncherProfiles mainProfileJson;
|
||||
public static final File launcherProfilesFile = new File(Tools.DIR_GAME_NEW, "launcher_profiles.json");
|
||||
public static MinecraftLauncherProfiles update() {
|
||||
try {
|
||||
if (mainProfileJson == null) {
|
||||
if (launcherProfilesFile.exists()) {
|
||||
mainProfileJson = Tools.GLOBAL_GSON.fromJson(Tools.read(launcherProfilesFile.getAbsolutePath()), MinecraftLauncherProfiles.class);
|
||||
if(mainProfileJson.profiles == null) mainProfileJson.profiles = new HashMap<>();
|
||||
else if(LauncherProfiles.normalizeProfileIds(mainProfileJson)){
|
||||
LauncherProfiles.update();
|
||||
}
|
||||
} else {
|
||||
mainProfileJson = new MinecraftLauncherProfiles();
|
||||
mainProfileJson.profiles = new HashMap<>();
|
||||
}
|
||||
} else {
|
||||
Tools.write(launcherProfilesFile.getAbsolutePath(), mainProfileJson.toJson());
|
||||
}
|
||||
private static final File launcherProfilesFile = new File(Tools.DIR_GAME_NEW, "launcher_profiles.json");
|
||||
|
||||
// insertMissing();
|
||||
return mainProfileJson;
|
||||
} catch (Throwable th) {
|
||||
throw new RuntimeException(th);
|
||||
/** Reload the profile from the file, creating a default one if necessary */
|
||||
public static void load(){
|
||||
if (launcherProfilesFile.exists()) {
|
||||
try {
|
||||
mainProfileJson = Tools.GLOBAL_GSON.fromJson(Tools.read(launcherProfilesFile.getAbsolutePath()), MinecraftLauncherProfiles.class);
|
||||
} catch (IOException e) {
|
||||
Log.e(LauncherProfiles.class.toString(), "Failed to load file: ", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill with default
|
||||
if (mainProfileJson == null) mainProfileJson = new MinecraftLauncherProfiles();
|
||||
if (mainProfileJson.profiles == null) mainProfileJson.profiles = new HashMap<>();
|
||||
if (mainProfileJson.profiles.size() == 0)
|
||||
mainProfileJson.profiles.put(UUID.randomUUID().toString(), MinecraftProfile.getDefaultProfile());
|
||||
|
||||
// Normalize profile names from mod installers
|
||||
if(normalizeProfileIds(mainProfileJson)){
|
||||
write();
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply the current configuration into a file */
|
||||
public static void write() {
|
||||
try {
|
||||
Tools.write(launcherProfilesFile.getAbsolutePath(), mainProfileJson.toJson());
|
||||
} catch (IOException e) {
|
||||
Log.e(LauncherProfiles.class.toString(), "Failed to write profile file", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull MinecraftProfile getCurrentProfile() {
|
||||
if(mainProfileJson == null) LauncherProfiles.update();
|
||||
if(mainProfileJson == null) LauncherProfiles.load();
|
||||
String defaultProfileName = LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, "");
|
||||
MinecraftProfile profile = mainProfileJson.profiles.get(defaultProfileName);
|
||||
if(profile == null) throw new RuntimeException("The current profile stopped existing :(");
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new profile into the profile map
|
||||
* @param minecraftProfile the profile to insert
|
||||
*/
|
||||
public static void insertMinecraftProfile(MinecraftProfile minecraftProfile) {
|
||||
mainProfileJson.profiles.put(getFreeProfileKey(), minecraftProfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick an unused normalized key to store a new profile with
|
||||
* @return an unused key
|
||||
*/
|
||||
public static String getFreeProfileKey() {
|
||||
Map<String, MinecraftProfile> profileMap = mainProfileJson.profiles;
|
||||
String freeKey = UUID.randomUUID().toString();
|
||||
while(profileMap.get(freeKey) != null) freeKey = UUID.randomUUID().toString();
|
||||
return freeKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* For all keys to be UUIDs, effectively isolating profile created by installers
|
||||
* This avoids certain profiles to be erased by the installer
|
||||
@@ -67,13 +99,9 @@ public class LauncherProfiles {
|
||||
}
|
||||
|
||||
// Swap the new keys
|
||||
for(String profileKey: keys){
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
while(launcherProfiles.profiles.containsKey(uuid)) {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
launcherProfiles.profiles.put(uuid, launcherProfiles.profiles.get(profileKey));
|
||||
for(String profileKey : keys){
|
||||
MinecraftProfile currentProfile = launcherProfiles.profiles.get(profileKey);
|
||||
insertMinecraftProfile(currentProfile);
|
||||
launcherProfiles.profiles.remove(profileKey);
|
||||
hasNormalized = true;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ public class MinecraftProfile {
|
||||
return TEMPLATE;
|
||||
}
|
||||
|
||||
public static MinecraftProfile getDefaultProfile(){
|
||||
MinecraftProfile defaultProfile = new MinecraftProfile();
|
||||
defaultProfile.name = "Default";
|
||||
defaultProfile.lastVersionId = "1.7.10";
|
||||
return defaultProfile;
|
||||
}
|
||||
|
||||
public MinecraftProfile(){}
|
||||
|
||||
public MinecraftProfile(MinecraftProfile profile){
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
#include "utils.h"
|
||||
#include "ctxbridges/gl_bridge.h"
|
||||
|
||||
#define GLFW_CLIENT_API 0x22001
|
||||
/* Consider GLFW_NO_API as Vulkan API */
|
||||
#define GLFW_NO_API 0
|
||||
#define GLFW_OPENGL_API 0x30001
|
||||
|
||||
|
||||
struct PotatoBridge {
|
||||
|
||||
/* EGLContext */ void* eglContextOld;
|
||||
@@ -46,15 +52,10 @@ struct PotatoBridge potatoBridge;
|
||||
|
||||
#define RENDERER_GL4ES 1
|
||||
#define RENDERER_VK_ZINK 2
|
||||
#define RENDERER_VULKAN 4
|
||||
|
||||
void* gbuffer;
|
||||
|
||||
void pojav_openGLOnLoad() {
|
||||
}
|
||||
void pojav_openGLOnUnload() {
|
||||
|
||||
}
|
||||
|
||||
void pojavTerminate() {
|
||||
printf("EGLBridge: Terminating\n");
|
||||
|
||||
@@ -225,7 +226,10 @@ int pojavInit() {
|
||||
pojav_environ->savedWidth = ANativeWindow_getWidth(pojav_environ->pojavWindow);
|
||||
pojav_environ->savedHeight = ANativeWindow_getHeight(pojav_environ->pojavWindow);
|
||||
ANativeWindow_setBuffersGeometry(pojav_environ->pojavWindow,pojav_environ->savedWidth,pojav_environ->savedHeight,AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pojavInitOpenGL() {
|
||||
// Only affects GL4ES as of now
|
||||
const char *forceVsync = getenv("FORCE_VSYNC");
|
||||
if (strcmp(forceVsync, "true") == 0)
|
||||
@@ -269,6 +273,25 @@ int pojavInit() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void pojavSetWindowHint(int hint, int value) {
|
||||
if (hint != GLFW_CLIENT_API) return;
|
||||
switch (value) {
|
||||
case GLFW_NO_API:
|
||||
pojav_environ->config_renderer = RENDERER_VULKAN;
|
||||
/* Nothing to do: initialization is handled in Java-side */
|
||||
// pojavInitVulkan();
|
||||
break;
|
||||
case GLFW_OPENGL_API:
|
||||
/* Nothing to do: initialization is called in pojavCreateContext */
|
||||
// pojavInitOpenGL();
|
||||
break;
|
||||
default:
|
||||
printf("GLFW: Unimplemented API 0x%x\n", value);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
ANativeWindow_Buffer buf;
|
||||
int32_t stride;
|
||||
bool stopSwapBuffers;
|
||||
@@ -344,6 +367,12 @@ Java_org_lwjgl_glfw_GLFW_nativeEglDetachOnCurrentThread(JNIEnv *env, jclass claz
|
||||
*/
|
||||
|
||||
void* pojavCreateContext(void* contextSrc) {
|
||||
if (pojav_environ->config_renderer == RENDERER_VULKAN) {
|
||||
return (void *)pojav_environ->pojavWindow;
|
||||
}
|
||||
|
||||
pojavInitOpenGL();
|
||||
|
||||
if (pojav_environ->config_renderer == RENDERER_GL4ES) {
|
||||
/*const EGLint ctx_attribs[] = {
|
||||
EGL_CONTEXT_CLIENT_VERSION, atoi(getenv("LIBGL_ES")),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdatomic.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "utils.h"
|
||||
@@ -139,7 +140,8 @@ void pojavPumpEvents(void* window) {
|
||||
if((pojav_environ->cLastX != pojav_environ->cursorX || pojav_environ->cLastY != pojav_environ->cursorY) && pojav_environ->GLFW_invoke_CursorPos) {
|
||||
pojav_environ->cLastX = pojav_environ->cursorX;
|
||||
pojav_environ->cLastY = pojav_environ->cursorY;
|
||||
pojav_environ->GLFW_invoke_CursorPos(window, pojav_environ->cursorX, pojav_environ->cursorY);
|
||||
pojav_environ->GLFW_invoke_CursorPos(window, floor(pojav_environ->cursorX),
|
||||
floor(pojav_environ->cursorY));
|
||||
}
|
||||
|
||||
// The out target index is updated by the rewinder
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_nanovg.so
Normal file
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_nanovg.so
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_tinyfd.so
Normal file
BIN
app_pojavlauncher/src/main/jniLibs/arm64-v8a/liblwjgl_tinyfd.so
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user