From be86071f4f0827218c7c5b05ee53a236311b9c7a Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:15:11 +0100 Subject: [PATCH] feat: refactor LogBoxModule file stracture --- .../kotlin/ShiggyXposed/xposed/xposed/Main.kt | 1 + .../modules/LogBox/CustomBundleDialog.kt | 170 ++ .../modules/LogBox/FlavorSelectionMenu.kt | 35 + .../xposed/modules/LogBox/LogBoxActions.kt | 186 ++ .../xposed/modules/LogBox/LogBoxComponents.kt | 257 +++ .../xposed/modules/LogBox/LogBoxConstants.kt | 35 + .../xposed/modules/LogBox/LogBoxDialogs.kt | 74 + .../xposed/modules/LogBox/LogBoxMenu.kt | 782 ++++++++ .../xposed/modules/LogBox/LogBoxModule.kt | 199 ++ .../xposed/modules/LogBox/LogBoxNavigation.kt | 9 + .../xposed/modules/LogBox/LogBoxTheme.kt | 95 + .../xposed/modules/LogBox/LogBoxUtils.kt | 114 ++ .../xposed/modules/LogBox/OptionsMenu.kt | 68 + .../xposed/modules/LogBox/RecoveryMenu.kt | 143 ++ .../xposed/xposed/modules/LogBoxModule.kt | 1653 ----------------- 15 files changed, 2168 insertions(+), 1653 deletions(-) create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/CustomBundleDialog.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/FlavorSelectionMenu.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxActions.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxComponents.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxConstants.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxDialogs.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxModule.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxNavigation.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxTheme.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxUtils.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/OptionsMenu.kt create mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/RecoveryMenu.kt delete mode 100644 app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt index 127533f..b7ddec6 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/Main.kt @@ -16,6 +16,7 @@ import ShiggyXposed.xposed.modules.bridge.AdditionalBridgeMethodsModule import ShiggyXposed.xposed.modules.bridge.BridgeModule import ShiggyXposed.xposed.modules.no_track.BlockCrashReportingModule import ShiggyXposed.xposed.modules.no_track.BlockDeepLinksTrackingModule +import ShiggyXposed.xposed.modules.LogBox.* import kotlinx.coroutines.CompletableDeferred object HookStateHolder { 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..39fd5b4 --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxActions.kt @@ -0,0 +1,186 @@ +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() + + if (bundle.exists()) { + if (disabled.exists()) disabled.delete() + val renamed = bundle.renameTo(disabled) + settings.put("bundleInjectionDisabled", true) + settingsFile.writeText(settings.toString()) + Toast.makeText( + context, + if (renamed) "Bundle injection disabled" else "Bundle injection disabled (marker set)", + Toast.LENGTH_SHORT + ).show() + } else if (disabled.exists()) { + val renamed = disabled.renameTo(bundle) + settings.put("bundleInjectionDisabled", false) + settingsFile.writeText(settings.toString()) + Toast.makeText( + context, + if (renamed) "Bundle injection enabled" else "Bundle injection enabled (marker removed)", + Toast.LENGTH_SHORT + ).show() + } else { + disabled.writeText("") + settings.put("bundleInjectionDisabled", true) + settingsFile.writeText(settings.toString()) + Toast.makeText(context, "Bundle injection disabled (marker created)", Toast.LENGTH_SHORT).show() + } + } 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..66713ec --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt @@ -0,0 +1,782 @@ +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 + +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) + }) + + 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..495e66f --- /dev/null +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxUtils.kt @@ -0,0 +1,114 @@ +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()) return true + if (bundle.exists()) return false + + 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) + } +} diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt deleted file mode 100644 index 48222de..0000000 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt +++ /dev/null @@ -1,1653 +0,0 @@ -package ShiggyXposed.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.os.Build -import android.graphics.drawable.GradientDrawable -import android.util.TypedValue -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -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 -import de.robv.android.xposed.callbacks.XC_LoadPackage -import ShiggyXposed.xposed.Module -import ShiggyXposed.xposed.Utils.Companion.reloadApp -import ShiggyXposed.xposed.Utils.Log -import ShiggyXposed.xposed.Constants -import kotlinx.coroutines.* -import org.json.JSONObject -import java.io.File -import java.net.HttpURLConnection -import java.net.URL - -/** - * LogBoxModule: provides a recovery/dev menu inside Discord's dev dialog. - * - * This file implements: - * - Recovery menu - * - Options menu (opens Themes submenu) - * - Themes submenu with segmented appearance selector (System / Light / Dark), - * flavor selection, and the relocated Clear Cache & Reset and Refetch Bundle buttons. - * - * Navigation: - * - Recovery -> Options -> Themes - * - Each submenu has a Back button to return to the previous menu. - * - * All visual changes in Themes affect only the LogBox menu UI (stored in files/logbox/...). - */ -object LogBoxModule : Module() { - lateinit var packageParam: XC_LoadPackage.LoadPackageParam - private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - private var contextForMenu: Context? = null - - // Hold currently visible menu container so we can update colors in-place without closing dialogs - private var currentMenuContainer: LinearLayout? = 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) { - } - } - - 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}") - - // 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" } - if (handleReloadJSMethod != null) { - XposedBridge.hookMethod(handleReloadJSMethod, object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam): Any? { - Log.e("handleReloadJS called - reloading app") - 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})") - - showRecoveryMenu(finalContext) - } catch (e: Exception) { - e.printStackTrace() - } - return null - } - }) - } - } catch (e: Exception) { - } - } catch (e: Exception) { - } - } - - private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? { - if (instance == null) { - 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", - "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 - } - } - - // Material 3 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 isDarkMode(context: Context): Boolean { - // Check logbox-specific appearance override first - 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 - // if "system" or empty, fall through to system detection - } - } 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 - } - - private 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" - } - } - - private fun getM3Colors(context: Context): M3Colors { - val isDark = isDarkMode(context) - // Base palettes - 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" - - // Apply LogBox menu flavor override if present (logbox/LOGBOX_THEMES) - 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 flavorMap = 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 pair = flavorMap[flavor] - if (pair != null) { - val primary = Color.parseColor(pair.first) - val primaryContainer = Color.parseColor(pair.second) - // Keep contrast choices simple: white on primary for most flavors - 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 - } - - 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 secondaryContainer: Int, - val onSecondaryContainer: Int, - val error: Int, - val onError: Int - ) - - private fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable { - return GradientDrawable().apply { - setColor(color) - setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat()) - } - } - - private fun createM3Button(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View { - return LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER - background = createM3Background(context, colors.primaryContainer, 20f) - setPadding( - dpToPx(context, 24), - dpToPx(context, 10), - dpToPx(context, 24), - dpToPx(context, 10) - ) - clipToOutline = true - isClickable = true - isFocusable = true - - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - dpToPx(context, 40) - ).apply { - setMargins(0, 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 - }) - - // Create a custom ripple effect that stays within bounds - val rippleDrawable = android.graphics.drawable.RippleDrawable( - ColorStateList.valueOf(Color.argb(30, 255, 255, 255)), - createM3Background(context, colors.primaryContainer, 20f), - null - ) - background = rippleDrawable - - setOnClickListener { onClick() } - } - } - - private fun showRecoveryMenu(context: Context) { - Log.e("showRecoveryMenu called with context: $context") - try { - val colors = getM3Colors(context) - 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, 28f) - } - - // Title - val titleView = TextView(context).apply { - text = "ShiggyXposed Recovery" - textSize = 22f - setTextColor(colors.onSurface) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) - setPadding(0, 0, 0, dpToPx(context, 16)) - gravity = Gravity.CENTER - } - container.addView(titleView) - - // Subtitle - val subtitleView = TextView(context).apply { - text = "Select an option to continue" - textSize = 14f - setTextColor(colors.onSurfaceVariant) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL) - setPadding(0, 0, 0, dpToPx(context, 24)) - gravity = Gravity.CENTER - } - container.addView(subtitleView) - - // Safe Mode Button - val safeModeText = if (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 (moved last with extra top spacing) - val optionsBtn = createM3Button(context, "Options", colors) { - dialog.dismiss() - handleMenuSelection(context, 5) - } - // increase top margin for visual gap - (optionsBtn.layoutParams as? LinearLayout.LayoutParams)?.let { lp -> - lp.setMargins(lp.leftMargin, dpToPx(context, 16), lp.rightMargin, lp.bottomMargin) - optionsBtn.layoutParams = lp - } - container.addView(optionsBtn) - - dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - // Constrain dialog/content width so the menu doesn't span the full screen - constrainContainerWidth(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 - } - } - - private fun handleMenuSelection(context: Context, index: Int) { - when (index) { - 0 -> toggleSafeMode(context) - 1 -> showConfirmAction( - context, "Refetch Bundle", - "This will download the latest bundle from Github." - ) { refetchBundle(context) } - - 2 -> showCustomBundleDialog(context) - 3 -> reloadApp() - 4 -> showConfirmAction( - context, "Clear Cache & Reset", - "This will clear all cached bundles and reset to default settings." - ) { clearCacheAndReset(context) } - - // Options submenu - 5 -> showOptionsMenu(context) - } - } - - private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) { - val colors = getM3Colors(context) - - 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, 28f) - } - - val titleView = TextView(context).apply { - text = title - textSize = 20f - 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)) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - lineHeight = dpToPx(context, 20) - } - } - container.addView(messageView) - - val buttonContainer = LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.END - } - - // Cancel button - 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, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - // Confirm button - 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, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - 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, 28f) - ) - - // Constrain to sane width before showing - constrainContainerWidth(context, container) - currentMenuContainer = container - - dialog.show() - setDialogWindowWidth(dialog, context) - } - - // Options menu -> entry point for Themes submenu & other options - private fun showOptionsMenu(context: Context) { - val colors = getM3Colors(context) - lateinit var dialog: AlertDialog - - val container = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - setPadding( - dpToPx(context, 20), - dpToPx(context, 20), - dpToPx(context, 20), - dpToPx(context, 20) - ) - background = createM3Background(context, colors.surface, 24f) - } - - val titleView = TextView(context).apply { - text = "Options" - textSize = 20f - setTextColor(colors.onSurface) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) - setPadding(0, 0, 0, dpToPx(context, 12)) - gravity = Gravity.CENTER - } - container.addView(titleView) - - // Themes button -> opens Themes submenu - container.addView(createM3Button(context, "Themes", colors) { - dialog.dismiss() - showThemesMenu(context) - }) - - // Disable/Enable bundle injection - val injectionDisabled = isBundleInjectionDisabled(context) - val injectionText = if (injectionDisabled) "Enable Bundle Injection" else "Disable Bundle Injection" - container.addView(createM3Button(context, injectionText, colors) { - dialog.dismiss() - toggleBundleInjection(context) - }) - - container.addView(createM3Button(context, "Refetch Bundle", colors) { - dialog.dismiss() - showConfirmAction(context, "Refetch Bundle", "This will download the latest bundle from Github.") { - 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.") { - clearCacheAndReset(context) - } - }) - - container.addView(createM3Button(context, "Back", colors) { - dialog.dismiss() - showRecoveryMenu(context) - }) - - dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - // Constrain to sane width before showing - constrainContainerWidth(context, container) - currentMenuContainer = container - dialog.show() - } - - // Themes submenu: segmented selector for appearance + flavors + relocated buttons (Refetch/Reset) - private fun showThemesMenu(context: Context) { - val colors = getM3Colors(context) - lateinit var dialog: AlertDialog - - val container = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - setPadding( - dpToPx(context, 20), - dpToPx(context, 20), - dpToPx(context, 20), - dpToPx(context, 20) - ) - background = createM3Background(context, colors.surface, 24f) - } - - val titleView = TextView(context).apply { - text = "Themes" - textSize = 20f - setTextColor(colors.onSurface) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) - setPadding(0, 0, 0, dpToPx(context, 12)) - gravity = Gravity.CENTER - } - container.addView(titleView) - - // Segmented appearance selector (Follow system / Force light / Force dark) - 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 = AlertDialog.Builder(context) - .setView(container) - .create() - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - currentMenuContainer = container - dialog.show() - setDialogWindowWidth(dialog, context) - setDialogWindowWidth(dialog, context) - } - - // Create a segmented selector (3 options) for appearance mode - private fun createAppearanceSelector(context: Context, colors: M3Colors): View { - val currentMode = 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, dpToPx(context, 8)) - } - - val label = TextView(context).apply { - text = "Appearance" - textSize = 14f - setTextColor(colors.onSurface) - setPadding(0, 0, 0, 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, - dpToPx(context, 40) - ) - layoutParams = params - } - - // Build three segments - 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(dpToPx(context, 8), 0, 0, 0) - layoutParams = segParams - - // Appearance of selected/unselected - val isSelected = currentMode == key - background = createM3Background(context, if (isSelected) colors.primaryContainer else colors.surfaceVariant, 12f) - setTextColor(if (isSelected) colors.onPrimaryContainer else colors.onSurface) - setPadding(0, 0, 0, 0) - setOnClickListener { - setAppearanceMode(context, key) - - // Update visuals for the segmented control immediately without closing dialogs. - // Row is the container holding the segments; update each child to reflect selection. - for (i in 0 until row.childCount) { - val child = row.getChildAt(i) as TextView - val sel = keys[i] == key - child.background = createM3Background(context, if (sel) colors.primaryContainer else colors.surfaceVariant, 12f) - child.setTextColor(if (sel) colors.onPrimaryContainer else colors.onSurface) - } - - // Force a repaint of the containing menu so changes are visible immediately - applyMenuColors(context) - } - } - row.addView(seg) - } - - wrapper.addView(row) - return wrapper - } - - private 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()) - applyMenuColors(context) - } catch (e: Exception) { - showError(context, "Failed to set appearance", e.message) - } - } - - private fun showFlavorSelection(context: Context) { - val colors = getM3Colors(context) - val flavors = listOf("blue", "green", "mocha", "vanilla", "purple", "amber", "teal") - lateinit var dialog: AlertDialog - - val container = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - setPadding( - dpToPx(context, 20), - dpToPx(context, 20), - dpToPx(context, 20), - dpToPx(context, 20) - ) - background = createM3Background(context, colors.surface, 20f) - } - - val titleView = TextView(context).apply { - text = "Select Menu Flavor" - textSize = 18f - setTextColor(colors.onSurface) - typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.BOLD) - setPadding(0, 0, 0, dpToPx(context, 12)) - gravity = Gravity.CENTER - } - container.addView(titleView) - - flavors.forEach { flavor -> - container.addView(createM3Button(context, flavor.replaceFirstChar { it.uppercase() }, colors) { - try { - dialog.dismiss() - } catch (_: Exception) {} - saveMenuFlavor(context, flavor) - showThemesMenu(context) - }) - } - - dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - // Constrain to sane width before showing - constrainContainerWidth(context, container) - currentMenuContainer = container - dialog.show() - } - - private 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()) - applyMenuColors(context) - } catch (e: Exception) { - Log.e("Error saving menu flavor: ${e.message}") - showError(context, "Failed to save flavor", e.message) - } - } - - private fun isBundleInjectionDisabled(context: Context): Boolean { - return try { - // Prefer checking the cache location used by HookScriptLoader. - 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()) return true - if (bundle.exists()) return false - - // Fallback to the persisted logbox setting if neither file exists - 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 - } - } - - private fun toggleBundleInjection(context: Context) { - try { - // Operate on the cache directory so HookScriptLoader sees the change immediately. - 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") - - // Persist user intention in logbox settings as well - val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") - settingsFile.parentFile?.mkdirs() - val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject() - - if (bundle.exists()) { - // disable injection by renaming the bundle out of the expected filename - if (disabled.exists()) disabled.delete() - val renamed = bundle.renameTo(disabled) - settings.put("bundleInjectionDisabled", true) - settingsFile.writeText(settings.toString()) - showM3Toast(context, if (renamed) "Bundle injection disabled" else "Bundle injection disabled (marker set)") - } else if (disabled.exists()) { - // enable injection by restoring the bundle filename - val renamed = disabled.renameTo(bundle) - settings.put("bundleInjectionDisabled", false) - settingsFile.writeText(settings.toString()) - showM3Toast(context, if (renamed) "Bundle injection enabled" else "Bundle injection enabled (marker removed)") - } else { - // Neither exists — create a disabled marker so injection stays disabled until enabled - disabled.writeText("") - settings.put("bundleInjectionDisabled", true) - settingsFile.writeText(settings.toString()) - showM3Toast(context, "Bundle injection disabled (marker created)") - } - applyMenuColors(context) - } catch (e: Exception) { - Log.e("Error toggling bundle injection: ${e.message}") - showError(context, "Failed to toggle bundle injection", e.message) - } - } - - private 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 - } - } - - private 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()) - - showM3Toast(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}") - reloadApp() - - } catch (e: Exception) { - Log.e("Error toggling safe mode: ${e.message}") - showError(context, "Failed to toggle safe mode", e.message) - } - } - - private fun showM3Toast(context: Context, message: String) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } - - private fun refetchBundle(context: Context) { - try { - val pyoncordDir = 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) - } - } - - private fun clearCacheAndReset(context: Context) { - scope.launch(Dispatchers.IO) { - try { - val pyoncordDir = 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() - } - } - } - } - - private fun showCustomBundleDialog(context: Context) { - val colors = getM3Colors(context) - val filesDir = File(context.filesDir, "pyoncord") - 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 = LinearLayout(context).apply { - orientation = LinearLayout.VERTICAL - setPadding( - dpToPx(context, 24), - dpToPx(context, 24), - dpToPx(context, 24), - dpToPx(context, 24) - ) - background = createM3Background(context, colors.surface, 28f) - } - - val titleView = TextView(context).apply { - text = "Custom Bundle URL" - textSize = 20f - setTextColor(colors.onSurface) - typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) - setPadding(0, 0, 0, dpToPx(context, 16)) - } - container.addView(titleView) - - // 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, 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 = 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) - ) - isEnabled - alpha = if (isEnabled) 1f else 0.5f - val params = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - setMargins(0, 0, 0, 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( - dpToPx(context, 16), - dpToPx(context, 10), - dpToPx(context, 16), - 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( - dpToPx(context, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - val dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - cancelButton.setOnClickListener { dialog.dismiss() } - saveButton.setOnClickListener { - try { - val url = urlInput.text?.toString()?.trim() ?: "" - val enabled = toggleSwitch.tag as Boolean - - if (enabled && url.isNotEmpty()) { - setCustomBundleURL(context, url, true) - dialog.dismiss() - } else if (!enabled) { - 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) - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - // Constrain to sane width before showing - constrainContainerWidth(context, container) - currentMenuContainer = container - dialog.show() - setDialogWindowWidth(dialog, context) - setDialogWindowWidth(dialog, context) - } - - 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( - dpToPx(context, 52), - 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(dpToPx(context, 16).toFloat()) - } - switchView.background = track - } - - if (switchView is LinearLayout) { - val thumb = if (switchView.childCount > 0) { - switchView.getChildAt(0) - } else { - // Create new thumb if none exists - View(context).apply { - val size = dpToPx(context, 24) - layoutParams = LinearLayout.LayoutParams(size, size).apply { - marginStart = dpToPx(context, 4) - marginEnd = dpToPx(context, 24) - } - switchView.addView(this) - } - } - - val currentParams = thumb.layoutParams as LinearLayout.LayoutParams - val targetMarginStart = if (isChecked) dpToPx(context, 24) else 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 = 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 setCustomBundleURL(context: Context, url: String, enabled: Boolean) { - try { - val pyoncordDir = 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) - } - } - - private fun disableCustomBundle(context: Context) { - try { - val configFile = File(getPyoncordDirectory(context), "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", false) - customLoadUrl.put("url", "http://localhost:4040/shiggycord.js") - config.put("customLoadUrl", customLoadUrl) - - configFile.writeText(config.toString()) - Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show() - reloadApp() - } catch (e: Exception) { - Toast.makeText(context, "Failed to disable custom bundle", Toast.LENGTH_SHORT).show() - } - } - - private fun resetBundle(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()) - } - - reloadApp() - } catch (e: Exception) { - Log.e("Error resetting bundle: ${e.message}") - showError(context, "Failed to reset bundle", e.message) - } - } - - private fun getPyoncordDirectory(context: Context): File { - val dir = File(context.filesDir, "pyoncord") - if (!dir.exists()) { - dir.mkdirs() - } - return dir - } - - /** - * Apply computed LogBox colors to any currently open menu dialogs in-place. - * This updates backgrounds and the simple button label color so changing the theme - * or flavor takes effect immediately without restarting Discord or closing the menu. - * - * Force a layout/invalidate pass so views repaint with new colors. - */ - private fun constrainContainerWidth(context: Context, container: LinearLayout) { - // Limit dialog content width so Xposed dev menu doesn't become extremely wide. - 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) { - } - } - - /** - * After showing the dialog, also constrain the window width to a sane maximum - * (min(90% of screen width, 420dp)) so dialogs don't expand too wide on tablets. - */ - private fun setDialogWindowWidth(dialog: AlertDialog, context: Context) { - try { - val screenW = context.resources.displayMetrics.widthPixels - val maxByPercent = (screenW * 0.9).toInt() - val maxByDp = 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 applyMenuColors(context: Context) { - try { - val container = currentMenuContainer ?: return - val colors = getM3Colors(context) - - // update container background and all nested children recursively - fun applyToGroup(group: ViewGroup) { - try { - group.background = createM3Background(context, colors.surface, 28f) - } catch (_: Exception) {} - for (i in 0 until group.childCount) { - val child = group.getChildAt(i) - try { - when (child) { - is EditText -> { - // EditText is a TextView subclass - ensure both text and hint colors update - child.setTextColor(colors.onSurface) - child.setHintTextColor(colors.onSurfaceVariant) - try { - child.background = createM3Background(context, colors.surfaceVariant, 12f) - } catch (_: Exception) {} - } - is TextView -> { - child.setTextColor(colors.onSurface) - } - is ViewGroup -> { - // If it's a clickable button-like group, style it as a button - if (child.isClickable && child.childCount > 0 && child.getChildAt(0) is TextView) { - try { - child.background = createM3Background(context, colors.primaryContainer, 20f) - (child.getChildAt(0) as TextView).setTextColor(colors.onPrimaryContainer) - } catch (_: Exception) {} - } - applyToGroup(child) - } - else -> { - // nothing special - } - } - } catch (_: Exception) { - } - - // Force redraw on the child - try { - child.invalidate() - child.requestLayout() - } catch (_: Exception) {} - } - } - - applyToGroup(container) - - // also ensure container invalidates/repaints on main thread and nudge visibility to force redraw - try { - container.post { - container.invalidate() - container.requestLayout() - val prev = container.visibility - container.visibility = View.GONE - container.visibility = prev - } - } catch (_: Exception) {} - - } catch (e: Exception) { - Log.e("Error applying menu colors: ${e.message}") - } - } - - // Helper to update nested ViewGroups (buttons/rows) - private fun applyMenuColorsToGroup(group: ViewGroup, colors: M3Colors, context: Context) { - try { - for (k in 0 until group.childCount) { - val v = group.getChildAt(k) - when (v) { - is TextView -> v.setTextColor(colors.onSurface) - is ViewGroup -> applyMenuColorsToGroup(v, colors, context) - else -> { /* ignore */ } - } - } - // If group appears to be a button container (clickable with text child), update its background - if (group.isClickable && group.childCount > 0 && group.getChildAt(0) is TextView) { - group.background = createM3Background(context, colors.primaryContainer, 20f) - (group.getChildAt(0) as TextView).setTextColor(colors.onPrimaryContainer) - } - } catch (_: Exception) { - } - } - - private fun showError(context: Context, title: String, message: String?) { - val colors = getM3Colors(context) - - 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, 28f) - } - - 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, 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)) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - lineHeight = 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( - dpToPx(context, 16), - dpToPx(context, 10), - dpToPx(context, 16), - dpToPx(context, 10) - ) - } - - val dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - okButton.setOnClickListener { dialog.dismiss() } - container.addView(okButton) - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - // Expose this container so applyMenuColors can update error dialog visuals immediately - constrainContainerWidth(context, container) - currentMenuContainer = container - - dialog.show() - setDialogWindowWidth(dialog, context) - } -}