diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 020a31e..596a892 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1250 - versionName = "1.2.5" + versionCode = 1320 + versionName = "1.3.2" } buildTypes { @@ -52,4 +52,5 @@ dependencies { implementation("io.ktor:ktor-client-core:3.3.1") implementation("io.ktor:ktor-client-cio:3.3.1") implementation("com.google.android.material:material:1.12.0") + implementation("dev.rushii:libunbound:1.0.0") } diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt index f92abbf..af27662 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Utils.kt @@ -37,11 +37,6 @@ class Utils { reloadApp() } - - 2 -> { - UpdaterModule.resetLoaderConfig(context) - reloadApp() - } } }.show() } diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt index e70f2fa..427eb59 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt @@ -83,9 +83,19 @@ object HookScriptLoaderModule : Module() { runBlocking { val ready = async { HookStateHolder.readyDeferred.join() } - val download = async { UpdaterModule.downloadScript().join() } + val isCustomUrl = UpdaterModule.isCustomUrlEnabled - awaitAll(ready, download) + if (!mainScript.exists() || isCustomUrl) { + val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist" + Log.i("$reason, downloading before load...") + val download = + async { UpdaterModule.downloadScript(showUpdateDialog = false).join() } + awaitAll(ready, download) + } else { + Log.i("Main script exists, updating in background...") + UpdaterModule.downloadScript(showUpdateDialog = true) + ready.await() + } } val loadSynchronously = args[2] diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/CustomBundleDialog.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/CustomBundleDialog.kt new file mode 100644 index 0000000..00f3299 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/CustomBundleDialog.kt @@ -0,0 +1,170 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.app.AlertDialog +import android.content.Context +import android.text.InputType +import android.widget.* +import org.json.JSONObject +import java.io.File + +object CustomBundleDialog { + + fun showCustomBundleDialog(context: Context) { + val colors = LogBoxTheme.getM3Colors(context) + val filesDir = LogBoxUtils.getPyoncordDirectory(context) + val configFile = File(filesDir, "loader.json") + var currentUrl: String? = null + var isEnabled = false + + if (configFile.exists()) { + try { + val json = JSONObject(configFile.readText()) + val customLoadUrl = json.optJSONObject("customLoadUrl") + if (customLoadUrl != null) { + isEnabled = customLoadUrl.optBoolean("enabled", false) + currentUrl = customLoadUrl.optString("url", "") + } + } catch (_: Exception) { + } + } + + val container = LogBoxComponents.createMenuContainer(context, colors) + container.addView(LogBoxComponents.createTitle(context, colors, "Custom Bundle URL")) + + val toggleContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = android.view.Gravity.CENTER_VERTICAL + val params = LinearLayout.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 0, 0, LogBoxUtils.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, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + 1f + ) + } + + val toggleSwitch = LogBoxComponents.createM3Switch(context, colors, isEnabled) + + toggleContainer.addView(toggleLabel) + toggleContainer.addView(toggleSwitch) + container.addView(toggleContainer) + + val urlInput = EditText(context).apply { + hint = "http://localhost:4040/shiggycord.js" + setTextColor(colors.onSurface) + setHintTextColor(colors.onSurfaceVariant) + background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f) + inputType = InputType.TYPE_TEXT_VARIATION_URI + setText(currentUrl ?: "") + textSize = 14f + setPadding( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 12), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 12) + ) + alpha = if (isEnabled) 1f else 0.5f + val params = LinearLayout.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 24)) + } + layoutParams = params + } + container.addView(urlInput) + + toggleSwitch.setOnClickListener { + val currentState = toggleSwitch.tag as Boolean + val newState = !currentState + toggleSwitch.tag = newState + LogBoxComponents.updateSwitchAppearance(context, toggleSwitch, colors, newState) + + urlInput.animate() + .alpha(if (newState) 1f else 0.5f) + .setDuration(250) + .withEndAction { + urlInput.isEnabled = newState + } + .start() + } + + val buttonContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = android.view.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( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + 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( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + val dialog = LogBoxDialogs.createDialog(context, container) + + cancelButton.setOnClickListener { dialog.dismiss() } + saveButton.setOnClickListener { + try { + val url = urlInput.text?.toString()?.trim() ?: "" + val enabled = toggleSwitch.tag as Boolean + + if (enabled && url.isNotEmpty()) { + LogBoxActions.setCustomBundleURL(context, url, true) + dialog.dismiss() + } else if (!enabled) { + LogBoxActions.setCustomBundleURL( + context, + url.ifEmpty { "http://localhost:4040/shiggycord.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) + + LogBoxDialogs.currentMenuContainer = container + dialog.show() + LogBoxDialogs.setDialogWindowWidth(dialog, context) + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/FlavorSelectionMenu.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/FlavorSelectionMenu.kt new file mode 100644 index 0000000..1ea94dc --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/FlavorSelectionMenu.kt @@ -0,0 +1,35 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.app.AlertDialog +import android.content.Context + +object FlavorSelectionMenu { + + fun showFlavorSelection(context: Context) { + val colors = LogBoxTheme.getM3Colors(context) + lateinit var dialog: AlertDialog + + val container = LogBoxComponents.createMenuContainer(context, colors, 20) + container.addView(LogBoxComponents.createTitle(context, colors, "Select Menu Flavor", center = true)) + + LogBoxConstants.FLAVORS.forEach { flavor -> + container.addView( + LogBoxComponents.createM3Button( + context, + flavor.replaceFirstChar { it.uppercase() }, + colors + ) { + try { + dialog.dismiss() + } catch (_: Exception) { + } + LogBoxUtils.saveMenuFlavor(context, flavor) + }) + } + + dialog = LogBoxDialogs.createDialog(context, container) + LogBoxDialogs.currentMenuContainer = container + dialog.show() + LogBoxDialogs.setDialogWindowWidth(dialog, context) + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxActions.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxActions.kt new file mode 100644 index 0000000..2558f30 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxActions.kt @@ -0,0 +1,193 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.content.Context +import android.widget.Toast +import org.json.JSONObject +import java.io.File +import ShiggyXposed.xposed.Utils.Log +import ShiggyXposed.xposed.Utils.Companion.reloadApp +import ShiggyXposed.xposed.Constants +import kotlinx.coroutines.* + +object LogBoxActions { + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + fun toggleSafeMode(context: Context) { + try { + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES") + + settingsFile.parentFile?.mkdirs() + + val settings = if (settingsFile.exists()) { + JSONObject(settingsFile.readText()) + } else { + JSONObject() + } + + val safeMode = settings.optJSONObject("safeMode") ?: JSONObject() + val currentState = safeMode.optBoolean("enabled", false) + val newState = !currentState + + safeMode.put("enabled", newState) + + if (newState && themeFile.exists()) { + val theme = JSONObject(themeFile.readText()) + val themeId = theme.optString("id") + if (themeId.isNotEmpty()) { + safeMode.put("currentThemeId", themeId) + themeFile.delete() + } + } + + settings.put("safeMode", safeMode) + settingsFile.writeText(settings.toString()) + + Toast.makeText(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}", Toast.LENGTH_SHORT).show() + reloadApp() + + } catch (e: Exception) { + Log.e("Error toggling safe mode: ${e.message}") + showError(context, "Failed to toggle safe mode", e.message) + } + } + + fun toggleBundleInjection(context: Context) { + try { + val cacheDir = File(context.dataDir, Constants.CACHE_DIR) + cacheDir.mkdirs() + val bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE) + val disabled = File(cacheDir, "${Constants.MAIN_SCRIPT_FILE}.disabled") + + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + settingsFile.parentFile?.mkdirs() + val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject() + + val currentlyDisabled = LogBoxUtils.isBundleInjectionDisabled(context) + val newState = !currentlyDisabled + + if (bundle.exists() && disabled.exists()) { + bundle.delete() + } + + if (newState) { + // Disable injection + if (disabled.exists()) { + disabled.delete() + } + if (bundle.exists()) { + bundle.renameTo(disabled) + } else { + disabled.writeText("") + } + settings.put("bundleInjectionDisabled", true) + Toast.makeText(context, "Bundle injection disabled", Toast.LENGTH_SHORT).show() + } else { + // Enable injection + if (disabled.exists()) { + disabled.renameTo(bundle) + } else if (!bundle.exists()) { + bundle.writeText("") + } + settings.put("bundleInjectionDisabled", false) + Toast.makeText(context, "Bundle injection enabled", Toast.LENGTH_SHORT).show() + } + + settingsFile.writeText(settings.toString()) + + } catch (e: Exception) { + Log.e("Error toggling bundle injection: ${e.message}") + showError(context, "Failed to toggle bundle injection", e.message) + } + } + + fun refetchBundle(context: Context) { + try { + val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context) + val bundleFile = File(pyoncordDir, "bundle.js") + val backupFile = File(pyoncordDir, "bundle.js.backup") + + if (bundleFile.exists()) { + backupFile.delete() + bundleFile.renameTo(backupFile) + Log.e("Bundle moved to backup") + } + + reloadApp() + + } catch (e: Exception) { + Log.e("Error refetching bundle: ${e.message}") + showError(context, "Failed to refetch bundle", e.message) + } + } + + fun clearCacheAndReset(context: Context) { + scope.launch(Dispatchers.IO) { + try { + val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context) + val bundleFile = File(pyoncordDir, "bundle.js") + val configFile = File(pyoncordDir, "loader.json") + + bundleFile.delete() + + if (configFile.exists()) { + val config = JSONObject(configFile.readText()) + val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() + customLoadUrl.put("enabled", false) + customLoadUrl.put("url", "http://localhost:4040/shiggycord.js") + config.put("customLoadUrl", customLoadUrl) + configFile.writeText(config.toString()) + } + + withContext(Dispatchers.Main) { + Toast.makeText(context, "Cache cleared successfully", Toast.LENGTH_SHORT).show() + reloadApp() + } + + } catch (e: Exception) { + Log.e("Error clearing cache: ${e.message}") + withContext(Dispatchers.Main) { + Toast.makeText(context, "Failed to clear cache", Toast.LENGTH_SHORT).show() + } + } + } + } + + fun setCustomBundleURL(context: Context, url: String, enabled: Boolean) { + try { + val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context) + val configFile = File(pyoncordDir, "loader.json") + + val config = if (configFile.exists()) { + JSONObject(configFile.readText()) + } else { + JSONObject().apply { + put("loadReactDevTools", false) + } + } + + val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() + customLoadUrl.put("enabled", enabled) + customLoadUrl.put("url", url) + config.put("customLoadUrl", customLoadUrl) + + configFile.writeText(config.toString()) + + if (enabled) { + File(pyoncordDir, "bundle.js").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) + } + } + + fun showError(context: Context, title: String, message: String?) { + LogBoxMenu.showErrorDialog(context, title, message) + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxComponents.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxComponents.kt new file mode 100644 index 0000000..2f7f58a --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxComponents.kt @@ -0,0 +1,257 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import android.animation.* +import android.view.animation.DecelerateInterpolator + +object LogBoxComponents { + + fun createM3Button(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View { + return LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER + background = LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f) + setPadding( + LogBoxUtils.dpToPx(context, 24), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 24), + LogBoxUtils.dpToPx(context, 10) + ) + clipToOutline = true + isClickable = true + isFocusable = true + + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + LogBoxUtils.dpToPx(context, 40) + ).apply { + setMargins(0, LogBoxUtils.dpToPx(context, 8), 0, 0) + } + layoutParams = params + + addView(TextView(context).apply { + this.text = text + setTextColor(colors.onPrimaryContainer) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + gravity = Gravity.CENTER + }) + + val rippleDrawable = android.graphics.drawable.RippleDrawable( + ColorStateList.valueOf(Color.argb(30, 255, 255, 255)), + LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f), + null + ) + background = rippleDrawable + + setOnClickListener { onClick() } + } + } + + fun createTitle(context: Context, colors: M3Colors, text: String, center: Boolean = false): TextView { + return TextView(context).apply { + this.text = text + textSize = if (center) 20f else 22f + setTextColor(colors.onSurface) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, if (center) 12 else 16)) + gravity = if (center) Gravity.CENTER else Gravity.START + } + } + + fun createSubtitle(context: Context, colors: M3Colors, text: String): TextView { + return TextView(context).apply { + this.text = text + textSize = 14f + setTextColor(colors.onSurfaceVariant) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24)) + gravity = Gravity.CENTER + } + } + + fun createMenuContainer(context: Context, colors: M3Colors, padding: Int = 24): LinearLayout { + return LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding( + LogBoxUtils.dpToPx(context, padding), + LogBoxUtils.dpToPx(context, padding), + LogBoxUtils.dpToPx(context, padding), + LogBoxUtils.dpToPx(context, padding) + ) + background = LogBoxUtils.createM3Background(context, colors.surface, if (padding == 24) 28f else 24f) + } + } + + fun createM3Switch(context: Context, colors: M3Colors, isChecked: Boolean): View { + val switchView = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + tag = isChecked + layoutParams = LinearLayout.LayoutParams( + LogBoxUtils.dpToPx(context, 52), + LogBoxUtils.dpToPx(context, 32) + ) + } + + updateSwitchAppearance(context, switchView, colors, isChecked) + return switchView + } + + fun updateSwitchAppearance(context: Context, switchView: View, colors: M3Colors, isChecked: Boolean) { + val trackColor = if (isChecked) colors.primary else colors.surfaceVariant + val thumbColor = if (isChecked) colors.onPrimary else colors.onSurfaceVariant + + val currentTrackColor = if (switchView.background is GradientDrawable) { + (switchView.background as GradientDrawable).color?.defaultColor + ?: (if (!isChecked) colors.primary else colors.surfaceVariant) + } else { + if (!isChecked) colors.primary else colors.surfaceVariant + } + + val trackColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentTrackColor, trackColor) + trackColorAnimator.duration = 250 + trackColorAnimator.interpolator = DecelerateInterpolator() + trackColorAnimator.addUpdateListener { animator -> + val animatedColor = animator.animatedValue as Int + val track = GradientDrawable().apply { + setColor(animatedColor) + setCornerRadius(LogBoxUtils.dpToPx(context, 16).toFloat()) + } + switchView.background = track + } + + if (switchView is LinearLayout) { + val thumb = if (switchView.childCount > 0) { + switchView.getChildAt(0) + } else { + View(context).apply { + val size = LogBoxUtils.dpToPx(context, 24) + layoutParams = LinearLayout.LayoutParams(size, size).apply { + marginStart = LogBoxUtils.dpToPx(context, 4) + marginEnd = LogBoxUtils.dpToPx(context, 24) + } + switchView.addView(this) + } + } + + val currentParams = thumb.layoutParams as LinearLayout.LayoutParams + val targetMarginStart = if (isChecked) LogBoxUtils.dpToPx(context, 24) else LogBoxUtils.dpToPx(context, 4) + + val marginAnimator = ValueAnimator.ofInt(currentParams.marginStart, targetMarginStart) + marginAnimator.duration = 250 + marginAnimator.interpolator = DecelerateInterpolator() + marginAnimator.addUpdateListener { animator -> + val animatedMargin = animator.animatedValue as Int + val params = thumb.layoutParams as LinearLayout.LayoutParams + params.marginStart = animatedMargin + params.marginEnd = LogBoxUtils.dpToPx(context, 28) - animatedMargin + thumb.layoutParams = params + } + + val currentThumbColor = if (thumb.background is GradientDrawable) { + (thumb.background as GradientDrawable).color?.defaultColor + ?: (if (!isChecked) colors.onPrimary else colors.onSurfaceVariant) + } else { + if (!isChecked) colors.onPrimary else colors.onSurfaceVariant + } + + val thumbColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentThumbColor, thumbColor) + thumbColorAnimator.duration = 250 + thumbColorAnimator.interpolator = DecelerateInterpolator() + thumbColorAnimator.addUpdateListener { animator -> + val animatedColor = animator.animatedValue as Int + thumb.background = GradientDrawable().apply { + setColor(animatedColor) + shape = GradientDrawable.OVAL + } + } + + val animatorSet = AnimatorSet() + animatorSet.playTogether(trackColorAnimator, marginAnimator, thumbColorAnimator) + animatorSet.start() + } + } + + fun createAppearanceSelector(context: Context, colors: M3Colors): View { + val currentMode = LogBoxUtils.getAppearanceMode(context) + val options = listOf("System", "Light", "Dark") + val keys = listOf("system", "light", "dark") + + val wrapper = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + layoutParams = params + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8)) + } + + val label = TextView(context).apply { + text = "Appearance" + textSize = 14f + setTextColor(colors.onSurface) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8)) + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + } + wrapper.addView(label) + + val row = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + LogBoxUtils.dpToPx(context, 40) + ) + layoutParams = params + } + + options.forEachIndexed { idx, title -> + val key = keys[idx] + val seg = TextView(context).apply { + text = title + gravity = Gravity.CENTER + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + val segParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) + if (idx > 0) segParams.setMargins(LogBoxUtils.dpToPx(context, 8), 0, 0, 0) + layoutParams = segParams + + val isSelected = currentMode == key + background = LogBoxUtils.createM3Background( + context, + if (isSelected) colors.primaryContainer else colors.surfaceVariant, + 12f + ) + setTextColor(if (isSelected) colors.onPrimaryContainer else colors.onSurface) + setPadding(0, 0, 0, 0) + setOnClickListener { + LogBoxUtils.setAppearanceMode(context, key) + + for (i in 0 until row.childCount) { + val child = row.getChildAt(i) as TextView + val sel = keys[i] == key + child.background = LogBoxUtils.createM3Background( + context, + if (sel) colors.primaryContainer else colors.surfaceVariant, + 12f + ) + child.setTextColor(if (sel) colors.onPrimaryContainer else colors.onSurface) + } + } + } + row.addView(seg) + } + + wrapper.addView(row) + return wrapper + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxConstants.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxConstants.kt new file mode 100644 index 0000000..f38bdce --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxConstants.kt @@ -0,0 +1,35 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.graphics.Color + +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 secondaryContainer: Int, + val onSecondaryContainer: Int, + val error: Int, + val onError: Int +) + +object LogBoxConstants { + const val TAG = "LogBoxModule" + + val FLAVOR_COLORS = mapOf( + "blue" to Pair("#0D47A1", "#82B1FF"), + "green" to Pair("#1B5E20", "#A5D6A7"), + "mocha" to Pair("#3E2723", "#BCAAA4"), + "vanilla" to Pair("#F9A825", "#FFF59D"), + "purple" to Pair("#6A1B9A", "#E1BEE7"), + "amber" to Pair("#FF6F00", "#FFE0B2"), + "teal" to Pair("#004D40", "#80CBC4") + ) + + val APPEARANCE_MODES = listOf("system", "light", "dark") + val FLAVORS = listOf("blue", "green", "mocha", "vanilla", "purple", "amber", "teal") +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxDialogs.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxDialogs.kt new file mode 100644 index 0000000..8c145f5 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxDialogs.kt @@ -0,0 +1,74 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.app.AlertDialog +import android.content.Context +import android.graphics.Color +import android.view.ViewGroup +import android.widget.LinearLayout +import ShiggyXposed.xposed.Utils.Log + +object LogBoxDialogs { + + var currentMenuContainer: LinearLayout? = null + + fun createDialog(context: Context, container: LinearLayout): AlertDialog { + val dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + dialog.window?.setBackgroundDrawable( + LogBoxUtils.createM3Background(context, Color.TRANSPARENT, 28f) + ) + + constrainContainerWidth(context, container) + return dialog + } + + fun constrainContainerWidth(context: Context, container: LinearLayout) { + try { + val maxW = (context.resources.displayMetrics.widthPixels * 0.9).toInt() + val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + container.layoutParams = lp + try { + container.minimumWidth = maxW + } catch (_: Exception) { + } + } catch (_: Exception) { + } + } + + fun setDialogWindowWidth(dialog: AlertDialog, context: Context) { + try { + val screenW = context.resources.displayMetrics.widthPixels + val maxByPercent = (screenW * 0.9).toInt() + val maxByDp = LogBoxUtils.dpToPx(context, 420) + val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp + + dialog.window?.setLayout(targetW, ViewGroup.LayoutParams.WRAP_CONTENT) + + try { + currentMenuContainer?.let { container -> + val lp = container.layoutParams ?: LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + lp.width = ViewGroup.LayoutParams.MATCH_PARENT + container.layoutParams = lp + try { + container.minimumWidth = targetW + } catch (_: Exception) { + } + container.post { + try { + container.requestLayout() + container.invalidate() + } catch (_: Exception) { + } + } + } + } catch (_: Exception) { + } + } catch (_: Exception) { + } + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt new file mode 100644 index 0000000..419abff --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt @@ -0,0 +1,784 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.app.AlertDialog +import android.content.Context +import android.graphics.Color +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.* +import android.animation.* +import android.graphics.drawable.GradientDrawable +import android.text.InputType +import android.content.res.ColorStateList +import android.view.animation.DecelerateInterpolator +import org.json.JSONObject +import java.io.File +import ShiggyXposed.xposed.Utils.Log +import ShiggyXposed.xposed.Utils + +object LogBoxMenu { + + private var currentMenuContainer: LinearLayout? = null + + fun showRecoveryMenu(context: Context) { + Log.e("showRecoveryMenu called with context: $context") + try { + val colors = LogBoxTheme.getM3Colors(context) + lateinit var dialog: AlertDialog + + val container = createMenuContainer(context, colors) + + // Title + container.addView(createTitle(context, colors, "ShiggyXposed Recovery", center = true)) + + // Subtitle + container.addView(createSubtitle(context, colors, "Select an option to continue")) + + // Safe Mode Button + val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode" + container.addView(createM3Button(context, safeModeText, colors) { + dialog.dismiss() + handleMenuSelection(context, 0) + }) + + // Load Custom Bundle Button + container.addView(createM3Button(context, "Load Custom Bundle", colors) { + dialog.dismiss() + handleMenuSelection(context, 2) + }) + + // Reload App Button + container.addView(createM3Button(context, "Reload App", colors) { + dialog.dismiss() + handleMenuSelection(context, 3) + }) + + // Options Button + val optionsBtn = createM3Button(context, "Options", colors) { + dialog.dismiss() + handleMenuSelection(context, 5) + } + (optionsBtn.layoutParams as? LinearLayout.LayoutParams)?.let { lp -> + lp.setMargins(lp.leftMargin, LogBoxUtils.dpToPx(context, 16), lp.rightMargin, lp.bottomMargin) + optionsBtn.layoutParams = lp + } + container.addView(optionsBtn) + + dialog = createDialog(context, container) + currentMenuContainer = container + dialog.show() + setDialogWindowWidth(dialog, context) + Log.e("Recovery menu shown successfully") + } catch (e: Exception) { + Log.e("Error showing recovery menu: ${e.message}", e) + throw e + } + } + + fun showOptionsMenu(context: Context) { + val colors = LogBoxTheme.getM3Colors(context) + lateinit var dialog: AlertDialog + + val container = createMenuContainer(context, colors, 20) + + container.addView(createTitle(context, colors, "Options", center = true)) + + // Themes button -> opens Themes submenu + container.addView(createM3Button(context, "Themes", colors) { + dialog.dismiss() + showThemesMenu(context) + }) + + // Disable/Enable bundle injection + val injectionDisabled = LogBoxUtils.isBundleInjectionDisabled(context) + val injectionText = if (injectionDisabled) "Enable Bundle Injection" else "Disable Bundle Injection" + container.addView(createM3Button(context, injectionText, colors) { + dialog.dismiss() + LogBoxActions.toggleBundleInjection(context) + Utils.reloadApp() + }) + + container.addView(createM3Button(context, "Refetch Bundle", colors) { + dialog.dismiss() + showConfirmAction(context, "Refetch Bundle", "This will download the latest bundle from Github.") { + LogBoxActions.refetchBundle(context) + } + }) + + container.addView(createM3Button(context, "Clear Cache & Reset", colors) { + dialog.dismiss() + showConfirmAction( + context, + "Clear Cache & Reset", + "This will clear all cached bundles and reset to default settings." + ) { + LogBoxActions.clearCacheAndReset(context) + } + }) + + container.addView(createM3Button(context, "Back", colors) { + dialog.dismiss() + showRecoveryMenu(context) + }) + + dialog = createDialog(context, container) + currentMenuContainer = container + dialog.show() + setDialogWindowWidth(dialog, context) + } + + fun showThemesMenu(context: Context) { + val colors = LogBoxTheme.getM3Colors(context) + lateinit var dialog: AlertDialog + + val container = createMenuContainer(context, colors, 20) + + container.addView(createTitle(context, colors, "Themes", center = true)) + + // Segmented appearance selector + container.addView(createAppearanceSelector(context, colors)) + + // Flavor selection + container.addView(createM3Button(context, "Menu Color Flavor", colors) { + dialog.dismiss() + showFlavorSelection(context) + }) + + // Back button (return to Options) + container.addView(createM3Button(context, "Back", colors) { + dialog.dismiss() + showOptionsMenu(context) + }) + + dialog = createDialog(context, container) + currentMenuContainer = container + dialog.show() + setDialogWindowWidth(dialog, context) + } + + fun showFlavorSelection(context: Context) { + val colors = LogBoxTheme.getM3Colors(context) + lateinit var dialog: AlertDialog + + val container = createMenuContainer(context, colors, 20) + + container.addView(createTitle(context, colors, "Select Menu Flavor", center = true)) + + LogBoxConstants.FLAVORS.forEach { flavor -> + container.addView(createM3Button(context, flavor.replaceFirstChar { it.uppercase() }, colors) { + try { + dialog.dismiss() + } catch (_: Exception) { + } + LogBoxUtils.saveMenuFlavor(context, flavor) + showThemesMenu(context) + }) + } + + dialog = createDialog(context, container) + currentMenuContainer = container + dialog.show() + setDialogWindowWidth(dialog, context) + } + + fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) { + val colors = LogBoxTheme.getM3Colors(context) + + val container = createMenuContainer(context, colors) + + container.addView(createTitle(context, colors, title)) + + val messageView = TextView(context).apply { + text = message + textSize = 14f + setTextColor(colors.onSurfaceVariant) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24)) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + lineHeight = LogBoxUtils.dpToPx(context, 20) + } + } + 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( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + 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( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + val dialog = createDialog(context, container) + + cancelButton.setOnClickListener { dialog.dismiss() } + confirmButton.setOnClickListener { + dialog.dismiss() + action() + } + + buttonContainer.addView(cancelButton) + buttonContainer.addView(confirmButton) + container.addView(buttonContainer) + + currentMenuContainer = container + dialog.show() + setDialogWindowWidth(dialog, context) + } + + fun showCustomBundleDialog(context: Context) { + val colors = LogBoxTheme.getM3Colors(context) + val filesDir = LogBoxUtils.getPyoncordDirectory(context) + val configFile = File(filesDir, "loader.json") + var currentUrl: String? = null + var isEnabled = false + + if (configFile.exists()) { + try { + val json = JSONObject(configFile.readText()) + val customLoadUrl = json.optJSONObject("customLoadUrl") + if (customLoadUrl != null) { + isEnabled = customLoadUrl.optBoolean("enabled", false) + currentUrl = customLoadUrl.optString("url", "") + } + } catch (_: Exception) { + } + } + + val container = createMenuContainer(context, colors) + + container.addView(createTitle(context, colors, "Custom Bundle URL")) + + // Toggle switch container + val toggleContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 0, 0, LogBoxUtils.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, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f + ) + } + + val toggleSwitch = createM3Switch(context, colors, isEnabled) + + toggleContainer.addView(toggleLabel) + toggleContainer.addView(toggleSwitch) + container.addView(toggleContainer) + + val urlInput = EditText(context).apply { + hint = "http://localhost:4040/shiggycord.js" + setTextColor(colors.onSurface) + setHintTextColor(colors.onSurfaceVariant) + background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f) + inputType = InputType.TYPE_TEXT_VARIATION_URI + setText(currentUrl ?: "") + textSize = 14f + setPadding( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 12), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 12) + ) + alpha = if (isEnabled) 1f else 0.5f + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 24)) + } + layoutParams = params + } + container.addView(urlInput) + + toggleSwitch.setOnClickListener { + val currentState = toggleSwitch.tag as Boolean + val newState = !currentState + toggleSwitch.tag = newState + updateSwitchAppearance(context, toggleSwitch, colors, newState) + + urlInput.animate() + .alpha(if (newState) 1f else 0.5f) + .setDuration(250) + .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( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + 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( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + val dialog = createDialog(context, container) + + cancelButton.setOnClickListener { dialog.dismiss() } + saveButton.setOnClickListener { + try { + val url = urlInput.text?.toString()?.trim() ?: "" + val enabled = toggleSwitch.tag as Boolean + + if (enabled && url.isNotEmpty()) { + LogBoxActions.setCustomBundleURL(context, url, true) + dialog.dismiss() + } else if (!enabled) { + LogBoxActions.setCustomBundleURL( + context, + url.ifEmpty { "http://localhost:4040/shiggycord.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) + + currentMenuContainer = container + dialog.show() + setDialogWindowWidth(dialog, context) + } + + fun showErrorDialog(context: Context, title: String, message: String?) { + val colors = LogBoxTheme.getM3Colors(context) + + val container = createMenuContainer(context, colors) + + val titleView = TextView(context).apply { + text = title + textSize = 20f + setTextColor(colors.error) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) + setPadding(0, 0, 0, LogBoxUtils.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, LogBoxUtils.dpToPx(context, 24)) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + lineHeight = LogBoxUtils.dpToPx(context, 20) + } + } + 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( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + val dialog = createDialog(context, container) + + okButton.setOnClickListener { dialog.dismiss() } + container.addView(okButton) + + currentMenuContainer = container + dialog.show() + setDialogWindowWidth(dialog, context) + } + + // Helper functions + private fun createMenuContainer(context: Context, colors: M3Colors, padding: Int = 24): LinearLayout { + return LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding( + LogBoxUtils.dpToPx(context, padding), + LogBoxUtils.dpToPx(context, padding), + LogBoxUtils.dpToPx(context, padding), + LogBoxUtils.dpToPx(context, padding) + ) + background = LogBoxUtils.createM3Background(context, colors.surface, if (padding == 24) 28f else 24f) + } + } + + private fun createTitle(context: Context, colors: M3Colors, text: String, center: Boolean = false): TextView { + return TextView(context).apply { + this.text = text + textSize = if (center) 20f else 22f + setTextColor(colors.onSurface) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, if (center) 12 else 16)) + gravity = if (center) Gravity.CENTER else Gravity.START + } + } + + private fun createSubtitle(context: Context, colors: M3Colors, text: String): TextView { + return TextView(context).apply { + this.text = text + textSize = 14f + setTextColor(colors.onSurfaceVariant) + typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24)) + gravity = Gravity.CENTER + } + } + + fun createM3Button(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View { + return LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER + background = LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f) + setPadding( + LogBoxUtils.dpToPx(context, 24), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 24), + LogBoxUtils.dpToPx(context, 10) + ) + clipToOutline = true + isClickable = true + isFocusable = true + + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + LogBoxUtils.dpToPx(context, 40) + ).apply { + setMargins(0, LogBoxUtils.dpToPx(context, 8), 0, 0) + } + layoutParams = params + + addView(TextView(context).apply { + this.text = text + setTextColor(colors.onPrimaryContainer) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + gravity = Gravity.CENTER + }) + + val rippleDrawable = android.graphics.drawable.RippleDrawable( + ColorStateList.valueOf(Color.argb(30, 255, 255, 255)), + LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f), + null + ) + background = rippleDrawable + + setOnClickListener { onClick() } + } + } + + private fun createAppearanceSelector(context: Context, colors: M3Colors): View { + val currentMode = LogBoxUtils.getAppearanceMode(context) + + val options = listOf("System", "Light", "Dark") + val keys = listOf("system", "light", "dark") + + val wrapper = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + layoutParams = params + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8)) + } + + val label = TextView(context).apply { + text = "Appearance" + textSize = 14f + setTextColor(colors.onSurface) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8)) + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + } + wrapper.addView(label) + + val row = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + val params = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + LogBoxUtils.dpToPx(context, 40) + ) + layoutParams = params + } + + options.forEachIndexed { idx, title -> + val key = keys[idx] + val seg = TextView(context).apply { + text = title + gravity = Gravity.CENTER + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + val segParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) + if (idx > 0) segParams.setMargins(LogBoxUtils.dpToPx(context, 8), 0, 0, 0) + layoutParams = segParams + + val isSelected = currentMode == key + background = LogBoxUtils.createM3Background( + context, + if (isSelected) colors.primaryContainer else colors.surfaceVariant, + 12f + ) + setTextColor(if (isSelected) colors.onPrimaryContainer else colors.onSurface) + setPadding(0, 0, 0, 0) + setOnClickListener { + LogBoxUtils.setAppearanceMode(context, key) + + for (i in 0 until row.childCount) { + val child = row.getChildAt(i) as TextView + val sel = keys[i] == key + child.background = LogBoxUtils.createM3Background( + context, + if (sel) colors.primaryContainer else colors.surfaceVariant, + 12f + ) + child.setTextColor(if (sel) colors.onPrimaryContainer else colors.onSurface) + } + } + } + row.addView(seg) + } + + wrapper.addView(row) + return wrapper + } + + private fun createM3Switch(context: Context, colors: M3Colors, isChecked: Boolean): View { + val switchView = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + tag = isChecked + layoutParams = LinearLayout.LayoutParams( + LogBoxUtils.dpToPx(context, 52), + LogBoxUtils.dpToPx(context, 32) + ) + } + + updateSwitchAppearance(context, switchView, colors, isChecked) + return switchView + } + + private fun updateSwitchAppearance(context: Context, switchView: View, colors: M3Colors, isChecked: Boolean) { + val trackColor = if (isChecked) colors.primary else colors.surfaceVariant + val thumbColor = if (isChecked) colors.onPrimary else colors.onSurfaceVariant + + val currentTrackColor = if (switchView.background is GradientDrawable) { + (switchView.background as GradientDrawable).color?.defaultColor + ?: (if (!isChecked) colors.primary else colors.surfaceVariant) + } else { + if (!isChecked) colors.primary else colors.surfaceVariant + } + + val trackColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentTrackColor, trackColor) + trackColorAnimator.duration = 250 + trackColorAnimator.interpolator = DecelerateInterpolator() + trackColorAnimator.addUpdateListener { animator -> + val animatedColor = animator.animatedValue as Int + val track = GradientDrawable().apply { + setColor(animatedColor) + setCornerRadius(LogBoxUtils.dpToPx(context, 16).toFloat()) + } + switchView.background = track + } + + if (switchView is LinearLayout) { + val thumb = if (switchView.childCount > 0) { + switchView.getChildAt(0) + } else { + View(context).apply { + val size = LogBoxUtils.dpToPx(context, 24) + layoutParams = LinearLayout.LayoutParams(size, size).apply { + marginStart = LogBoxUtils.dpToPx(context, 4) + marginEnd = LogBoxUtils.dpToPx(context, 24) + } + switchView.addView(this) + } + } + + val currentParams = thumb.layoutParams as LinearLayout.LayoutParams + val targetMarginStart = if (isChecked) LogBoxUtils.dpToPx(context, 24) else LogBoxUtils.dpToPx(context, 4) + + val marginAnimator = ValueAnimator.ofInt(currentParams.marginStart, targetMarginStart) + marginAnimator.duration = 250 + marginAnimator.interpolator = DecelerateInterpolator() + marginAnimator.addUpdateListener { animator -> + val animatedMargin = animator.animatedValue as Int + val params = thumb.layoutParams as LinearLayout.LayoutParams + params.marginStart = animatedMargin + params.marginEnd = LogBoxUtils.dpToPx(context, 28) - animatedMargin + thumb.layoutParams = params + } + + val currentThumbColor = if (thumb.background is GradientDrawable) { + (thumb.background as GradientDrawable).color?.defaultColor + ?: (if (!isChecked) colors.onPrimary else colors.onSurfaceVariant) + } else { + if (!isChecked) colors.onPrimary else colors.onSurfaceVariant + } + + val thumbColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentThumbColor, thumbColor) + thumbColorAnimator.duration = 250 + thumbColorAnimator.interpolator = DecelerateInterpolator() + thumbColorAnimator.addUpdateListener { animator -> + val animatedColor = animator.animatedValue as Int + thumb.background = GradientDrawable().apply { + setColor(animatedColor) + shape = GradientDrawable.OVAL + } + } + + val animatorSet = AnimatorSet() + animatorSet.playTogether(trackColorAnimator, marginAnimator, thumbColorAnimator) + animatorSet.start() + } + } + + private fun createDialog(context: Context, container: LinearLayout): AlertDialog { + val dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + dialog.window?.setBackgroundDrawable( + LogBoxUtils.createM3Background(context, Color.TRANSPARENT, 28f) + ) + + constrainContainerWidth(context, container) + return dialog + } + + private fun constrainContainerWidth(context: Context, container: LinearLayout) { + try { + val maxW = (context.resources.displayMetrics.widthPixels * 0.9).toInt() + val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.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.9).toInt() + val maxByDp = LogBoxUtils.dpToPx(context, 420) + val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp + + dialog.window?.setLayout(targetW, ViewGroup.LayoutParams.WRAP_CONTENT) + + try { + currentMenuContainer?.let { container -> + val lp = container.layoutParams ?: ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + lp.width = ViewGroup.LayoutParams.MATCH_PARENT + container.layoutParams = lp + try { + container.minimumWidth = targetW + } catch (_: Exception) { + } + container.post { + try { + container.requestLayout() + container.invalidate() + } catch (_: Exception) { + } + } + } + } catch (_: Exception) { + } + } catch (_: Exception) { + } + } + + private fun handleMenuSelection(context: Context, index: Int) { + when (index) { + 0 -> LogBoxActions.toggleSafeMode(context) + 1 -> showConfirmAction( + context, "Refetch Bundle", + "This will download the latest bundle from Github." + ) { LogBoxActions.refetchBundle(context) } + + 2 -> showCustomBundleDialog(context) + 3 -> ShiggyXposed.xposed.Utils.Companion.reloadApp() + 4 -> showConfirmAction( + context, "Clear Cache & Reset", + "This will clear all cached bundles and reset to default settings." + ) { LogBoxActions.clearCacheAndReset(context) } + + // Options submenu + 5 -> showOptionsMenu(context) + } + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxModule.kt new file mode 100644 index 0000000..b6c9457 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxModule.kt @@ -0,0 +1,199 @@ +package ShiggyXposed.xposed.modules.LogBox + +import ShiggyXposed.xposed.Module +import ShiggyXposed.xposed.Utils.Log +import android.content.Context +import de.robv.android.xposed.XC_MethodReplacement +import de.robv.android.xposed.XposedBridge +import de.robv.android.xposed.XposedHelpers +import de.robv.android.xposed.callbacks.XC_LoadPackage +import kotlinx.coroutines.* + +object LogBoxModule : Module() { + lateinit var packageParam: XC_LoadPackage.LoadPackageParam + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + var contextForMenu: Context? = null + + override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { + this@LogBoxModule.packageParam = packageParam + + try { + val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") + val getUseDeveloperSupportMethod = + dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } + + getUseDeveloperSupportMethod.hook { + before { + result = true + } + } + Log.e("Successfully hooked DCDReactNativeHost") + } catch (e: Exception) { + Log.e("Failed to hook DCDReactNativeHost: ${e.message}") + } + + return@with + } + + override fun onContext(context: Context) { + try { + Log.e("onContext called with context: $context") + contextForMenu = context + + val possibleClasses = listOf( + "com.facebook.react.devsupport.BridgeDevSupportManager", + "com.facebook.react.devsupport.BridgelessDevSupportManager", + "com.facebook.react.devsupport.DevSupportManagerImpl", + "com.facebook.react.devsupport.DevSupportManagerBase", + "com.facebook.react.devsupport.DefaultDevSupportManager" + ) + + var foundAny = false + possibleClasses.forEach { className -> + try { + val clazz = packageParam.classLoader.loadClass(className) + Log.e("Found class: $className") + hookDevSupportManager(clazz, context) + foundAny = true + } catch (e: Exception) { + Log.e("Class not found: $className - ${e.message}") + } + } + + if (!foundAny) { + tryFindDevSupportClasses(context) + } + } catch (e: Exception) { + // Handle exception + } + } + + private fun tryFindDevSupportClasses(context: Context) { + try { + val dexFile = packageParam.classLoader.javaClass.getDeclaredField("pathList") + dexFile.isAccessible = true + Log.e("Searching for DevSupport classes in classloader...") + } catch (e: Exception) { + Log.e("Could not search for classes: ${e.message}") + } + } + + private fun hookDevSupportManager(clazz: Class<*>, context: Context) { + Log.e("Attempting to hook ${clazz.name}") + + 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" } + if (handleReloadJSMethod != null) { + XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam): Any? { + Log.e("handleReloadJS called - reloading app") + ShiggyXposed.xposed.Utils.Companion.reloadApp() + return null + } + }) + } + } catch (e: Exception) { + Log.e("Failed to hook handleReloadJS: ${e.message}") + } + + try { + val showDevOptionsDialogMethod = clazz.methods.firstOrNull { it.name == "showDevOptionsDialog" } + if (showDevOptionsDialogMethod != null) { + XposedBridge.hookMethod(showDevOptionsDialogMethod, object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: MethodHookParam): Any? { + try { + var activityContext: Context? = null + try { + activityContext = getContextFromDevSupport(clazz, param.thisObject) + if (activityContext != null) { + Log.e("Successfully got context from DevSupport") + } + } 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})") + + LogBoxNavigation.showRecoveryMenu(finalContext) + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + }) + } + } catch (e: Exception) { + // Handle exception + } + } catch (e: Exception) { + // Handle exception + } + } + + private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? { + if (instance == null) { + Log.e("getContextFromDevSupport: instance is null") + return null + } + + return try { + val helpers = listOf( + "mReactInstanceDevHelper", + "reactInstanceDevHelper", + "mReactInstanceManager", + "mApplicationContext" + ) + + for (helperName in helpers) { + try { + Log.e("Trying field: $helperName") + val helperField = XposedHelpers.findFieldIfExists(clazz, helperName) + if (helperField == null) { + 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" + } + + if (getCurrentActivityMethod != null) { + val ctx = getCurrentActivityMethod.invoke(helper) as? Context + if (ctx != null) { + Log.e("Got context from $helperName.getCurrentActivity()") + return ctx + } + } + } catch (e: Exception) { + Log.e("Error trying $helperName: ${e.message}") + } + } + + Log.e("Could not get context from DevSupport object using any method") + null + } catch (e: Exception) { + Log.e("Failed to get context (outer catch): ${e.message}") + null + } + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxNavigation.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxNavigation.kt new file mode 100644 index 0000000..71dc024 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxNavigation.kt @@ -0,0 +1,9 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.content.Context + +object LogBoxNavigation { + fun showRecoveryMenu(context: Context) { + LogBoxMenu.showRecoveryMenu(context) + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxTheme.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxTheme.kt new file mode 100644 index 0000000..1f0316d --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxTheme.kt @@ -0,0 +1,95 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.content.Context +import android.graphics.Color +import org.json.JSONObject +import java.io.File +import ShiggyXposed.xposed.Utils.Log + +object LogBoxTheme { + + fun isDarkMode(context: Context): Boolean { + try { + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + if (settingsFile.exists()) { + val json = JSONObject(settingsFile.readText()) + val mode = json.optString("appearanceMode", "") + if (mode == "light") return false + if (mode == "dark") return true + } + } catch (e: Exception) { + Log.e("Error reading LogBox appearance settings: ${e.message}") + } + + return (context.resources.configuration.uiMode and + android.content.res.Configuration.UI_MODE_NIGHT_MASK) == + android.content.res.Configuration.UI_MODE_NIGHT_YES + } + + fun getM3Colors(context: Context): M3Colors { + + val isDark = isDarkMode(context) + val base = if (isDark) { + M3Colors( + surface = Color.parseColor("#1C1B1F"), + surfaceVariant = Color.parseColor("#49454F"), + onSurface = Color.parseColor("#E6E1E5"), + onSurfaceVariant = Color.parseColor("#CAC4D0"), + primary = Color.parseColor("#D0BCFF"), + onPrimary = Color.parseColor("#381E72"), + primaryContainer = Color.parseColor("#4F378B"), + onPrimaryContainer = Color.parseColor("#EADDFF"), + secondaryContainer = Color.parseColor("#4A4458"), + onSecondaryContainer = Color.parseColor("#E8DEF8"), + error = Color.parseColor("#F2B8B5"), + onError = Color.parseColor("#601410") + ) + } else { + M3Colors( + surface = Color.parseColor("#FFFBFE"), + surfaceVariant = Color.parseColor("#E7E0EC"), + onSurface = Color.parseColor("#1C1B1F"), + onSurfaceVariant = Color.parseColor("#49454F"), + primary = Color.parseColor("#6750A4"), + onPrimary = Color.parseColor("#FFFFFF"), + primaryContainer = Color.parseColor("#EADDFF"), + onPrimaryContainer = Color.parseColor("#21005D"), + secondaryContainer = Color.parseColor("#E8DEF8"), + onSecondaryContainer = Color.parseColor("#1D192B"), + error = Color.parseColor("#B3261E"), + onError = Color.parseColor("#FFFFFF") + ) + } + + var flavor = "mocha" + + try { + val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES") + if (themeFile.exists()) { + val json = JSONObject(themeFile.readText()) + val f = json.optString("menuFlavor", json.optString("flavor", json.optString("id", ""))).lowercase() + if (f.isNotEmpty()) flavor = f + } + + if (flavor.isNotEmpty()) { + val pair = LogBoxConstants.FLAVOR_COLORS[flavor] + if (pair != null) { + val primary = Color.parseColor(pair.first) + val primaryContainer = Color.parseColor(pair.second) + val onPrimary = if (isDark) Color.WHITE else Color.WHITE + val onPrimaryContainer = if (isDark) Color.BLACK else Color.BLACK + return base.copy( + primary = primary, + primaryContainer = primaryContainer, + onPrimary = onPrimary, + onPrimaryContainer = onPrimaryContainer + ) + } + } + } catch (e: Exception) { + Log.e("Error applying LogBox flavor: ${e.message}") + } + + return base + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxUtils.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxUtils.kt new file mode 100644 index 0000000..fc6f52f --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxUtils.kt @@ -0,0 +1,121 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.content.Context +import android.util.TypedValue +import android.graphics.drawable.GradientDrawable +import android.graphics.Color +import android.content.res.ColorStateList +import android.view.View +import android.widget.Toast +import org.json.JSONObject +import java.io.File +import ShiggyXposed.xposed.Constants +import ShiggyXposed.xposed.Utils.Log + +object LogBoxUtils { + + fun dpToPx(context: Context, dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + context.resources.displayMetrics + ).toInt() + } + + fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable { + return GradientDrawable().apply { + setColor(color) + setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat()) + } + } + + fun getPyoncordDirectory(context: Context): File { + val dir = File(context.filesDir, "pyoncord") + if (!dir.exists()) { + dir.mkdirs() + } + return dir + } + + fun showM3Toast(context: Context, message: String) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + fun getAppearanceMode(context: Context): String { + return try { + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + if (!settingsFile.exists()) return "system" + val json = JSONObject(settingsFile.readText()) + json.optString("appearanceMode", "system") + } catch (e: Exception) { + Log.e("Error reading appearance mode: ${e.message}") + "system" + } + } + + fun setAppearanceMode(context: Context, mode: String) { + try { + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + settingsFile.parentFile?.mkdirs() + val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject() + settings.put("appearanceMode", mode) + settingsFile.writeText(settings.toString()) + } catch (e: Exception) { + LogBoxActions.showError(context, "Failed to set appearance", e.message) + } + } + + fun saveMenuFlavor(context: Context, flavor: String) { + try { + val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES") + themeFile.parentFile?.mkdirs() + + val themeJson = JSONObject().apply { + put("menuFlavor", flavor) + } + + themeFile.writeText(themeJson.toString()) + } catch (e: Exception) { + Log.e("Error saving menu flavor: ${e.message}") + LogBoxActions.showError(context, "Failed to save flavor", e.message) + } + } + + fun isBundleInjectionDisabled(context: Context): Boolean { + return try { + val cacheDir = File(context.dataDir, Constants.CACHE_DIR) + val bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE) + val disabled = File(cacheDir, "${Constants.MAIN_SCRIPT_FILE}.disabled") + + if (disabled.exists()) { + if (bundle.exists()) { + bundle.delete() + } + return true + } + if (bundle.exists()) return false + + // If neither file exists, check settings + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + if (!settingsFile.exists()) return false + val json = JSONObject(settingsFile.readText()) + json.optBoolean("bundleInjectionDisabled", false) + } catch (e: Exception) { + Log.e("Error reading injection setting: ${e.message}") + false + } + } + + fun isSafeModeEnabled(context: Context): Boolean { + return try { + val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") + if (!settingsFile.exists()) return false + + val json = JSONObject(settingsFile.readText()) + json.optJSONObject("safeMode")?.optBoolean("enabled", false) ?: false + } catch (e: Exception) { + Log.e("Error checking safe mode: ${e.message}") + false + } + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/OptionsMenu.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/OptionsMenu.kt new file mode 100644 index 0000000..269385e --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/OptionsMenu.kt @@ -0,0 +1,68 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.app.AlertDialog +import android.content.Context + +object OptionsMenu { + + fun showOptionsMenu(context: Context) { + val colors = LogBoxTheme.getM3Colors(context) + lateinit var dialog: AlertDialog + + val container = LogBoxComponents.createMenuContainer(context, colors, 20) + container.addView(LogBoxComponents.createTitle(context, colors, "Options", center = true)) + + + + val injectionDisabled = LogBoxUtils.isBundleInjectionDisabled(context) + val injectionText = if (injectionDisabled) "Enable Bundle Injection" else "Disable Bundle Injection" + container.addView(LogBoxComponents.createM3Button(context, injectionText, colors) { + dialog.dismiss() + LogBoxActions.toggleBundleInjection(context) + }) + + container.addView(LogBoxComponents.createM3Button(context, "Refetch Bundle", colors) { + dialog.dismiss() + RecoveryMenu::class.java.getDeclaredMethod( + "showConfirmAction", + Context::class.java, + String::class.java, + String::class.java, + Runnable::class.java + ).apply { isAccessible = true }.invoke( + null, + context, + "Refetch Bundle", + "This will download the latest bundle from Github.", + Runnable { LogBoxActions.refetchBundle(context) } + ) + }) + + container.addView(LogBoxComponents.createM3Button(context, "Clear Cache & Reset", colors) { + dialog.dismiss() + RecoveryMenu::class.java.getDeclaredMethod( + "showConfirmAction", + Context::class.java, + String::class.java, + String::class.java, + Runnable::class.java + ).apply { isAccessible = true }.invoke( + null, + context, + "Clear Cache & Reset", + "This will clear all cached bundles and reset to default settings.", + Runnable { LogBoxActions.clearCacheAndReset(context) } + ) + }) + + container.addView(LogBoxComponents.createM3Button(context, "Back", colors) { + dialog.dismiss() + RecoveryMenu.showRecoveryMenu(context) + }) + + dialog = LogBoxDialogs.createDialog(context, container) + LogBoxDialogs.currentMenuContainer = container + dialog.show() + LogBoxDialogs.setDialogWindowWidth(dialog, context) + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/RecoveryMenu.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/RecoveryMenu.kt new file mode 100644 index 0000000..37dd375 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/RecoveryMenu.kt @@ -0,0 +1,143 @@ +package ShiggyXposed.xposed.modules.LogBox + +import android.app.AlertDialog +import android.content.Context +import android.widget.LinearLayout +import ShiggyXposed.xposed.Utils.Log + +object RecoveryMenu { + + fun showRecoveryMenu(context: Context) { + Log.e("showRecoveryMenu called with context: $context") + try { + val colors = LogBoxTheme.getM3Colors(context) + lateinit var dialog: AlertDialog + + val container = LogBoxComponents.createMenuContainer(context, colors) + + container.addView(LogBoxComponents.createTitle(context, colors, "ShiggyXposed Recovery", center = true)) + container.addView(LogBoxComponents.createSubtitle(context, colors, "Select an option to continue")) + + val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode" + container.addView(LogBoxComponents.createM3Button(context, safeModeText, colors) { + dialog.dismiss() + handleMenuSelection(context, 0) + }) + + container.addView(LogBoxComponents.createM3Button(context, "Load Custom Bundle", colors) { + dialog.dismiss() + handleMenuSelection(context, 2) + }) + + container.addView(LogBoxComponents.createM3Button(context, "Reload App", colors) { + dialog.dismiss() + handleMenuSelection(context, 3) + }) + + val optionsBtn = LogBoxComponents.createM3Button(context, "Options", colors) { + dialog.dismiss() + handleMenuSelection(context, 5) + } + (optionsBtn.layoutParams as? LinearLayout.LayoutParams)?.let { lp -> + lp.setMargins(lp.leftMargin, LogBoxUtils.dpToPx(context, 16), lp.rightMargin, lp.bottomMargin) + optionsBtn.layoutParams = lp + } + container.addView(optionsBtn) + + dialog = LogBoxDialogs.createDialog(context, container) + LogBoxDialogs.currentMenuContainer = container + dialog.show() + LogBoxDialogs.setDialogWindowWidth(dialog, context) + Log.e("Recovery menu shown successfully") + } catch (e: Exception) { + Log.e("Error showing recovery menu: ${e.message}", e) + throw e + } + } + + private fun handleMenuSelection(context: Context, index: Int) { + when (index) { + 0 -> LogBoxActions.toggleSafeMode(context) + 1 -> showConfirmAction(context, "Refetch Bundle", "This will download the latest bundle from Github.") { + LogBoxActions.refetchBundle(context) + } + + 2 -> CustomBundleDialog.showCustomBundleDialog(context) + 3 -> ShiggyXposed.xposed.Utils.Companion.reloadApp() + 4 -> showConfirmAction( + context, + "Clear Cache & Reset", + "This will clear all cached bundles and reset to default settings." + ) { + LogBoxActions.clearCacheAndReset(context) + } + + 5 -> OptionsMenu.showOptionsMenu(context) + } + } + + private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) { + val colors = LogBoxTheme.getM3Colors(context) + val container = LogBoxComponents.createMenuContainer(context, colors) + + container.addView(LogBoxComponents.createTitle(context, colors, title)) + + val messageView = android.widget.TextView(context).apply { + text = message + textSize = 14f + setTextColor(colors.onSurfaceVariant) + setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24)) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + lineHeight = LogBoxUtils.dpToPx(context, 20) + } + } + container.addView(messageView) + + val buttonContainer = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = android.view.Gravity.END + } + + val cancelButton = android.widget.TextView(context).apply { + text = "Cancel" + setTextColor(colors.primary) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + setPadding( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + val confirmButton = android.widget.TextView(context).apply { + text = "Confirm" + setTextColor(colors.primary) + textSize = 14f + typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL) + setPadding( + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10), + LogBoxUtils.dpToPx(context, 16), + LogBoxUtils.dpToPx(context, 10) + ) + } + + val dialog = LogBoxDialogs.createDialog(context, container) + + cancelButton.setOnClickListener { dialog.dismiss() } + confirmButton.setOnClickListener { + dialog.dismiss() + action() + } + + buttonContainer.addView(cancelButton) + buttonContainer.addView(confirmButton) + container.addView(buttonContainer) + + LogBoxDialogs.currentMenuContainer = container + dialog.show() + LogBoxDialogs.setDialogWindowWidth(dialog, context) + } +}