Merge pull request 'Make Xposed Recovery Menu look better :P' (#1) from kmmiio99o/KettuXposed:master into master
Reviewed-on: https://codeberg.org/cocobo1/KettuXposed/pulls/1
This commit is contained in:
commit
27e4917c34
1 changed files with 658 additions and 64 deletions
|
|
@ -1,8 +1,25 @@
|
|||
package io.github.revenge.xposed.modules
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.setPadding
|
||||
import de.robv.android.xposed.XC_MethodReplacement
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
|
|
@ -10,11 +27,12 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage
|
|||
import io.github.revenge.xposed.Module
|
||||
import io.github.revenge.xposed.Utils.Companion.reloadApp
|
||||
import io.github.revenge.xposed.Utils.Log
|
||||
import io.github.revenge.xposed.Constants
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
object LogBoxModule : Module() {
|
||||
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
|
||||
|
|
@ -45,7 +63,7 @@ object LogBoxModule : Module() {
|
|||
try {
|
||||
Log.e("onContext called with context: $context")
|
||||
contextForMenu = context
|
||||
|
||||
|
||||
val possibleClasses = listOf(
|
||||
"com.facebook.react.devsupport.BridgeDevSupportManager",
|
||||
"com.facebook.react.devsupport.BridgelessDevSupportManager",
|
||||
|
|
@ -53,7 +71,7 @@ object LogBoxModule : Module() {
|
|||
"com.facebook.react.devsupport.DevSupportManagerBase",
|
||||
"com.facebook.react.devsupport.DefaultDevSupportManager"
|
||||
)
|
||||
|
||||
|
||||
var foundAny = false
|
||||
possibleClasses.forEach { className ->
|
||||
try {
|
||||
|
|
@ -65,14 +83,14 @@ object LogBoxModule : Module() {
|
|||
Log.e("Class not found: $className - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!foundAny) {
|
||||
tryFindDevSupportClasses(context)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun tryFindDevSupportClasses(context: Context) {
|
||||
try {
|
||||
val dexFile = packageParam.classLoader.javaClass.getDeclaredField("pathList")
|
||||
|
|
@ -85,15 +103,14 @@ object LogBoxModule : Module() {
|
|||
|
||||
private fun hookDevSupportManager(clazz: Class<*>, context: Context) {
|
||||
Log.e("Attempting to hook ${clazz.name}")
|
||||
|
||||
// List all methods to see what's available
|
||||
|
||||
Log.e("Available methods in ${clazz.simpleName}:")
|
||||
clazz.methods.forEach { method ->
|
||||
if (method.name.contains("Dev") || method.name.contains("Reload") || method.name.contains("Options")) {
|
||||
Log.e(" - ${method.name}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
try {
|
||||
val handleReloadJSMethod = clazz.methods.firstOrNull { it.name == "handleReloadJS" }
|
||||
|
|
@ -105,7 +122,6 @@ object LogBoxModule : Module() {
|
|||
return null
|
||||
}
|
||||
})
|
||||
} else {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Failed to hook handleReloadJS: ${e.message}")
|
||||
|
|
@ -126,10 +142,10 @@ object LogBoxModule : Module() {
|
|||
} catch (e: Exception) {
|
||||
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
|
||||
}
|
||||
|
||||
|
||||
val finalContext = activityContext ?: contextForMenu ?: context
|
||||
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
|
||||
|
||||
|
||||
showRecoveryMenu(finalContext)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
@ -137,7 +153,6 @@ object LogBoxModule : Module() {
|
|||
return null
|
||||
}
|
||||
})
|
||||
} else {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
|
@ -150,9 +165,9 @@ object LogBoxModule : Module() {
|
|||
Log.e("getContextFromDevSupport: instance is null")
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
return try {
|
||||
|
||||
|
||||
// what if we just did this and searched for literally everything
|
||||
val helpers = listOf(
|
||||
"mReactInstanceDevHelper",
|
||||
|
|
@ -160,7 +175,7 @@ object LogBoxModule : Module() {
|
|||
"mReactInstanceManager",
|
||||
"mApplicationContext"
|
||||
)
|
||||
|
||||
|
||||
for (helperName in helpers) {
|
||||
try {
|
||||
Log.e("Trying field: $helperName")
|
||||
|
|
@ -169,22 +184,22 @@ object LogBoxModule : Module() {
|
|||
Log.e("Field $helperName not found, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
val helper = helperField.get(instance)
|
||||
if (helper == null) {
|
||||
Log.e("Field $helperName is null, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if (helper is Context) {
|
||||
Log.e("Field $helperName is a Context, returning it")
|
||||
return helper
|
||||
}
|
||||
|
||||
val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull {
|
||||
it.name == "getCurrentActivity"
|
||||
|
||||
val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull {
|
||||
it.name == "getCurrentActivity"
|
||||
}
|
||||
|
||||
|
||||
if (getCurrentActivityMethod != null) {
|
||||
val ctx = getCurrentActivityMethod.invoke(helper) as? Context
|
||||
if (ctx != null) {
|
||||
|
|
@ -196,7 +211,7 @@ object LogBoxModule : Module() {
|
|||
Log.e("Error trying $helperName: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Log.e("Could not get context from DevSupport object using any method")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -205,22 +220,157 @@ object LogBoxModule : Module() {
|
|||
}
|
||||
}
|
||||
|
||||
// material design helper functions
|
||||
private fun dpToPx(context: Context, dp: Int): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dp.toFloat(),
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
private fun getM3Colors(): M3Colors {
|
||||
// color scheme
|
||||
return M3Colors(
|
||||
surface = Color.parseColor("#000000"),
|
||||
surfaceVariant = Color.parseColor("#1A1A1A"),
|
||||
onSurface = Color.parseColor("#FFFFFF"),
|
||||
onSurfaceVariant = Color.parseColor("#CCCCCC"),
|
||||
primary = Color.parseColor("#FFFFFF"),
|
||||
onPrimary = Color.parseColor("#000000"),
|
||||
primaryContainer = Color.parseColor("#2A2A2A"),
|
||||
onPrimaryContainer = Color.parseColor("#FFFFFF"),
|
||||
error = Color.parseColor("#FF6B6B")
|
||||
)
|
||||
}
|
||||
|
||||
private data class M3Colors(
|
||||
val surface: Int,
|
||||
val surfaceVariant: Int,
|
||||
val onSurface: Int,
|
||||
val onSurfaceVariant: Int,
|
||||
val primary: Int,
|
||||
val onPrimary: Int,
|
||||
val primaryContainer: Int,
|
||||
val onPrimaryContainer: Int,
|
||||
val error: Int
|
||||
)
|
||||
|
||||
private fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable {
|
||||
return GradientDrawable().apply {
|
||||
setColor(color)
|
||||
setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createButton(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View {
|
||||
return LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
background = createM3Background(context, colors.primaryContainer, 12f)
|
||||
setPadding(
|
||||
dpToPx(context, 32),
|
||||
dpToPx(context, 12),
|
||||
dpToPx(context, 32),
|
||||
dpToPx(context, 12)
|
||||
)
|
||||
isClickable = true
|
||||
isFocusable = true
|
||||
|
||||
val params = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
dpToPx(context, 48)
|
||||
).apply {
|
||||
setMargins(0, dpToPx(context, 8), 0, 0)
|
||||
}
|
||||
layoutParams = params
|
||||
|
||||
addView(TextView(context).apply {
|
||||
this.text = text
|
||||
setTextColor(colors.onPrimaryContainer)
|
||||
textSize = 15f
|
||||
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||
gravity = Gravity.CENTER
|
||||
})
|
||||
|
||||
// Ripple effect
|
||||
val rippleDrawable = android.graphics.drawable.RippleDrawable(
|
||||
ColorStateList.valueOf(Color.argb(40, 255, 255, 255)),
|
||||
createM3Background(context, colors.primaryContainer, 12f),
|
||||
null
|
||||
)
|
||||
background = rippleDrawable
|
||||
|
||||
setOnClickListener { onClick() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRecoveryMenu(context: Context) {
|
||||
Log.e("showRecoveryMenu called with context: $context")
|
||||
try {
|
||||
val options = arrayOf(
|
||||
if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode",
|
||||
"Reset Bundle",
|
||||
"Reload App"
|
||||
val colors = getM3Colors()
|
||||
lateinit var dialog: AlertDialog
|
||||
|
||||
val container = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24)
|
||||
)
|
||||
background = createM3Background(context, colors.surface, 24f)
|
||||
}
|
||||
|
||||
val titleView = TextView(context).apply {
|
||||
text = "KettuXposed Recovery"
|
||||
textSize = 20f
|
||||
setTextColor(colors.onSurface)
|
||||
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
|
||||
setPadding(0, 0, 0, dpToPx(context, 24))
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
container.addView(titleView)
|
||||
|
||||
// safe Mode Button
|
||||
val safeModeText = if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
|
||||
container.addView(createButton(context, safeModeText, colors) {
|
||||
dialog.dismiss()
|
||||
toggleSafeMode(context)
|
||||
})
|
||||
|
||||
// load Custom Bundle Button
|
||||
container.addView(createButton(context, "Load Custom Bundle", colors) {
|
||||
dialog.dismiss()
|
||||
showCustomBundleDialog(context)
|
||||
})
|
||||
|
||||
// refetch Bundle Button
|
||||
container.addView(createButton(context, "Refetch Bundle", colors) {
|
||||
dialog.dismiss()
|
||||
showConfirmAction(
|
||||
context, "Refetch Bundle",
|
||||
"This will download the latest bundle from Github."
|
||||
) { refetchBundle(context) }
|
||||
})
|
||||
|
||||
// reload App Button
|
||||
container.addView(createButton(context, "Reload App", colors) {
|
||||
dialog.dismiss()
|
||||
reloadApp()
|
||||
})
|
||||
|
||||
dialog = AlertDialog.Builder(context)
|
||||
.setView(container)
|
||||
.create()
|
||||
|
||||
dialog.window?.setBackgroundDrawable(
|
||||
createM3Background(context, Color.TRANSPARENT, 24f)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("KettuXposed Recovery Menu")
|
||||
.setItems(options) { _, which ->
|
||||
handleMenuSelection(context, which)
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
constrainContainerWidth(context, container)
|
||||
dialog.show()
|
||||
setDialogWindowWidth(dialog, context)
|
||||
Log.e("Recovery menu shown successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error showing recovery menu: ${e.message}", e)
|
||||
|
|
@ -228,21 +378,390 @@ object LogBoxModule : Module() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleMenuSelection(context: Context, index: Int) {
|
||||
when (index) {
|
||||
0 -> toggleSafeMode(context)
|
||||
1 -> confirmAction(context, "reset bundle") { resetBundle(context) }
|
||||
2 -> reloadApp()
|
||||
private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
|
||||
val colors = getM3Colors()
|
||||
|
||||
val container = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24)
|
||||
)
|
||||
background = createM3Background(context, colors.surface, 24f)
|
||||
}
|
||||
|
||||
val titleView = TextView(context).apply {
|
||||
text = title
|
||||
textSize = 18f
|
||||
setTextColor(colors.onSurface)
|
||||
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
|
||||
setPadding(0, 0, 0, dpToPx(context, 16))
|
||||
}
|
||||
container.addView(titleView)
|
||||
|
||||
val messageView = TextView(context).apply {
|
||||
text = message
|
||||
textSize = 14f
|
||||
setTextColor(colors.onSurfaceVariant)
|
||||
setPadding(0, 0, 0, dpToPx(context, 24))
|
||||
}
|
||||
container.addView(messageView)
|
||||
|
||||
val buttonContainer = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.END
|
||||
}
|
||||
|
||||
val cancelButton = TextView(context).apply {
|
||||
text = "Cancel"
|
||||
setTextColor(colors.primary)
|
||||
textSize = 14f
|
||||
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||
setPadding(
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12),
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12)
|
||||
)
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
val confirmButton = TextView(context).apply {
|
||||
text = "Confirm"
|
||||
setTextColor(colors.primary)
|
||||
textSize = 14f
|
||||
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||
setPadding(
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12),
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12)
|
||||
)
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(container)
|
||||
.create()
|
||||
|
||||
cancelButton.setOnClickListener { dialog.dismiss() }
|
||||
confirmButton.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
action()
|
||||
}
|
||||
|
||||
buttonContainer.addView(cancelButton)
|
||||
buttonContainer.addView(confirmButton)
|
||||
container.addView(buttonContainer)
|
||||
|
||||
dialog.window?.setBackgroundDrawable(
|
||||
createM3Background(context, Color.TRANSPARENT, 24f)
|
||||
)
|
||||
|
||||
constrainContainerWidth(context, container)
|
||||
dialog.show()
|
||||
setDialogWindowWidth(dialog, context)
|
||||
}
|
||||
|
||||
private fun confirmAction(context: Context, actionText: String, action: () -> Unit) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("Confirm Action")
|
||||
.setMessage("Are you sure you want to $actionText?")
|
||||
.setPositiveButton("Confirm") { _, _ -> action() }
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
private fun showCustomBundleDialog(context: Context) {
|
||||
val colors = getM3Colors()
|
||||
val filesDir = File(context.filesDir, "pyoncord")
|
||||
val configFile = File(filesDir, "loader_config.json")
|
||||
var currentUrl: String? = null
|
||||
var isEnabled = false
|
||||
|
||||
if (configFile.exists()) {
|
||||
try {
|
||||
val json = JSONObject(configFile.readText())
|
||||
val custom = json.optJSONObject("customLoadUrl")
|
||||
if (custom != null) {
|
||||
isEnabled = custom.optBoolean("enabled", false)
|
||||
currentUrl = custom.optString("url", "")
|
||||
} else {
|
||||
isEnabled = false
|
||||
currentUrl = ""
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
val container = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24)
|
||||
)
|
||||
background = createM3Background(context, colors.surface, 24f)
|
||||
}
|
||||
|
||||
val titleView = TextView(context).apply {
|
||||
text = "Custom Bundle URL"
|
||||
textSize = 18f
|
||||
setTextColor(colors.onSurface)
|
||||
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
|
||||
setPadding(0, 0, 0, dpToPx(context, 16))
|
||||
}
|
||||
container.addView(titleView)
|
||||
|
||||
val toggleContainer = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
val params = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(0, 0, 0, dpToPx(context, 16))
|
||||
}
|
||||
layoutParams = params
|
||||
}
|
||||
|
||||
val toggleLabel = TextView(context).apply {
|
||||
text = "Enable Custom URL"
|
||||
textSize = 14f
|
||||
setTextColor(colors.onSurface)
|
||||
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
)
|
||||
}
|
||||
|
||||
// switch container
|
||||
val trackPadding = dpToPx(context, 4)
|
||||
val switchTrackWidth = dpToPx(context, 52)
|
||||
val switchTrackHeight = dpToPx(context, 32)
|
||||
val trackCornerRadius = switchTrackHeight / 2f
|
||||
|
||||
val switchContainer = FrameLayout(context).apply {
|
||||
background = createM3Background(context, colors.surfaceVariant, trackCornerRadius)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
switchTrackWidth,
|
||||
switchTrackHeight
|
||||
)
|
||||
tag = isEnabled
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
// create the thumb with proper sizing
|
||||
val thumbSize = switchTrackHeight - (trackPadding * 2)
|
||||
val thumbCornerRadius = thumbSize / 2f
|
||||
|
||||
val switchThumb = View(context).apply {
|
||||
background = createM3Background(context, if (isEnabled) colors.onPrimary else colors.onSurfaceVariant, thumbCornerRadius)
|
||||
val params = FrameLayout.LayoutParams(thumbSize, thumbSize)
|
||||
|
||||
val thumbLeft = if (isEnabled) {
|
||||
switchTrackWidth - thumbSize - trackPadding
|
||||
} else {
|
||||
trackPadding
|
||||
}
|
||||
|
||||
params.leftMargin = thumbLeft
|
||||
params.topMargin = trackPadding
|
||||
|
||||
layoutParams = params
|
||||
}
|
||||
|
||||
if (isEnabled) {
|
||||
switchContainer.background = createM3Background(context, colors.primary, trackCornerRadius)
|
||||
}
|
||||
|
||||
switchContainer.addView(switchThumb)
|
||||
|
||||
toggleContainer.addView(toggleLabel)
|
||||
toggleContainer.addView(switchContainer)
|
||||
container.addView(toggleContainer)
|
||||
|
||||
val urlInput = EditText(context).apply {
|
||||
hint = "http://localhost:4040/bundle.js"
|
||||
setTextColor(colors.onSurface)
|
||||
setHintTextColor(colors.onSurfaceVariant)
|
||||
background = createM3Background(context, colors.surfaceVariant, 12f)
|
||||
inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI
|
||||
setText(currentUrl ?: "")
|
||||
textSize = 14f
|
||||
setPadding(
|
||||
dpToPx(context, 16),
|
||||
dpToPx(context, 12),
|
||||
dpToPx(context, 16),
|
||||
dpToPx(context, 12)
|
||||
)
|
||||
this.isEnabled = isEnabled
|
||||
alpha = if (isEnabled) 1f else 0.5f
|
||||
val params = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(0, 0, 0, dpToPx(context, 24))
|
||||
}
|
||||
layoutParams = params
|
||||
}
|
||||
container.addView(urlInput)
|
||||
|
||||
switchContainer.setOnClickListener {
|
||||
val currentState = switchContainer.tag as Boolean
|
||||
val newState = !currentState
|
||||
switchContainer.tag = newState
|
||||
|
||||
val thumbParams = switchThumb.layoutParams as FrameLayout.LayoutParams
|
||||
val targetLeftMargin = if (newState) {
|
||||
switchTrackWidth - thumbSize - trackPadding
|
||||
} else {
|
||||
trackPadding
|
||||
}
|
||||
|
||||
// Animate the thumb movement
|
||||
val animator = ValueAnimator.ofInt(thumbParams.leftMargin, targetLeftMargin).apply {
|
||||
duration = 200
|
||||
interpolator = DecelerateInterpolator()
|
||||
addUpdateListener { valueAnimator ->
|
||||
val animatedValue = valueAnimator.animatedValue as Int
|
||||
thumbParams.leftMargin = animatedValue
|
||||
switchThumb.layoutParams = thumbParams
|
||||
}
|
||||
}
|
||||
animator.start()
|
||||
|
||||
// animate track color change
|
||||
val trackAnimator = ValueAnimator.ofObject(
|
||||
ArgbEvaluator(),
|
||||
if (currentState) colors.primary else colors.surfaceVariant,
|
||||
if (newState) colors.primary else colors.surfaceVariant
|
||||
).apply {
|
||||
duration = 200
|
||||
addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
switchContainer.background = createM3Background(context, color, trackCornerRadius)
|
||||
}
|
||||
}
|
||||
trackAnimator.start()
|
||||
|
||||
// animate thumb color change
|
||||
val thumbAnimator = ValueAnimator.ofObject(
|
||||
ArgbEvaluator(),
|
||||
if (currentState) colors.onPrimary else colors.onSurfaceVariant,
|
||||
if (newState) colors.onPrimary else colors.onSurfaceVariant
|
||||
).apply {
|
||||
duration = 200
|
||||
addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
switchThumb.background = createM3Background(context, color, thumbCornerRadius)
|
||||
}
|
||||
}
|
||||
thumbAnimator.start()
|
||||
|
||||
urlInput.animate()
|
||||
.alpha(if (newState) 1f else 0.5f)
|
||||
.setDuration(200)
|
||||
.withEndAction {
|
||||
urlInput.isEnabled = newState
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
val buttonContainer = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.END
|
||||
}
|
||||
|
||||
val cancelButton = TextView(context).apply {
|
||||
text = "Cancel"
|
||||
setTextColor(colors.primary)
|
||||
textSize = 14f
|
||||
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||
setPadding(
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12),
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12)
|
||||
)
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
val saveButton = TextView(context).apply {
|
||||
text = "Save"
|
||||
setTextColor(colors.primary)
|
||||
textSize = 14f
|
||||
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||
setPadding(
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12),
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12)
|
||||
)
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(container)
|
||||
.create()
|
||||
|
||||
cancelButton.setOnClickListener { dialog.dismiss() }
|
||||
saveButton.setOnClickListener {
|
||||
try {
|
||||
val url = urlInput.text?.toString()?.trim() ?: ""
|
||||
val enabled = switchContainer.tag as Boolean
|
||||
|
||||
if (enabled && url.isNotEmpty()) {
|
||||
setCustomBundleURL(context, url, true)
|
||||
dialog.dismiss()
|
||||
} else if (!enabled) {
|
||||
setCustomBundleURL(context, url.ifEmpty { "http://localhost:4040/bundle.js" }, false)
|
||||
dialog.dismiss()
|
||||
} else {
|
||||
Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val err = e.message ?: "unknown error"
|
||||
Toast.makeText(context, "Failed to set custom bundle: $err", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
buttonContainer.addView(cancelButton)
|
||||
buttonContainer.addView(saveButton)
|
||||
container.addView(buttonContainer)
|
||||
|
||||
dialog.window?.setBackgroundDrawable(
|
||||
createM3Background(context, Color.TRANSPARENT, 24f)
|
||||
)
|
||||
|
||||
constrainContainerWidth(context, container)
|
||||
dialog.show()
|
||||
setDialogWindowWidth(dialog, context)
|
||||
}
|
||||
|
||||
private fun setCustomBundleURL(context: Context, url: String, enabled: Boolean) {
|
||||
try {
|
||||
val filesDir = File(context.dataDir, Constants.FILES_DIR)
|
||||
filesDir.mkdirs()
|
||||
val configFile = File(filesDir, "loader.json")
|
||||
|
||||
val cfg = io.github.revenge.xposed.modules.LoaderConfig(
|
||||
io.github.revenge.xposed.modules.CustomLoadUrl(enabled, url)
|
||||
)
|
||||
|
||||
val jsonText = io.github.revenge.xposed.Utils.JSON.encodeToString(cfg)
|
||||
configFile.writeText(jsonText)
|
||||
|
||||
// remove cached bundle so UpdaterModule will fetch the custom one
|
||||
val cacheBundle = File(context.dataDir, "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}")
|
||||
if (enabled) {
|
||||
if (cacheBundle.exists()) cacheBundle.delete()
|
||||
Toast.makeText(context, "Custom bundle enabled", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
reloadApp()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error setting custom bundle URL: ${e.message}")
|
||||
showError(context, "Failed to save configuration", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSafeModeEnabled(context: Context): Boolean {
|
||||
|
|
@ -298,26 +817,23 @@ object LogBoxModule : Module() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun resetBundle(context: Context) {
|
||||
private fun refetchBundle(context: Context) {
|
||||
try {
|
||||
val pyoncordDir = getPyoncordDirectory(context)
|
||||
val bundleFile = File(pyoncordDir, "bundle.js")
|
||||
val backupFile = File(pyoncordDir, "bundle.js.backup")
|
||||
val configFile = File(pyoncordDir, "loader_config.json")
|
||||
|
||||
bundleFile.delete()
|
||||
backupFile.delete()
|
||||
|
||||
if (configFile.exists()) {
|
||||
val config = JSONObject(configFile.readText())
|
||||
config.put("customLoadUrlEnabled", false)
|
||||
configFile.writeText(config.toString())
|
||||
if (bundleFile.exists()) {
|
||||
backupFile.delete()
|
||||
bundleFile.renameTo(backupFile)
|
||||
Log.e("Bundle moved to backup")
|
||||
}
|
||||
|
||||
reloadApp()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("Error resetting bundle: ${e.message}")
|
||||
showError(context, "Failed to reset bundle", e.message)
|
||||
Log.e("Error refetching bundle: ${e.message}")
|
||||
showError(context, "Failed to refetch bundle", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,11 +845,89 @@ object LogBoxModule : Module() {
|
|||
return dir
|
||||
}
|
||||
|
||||
private fun showError(context: Context, title: String, message: String?) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message ?: "An unknown error occurred")
|
||||
.setPositiveButton("OK", null)
|
||||
.show()
|
||||
private fun constrainContainerWidth(context: Context, container: LinearLayout) {
|
||||
try {
|
||||
val maxW = (context.resources.displayMetrics.widthPixels * 0.88).toInt()
|
||||
val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
container.layoutParams = lp
|
||||
try {
|
||||
container.minimumWidth = maxW
|
||||
} catch (_: Exception) {}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDialogWindowWidth(dialog: AlertDialog, context: Context) {
|
||||
try {
|
||||
val screenW = context.resources.displayMetrics.widthPixels
|
||||
val maxByPercent = (screenW * 0.88).toInt()
|
||||
val maxByDp = dpToPx(context, 380)
|
||||
val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp
|
||||
|
||||
dialog.window?.setLayout(targetW, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(context: Context, title: String, message: String?) {
|
||||
val colors = getM3Colors()
|
||||
|
||||
val container = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 24)
|
||||
)
|
||||
background = createM3Background(context, colors.surface, 24f)
|
||||
}
|
||||
|
||||
val titleView = TextView(context).apply {
|
||||
text = title
|
||||
textSize = 18f
|
||||
setTextColor(colors.error)
|
||||
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
|
||||
setPadding(0, 0, 0, dpToPx(context, 16))
|
||||
}
|
||||
container.addView(titleView)
|
||||
|
||||
val messageView = TextView(context).apply {
|
||||
text = message ?: "An unknown error occurred"
|
||||
textSize = 14f
|
||||
setTextColor(colors.onSurfaceVariant)
|
||||
setPadding(0, 0, 0, dpToPx(context, 24))
|
||||
}
|
||||
container.addView(messageView)
|
||||
|
||||
val okButton = TextView(context).apply {
|
||||
text = "OK"
|
||||
setTextColor(colors.primary)
|
||||
textSize = 14f
|
||||
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||
gravity = Gravity.END
|
||||
setPadding(
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12),
|
||||
dpToPx(context, 20),
|
||||
dpToPx(context, 12)
|
||||
)
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(container)
|
||||
.create()
|
||||
|
||||
okButton.setOnClickListener { dialog.dismiss() }
|
||||
container.addView(okButton)
|
||||
|
||||
dialog.window?.setBackgroundDrawable(
|
||||
createM3Background(context, Color.TRANSPARENT, 24f)
|
||||
)
|
||||
|
||||
constrainContainerWidth(context, container)
|
||||
dialog.show()
|
||||
setDialogWindowWidth(dialog, context)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue