From 9f1941e9743835bf9f17a9111f67260ed9a64df0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 18 Nov 2025 13:02:47 +0000 Subject: [PATCH 01/19] chore: bump version to 1.4.6 (1406) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 20777b0..62b89fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "io.github.revenge.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1405 - versionName = "1.4.5" + versionCode = 1406 + versionName = "1.4.6" } buildTypes { From 06d6778c4596ce472bfbd5bce633c05e0a0c6dbb Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Fri, 21 Nov 2025 13:01:55 +0700 Subject: [PATCH 02/19] fix(modules/no_track/BlockCrashReportingModule): also hook isDisabled to true Prevents Discord from crashing when trying to set a Sentry tag. --- .../no_track/BlockCrashReportingModule.kt | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt index b6468fd..529b7ca 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt @@ -12,12 +12,23 @@ import io.github.revenge.xposed.Utils.Log object BlockCrashReportingModule : Module() { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting") - crashReportingClass?.hookMethod( - "init", Context::class.java, String::class.java - ) { - before { - Log.i("Blocked CrashReporting initialization") - result = null + crashReportingClass?.apply { + // Hooking this will result in a crash after a few seconds, since Discord expects initialization to complete when setting a Sentry tag. + // So we also hook isDisabled to make sure those calls are no-ops. + hookMethod( + "init", Context::class.java, String::class.java + ) { + before { + Log.i("Blocked CrashReporting initialization") + result = null + } + } + + hookMethod("isDisabled") { + before { + Log.i("Forced CrashReporting.isDisabled() to true") + result = true + } } } From f49ea13da528f27748f22028bc61fa5d45aaa3f1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 21 Nov 2025 06:17:17 +0000 Subject: [PATCH 03/19] chore: bump version to 1.4.7 (1407) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 62b89fe..c69992d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "io.github.revenge.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1406 - versionName = "1.4.6" + versionCode = 1407 + versionName = "1.4.7" } buildTypes { From 4a8df8954e433269198b3af7688822a779554664 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Fri, 21 Nov 2025 16:36:14 +0700 Subject: [PATCH 04/19] fix(modules/no_track/BlockCrashReportingModule): make backwards compatible --- .../modules/no_track/BlockCrashReportingModule.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt index 529b7ca..986662b 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt @@ -24,10 +24,13 @@ object BlockCrashReportingModule : Module() { } } - hookMethod("isDisabled") { - before { - Log.i("Forced CrashReporting.isDisabled() to true") - result = true + // This only exists on 30720x and above + runCatching { + hookMethod("isDisabled") { + before { + Log.i("Forced CrashReporting.isDisabled() to true") + result = true + } } } } From c20cf01f6bb9612e3aede1c5544f90d4c4a43ba1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 21 Nov 2025 09:44:30 +0000 Subject: [PATCH 05/19] chore: bump version to 1.4.8 (1408) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c69992d..f95c4b2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "io.github.revenge.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1407 - versionName = "1.4.7" + versionCode = 1408 + versionName = "1.4.8" } buildTypes { From 38c708276041da7d9e56101e9795ad48b16370eb Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Sat, 22 Nov 2025 14:49:17 +0700 Subject: [PATCH 06/19] fix(modules/no_track/BlockCrashReportingModule): support 307204+ Discord pushed an update that changed some logic for the setTag method. Previously it only asserted initialization if isDisabled returns false, now it asserts initialization regardless. The init method sets the initialization latch and returns if crash reporting is disabled, so we don't need to hook that anymore. --- .../no_track/BlockCrashReportingModule.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt index 986662b..c387a91 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/no_track/BlockCrashReportingModule.kt @@ -13,16 +13,6 @@ object BlockCrashReportingModule : Module() { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting") crashReportingClass?.apply { - // Hooking this will result in a crash after a few seconds, since Discord expects initialization to complete when setting a Sentry tag. - // So we also hook isDisabled to make sure those calls are no-ops. - hookMethod( - "init", Context::class.java, String::class.java - ) { - before { - Log.i("Blocked CrashReporting initialization") - result = null - } - } // This only exists on 30720x and above runCatching { @@ -32,6 +22,18 @@ object BlockCrashReportingModule : Module() { result = true } } + }.onFailure { + // In older versions, this hook works fine. + // Hooking this on 30720x will result in a crash after a few seconds, + // since Discord asserts initialization when setting a Sentry tag. + hookMethod( + "init", Context::class.java, String::class.java + ) { + before { + Log.i("Blocked CrashReporting initialization") + result = null + } + } } } From 5c29b1072bd78358893848d3224dab2df8812cdd Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 23 Nov 2025 22:03:50 +0000 Subject: [PATCH 07/19] chore: bump version to 1.4.9 (1409) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f95c4b2..a9d327d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "io.github.revenge.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1408 - versionName = "1.4.8" + versionCode = 1409 + versionName = "1.4.9" } buildTypes { From bf0389fe99fb37c3fa20859d184cb6a1f9025f59 Mon Sep 17 00:00:00 2001 From: cocobo1 Date: Sun, 11 Jan 2026 16:34:38 +0200 Subject: [PATCH 08/19] feat: hermes bytecode support --- app/build.gradle.kts | 5 +- .../kotlin/io/github/revenge/xposed/Utils.kt | 5 - .../revenge/xposed/modules/UpdaterModule.kt | 189 ++++++++---------- 3 files changed, 85 insertions(+), 114 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 704ca93..8572120 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "cocobo1.pupu.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1330 - versionName = "1.3.3" + versionCode = 1340 + versionName = "1.3.4" } buildTypes { @@ -47,4 +47,5 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") implementation("io.ktor:ktor-client-core:3.3.1") implementation("io.ktor:ktor-client-cio:3.3.1") + implementation("dev.rushii:libunbound:1.0.0") } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt b/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt index f001622..f174d45 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt @@ -37,11 +37,6 @@ class Utils { reloadApp() } - - 2 -> { - UpdaterModule.resetLoaderConfig(context) - reloadApp() - } } }.show() } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt index 351ce91..f35acb4 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt @@ -5,8 +5,8 @@ import android.app.AlertDialog import android.content.Context import android.util.AtomicFile import android.widget.Toast -import androidx.core.util.writeBytes import de.robv.android.xposed.callbacks.XC_LoadPackage +import dev.rushii.libunbound.LibUnbound import io.github.revenge.xposed.Constants import io.github.revenge.xposed.Module import io.github.revenge.xposed.Utils @@ -22,25 +22,19 @@ import io.ktor.client.statement.* import io.ktor.http.* import kotlinx.coroutines.* import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString import java.io.File import java.lang.ref.WeakReference @Serializable -data class CustomLoadUrl( - val enabled: Boolean = false, val url: String = "" -) +data class CustomLoadUrl(val enabled: Boolean = false, val url: String = "") @Serializable -data class LoaderConfig( - val customLoadUrl: CustomLoadUrl = CustomLoadUrl(), - val disableInjection: Boolean = false -) +data class LoaderConfig(val customLoadUrl: CustomLoadUrl = CustomLoadUrl(), val disableInjection: Boolean = false) + +@Serializable +data class EndpointInfo(val paths: ArrayList, val hash: String? = null, val version: String) -/** - * Module that updates the JS bundle by downloading it from a remote URL. - * - * Shows dialogs when failed allowing retry. - */ object UpdaterModule : Module() { private lateinit var config: LoaderConfig private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -50,13 +44,13 @@ object UpdaterModule : Module() { private lateinit var bundle: File private lateinit var etag: File - private const val TIMEOUT_CACHED = 5000L - private const val TIMEOUT = 10000L + private const val TIMEOUT_STRICT = 15000L + private const val MIN_BYTECODE_SIZE = 512 private const val ETAG_FILE = "etag.txt" private const val CONFIG_FILE = "loader.json" - private const val DEFAULT_BUNDLE_URL = - "https://codeberg.org/cocobo1/Kettu/raw/branch/dist/kettu.min.js" + private const val DEFAULT_BASE_URL = "https://codeberg.org/cocobo1/Kettu/raw/branch/dist/" + private const val DEFAULT_BUNDLE_NAME = "kettu.min.js" override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() } @@ -66,11 +60,8 @@ object UpdaterModule : Module() { etag = File(cacheDir, ETAG_FILE) val configFile = File(filesDir, CONFIG_FILE) - config = runCatching { - if (configFile.exists()) { - JSON.decodeFromString(configFile.readText()) - } else LoaderConfig() + if (configFile.exists()) JSON.decodeFromString(configFile.readText()) else LoaderConfig() }.getOrDefault(LoaderConfig()) } @@ -78,62 +69,92 @@ object UpdaterModule : Module() { try { HttpClient(CIO) { expectSuccess = false + install(HttpTimeout) { + requestTimeoutMillis = TIMEOUT_STRICT + connectTimeoutMillis = 5000L + socketTimeoutMillis = TIMEOUT_STRICT + } install(UserAgent) { agent = Constants.USER_AGENT } - install(HttpRedirect) {} + install(HttpRedirect) { checkHttpMethod = false } }.use { client -> - val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL - Log.i("Fetching JS bundle from: $url") + val targetUrl = resolveTargetUrl(client) + Log.i("Fetching bundle: $targetUrl") - val response: HttpResponse = client.get(url) { + val response: HttpResponse = client.get(targetUrl) { headers { if (etag.exists() && bundle.exists()) { append(HttpHeaders.IfNoneMatch, etag.readText()) } } - - // Retries don't need timeout - if (activity == null) { - timeout { - requestTimeoutMillis = if (!bundle.exists()) TIMEOUT else TIMEOUT_CACHED - } - } } when (response.status) { HttpStatusCode.OK -> { val bytes: ByteArray = response.body() - AtomicFile(bundle).writeBytes(bytes) + + if (bytes.size < MIN_BYTECODE_SIZE) { + throw Exception("Payload too small (${bytes.size} bytes). Possible corrupt build.") + } - val newTag = response.headers[HttpHeaders.ETag] - if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete() + AtomicFile(bundle).apply { + val stream = startWrite() + try { + stream.write(bytes) + finishWrite(stream) + } catch (e: Exception) { + failWrite(stream) + throw e + } + } - Log.i("Bundle updated (${bytes.size} bytes)") + response.headers[HttpHeaders.ETag]?.let { etag.writeText(it) } ?: etag.delete() + Log.i("Bundle updated: ${bytes.size} bytes") - // This is a retry, so we show a dialog if (activity != null) { withContext(Dispatchers.Main) { - AlertDialog.Builder(activity).setTitle("Kettu Update Successful") - .setMessage("A reload is required for changes to take effect.") - .setPositiveButton("Reload") { dialog, _ -> - reloadApp() - dialog.dismiss() - }.setCancelable(false).show() + AlertDialog.Builder(activity) + .setTitle("Update Successful") + .setMessage("Reload required.") + .setPositiveButton("Reload") { _, _ -> reloadApp() } + .setCancelable(false) + .show() } } } - - HttpStatusCode.NotModified -> { - Log.i("Server responded with 304, no changes") - } - - else -> { - throw ResponseException(response, "Received status: ${response.status}") - } + HttpStatusCode.NotModified -> Log.i("Bundle is up to date (304)") + else -> throw ResponseException(response, "HTTP ${response.status}") } } } catch (e: Throwable) { - Log.e("Failed to download script", e) - showErrorDialog(e) + Log.e("Updater Error", e) + if (activity != null) withContext(Dispatchers.Main) { + Toast.makeText(activity, "Update failed: ${e.message}", Toast.LENGTH_LONG).show() + } + } + } + + private suspend fun resolveTargetUrl(client: HttpClient): String { + if (config.customLoadUrl.enabled && config.customLoadUrl.url.isNotEmpty()) { + return config.customLoadUrl.url + } + + return try { + val infoResponse = client.get("${DEFAULT_BASE_URL}info.json") + if (infoResponse.status == HttpStatusCode.OK) { + val info = JSON.decodeFromString(infoResponse.bodyAsText()) + val hermesVersion = withTimeoutOrNull(2000L) { + runCatching { LibUnbound.getHermesRuntimeBytecodeVersion() }.getOrNull() + } ?: 96 + + val hbcName = "kettu.$hermesVersion.hbc" + when { + info.paths.contains(hbcName) -> DEFAULT_BASE_URL + hbcName + info.paths.contains("kettu.min.js") -> DEFAULT_BASE_URL + "kettu.min.js" + else -> DEFAULT_BASE_URL + DEFAULT_BUNDLE_NAME + } + } else DEFAULT_BASE_URL + DEFAULT_BUNDLE_NAME + } catch (e: Exception) { + DEFAULT_BASE_URL + DEFAULT_BUNDLE_NAME } } @@ -141,62 +162,16 @@ object UpdaterModule : Module() { lastActivity = WeakReference(activity) } - fun showErrorDialog(e: Throwable) { - val activity = lastActivity?.get() ?: return - - activity.runOnUiThread { - } - } - - fun resetLoaderConfig(context: Context) { - val filesDir = File(context.dataDir, Constants.FILES_DIR) - val config = File(filesDir, CONFIG_FILE) - if (config.exists()) config.delete() - } - fun setDisableInjection(context: Context, disabled: Boolean) { - try { - val filesDir = File(context.dataDir, Constants.FILES_DIR) - filesDir.mkdirs() - val configFile = File(filesDir, CONFIG_FILE) - - val current = runCatching { - if (configFile.exists()) JSON.decodeFromString(configFile.readText()) else LoaderConfig() - }.getOrDefault(LoaderConfig()) - - val newCfg = current.copy(disableInjection = disabled) - configFile.writeText(JSON.encodeToString(newCfg)) - Toast.makeText(context, "Bundle injection ${if (disabled) "disabled" else "enabled"}", Toast.LENGTH_SHORT) - .show() - } catch (e: Exception) { - Log.e("Failed to update loader config: ${e.message}", e) - Toast.makeText(context, "Failed to update loader config", Toast.LENGTH_SHORT).show() - } + val filesDir = File(context.dataDir, Constants.FILES_DIR).apply { mkdirs() } + val configFile = File(filesDir, CONFIG_FILE) + val newCfg = config.copy(disableInjection = disabled) + configFile.writeText(JSON.encodeToString(newCfg)) + config = newCfg + Toast.makeText(context, "Injection ${if (disabled) "disabled" else "enabled"}", Toast.LENGTH_SHORT).show() } - /** - * Convenience to check whether injection is disabled (reads current in-memory config first, - * falls back to disk if needed). - */ fun isInjectionDisabled(context: Context? = null): Boolean { - // Prefer in-memory config if loaded - try { - if (::config.isInitialized) { - return config.disableInjection - } - - // If a Context is provided, attempt to read on-disk config - if (context != null) { - val filesDir = File(context.dataDir, Constants.FILES_DIR) - val configFile = File(filesDir, CONFIG_FILE) - if (configFile.exists()) { - val disk = JSON.decodeFromString(configFile.readText()) - return disk.disableInjection - } - } - } catch (e: Exception) { - Log.e("Error checking disableInjection flag: ${e.message}", e) - } - return false + return if (::config.isInitialized) config.disableInjection else false } -} +} \ No newline at end of file From e0c75e30afb6323ac447e5628d3414faf3186a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Purple=5F=CE=9Eye=E2=84=A2?= <168314373+Purple-EyeZ@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:25:16 +0100 Subject: [PATCH 09/19] perf(modules/HookScriptLoaderModule): don't block startup on bundle update if cached --- .../revenge/xposed/modules/HookScriptLoaderModule.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt index 1e0d282..536a6de 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt @@ -83,9 +83,17 @@ object HookScriptLoaderModule : Module() { runBlocking { val ready = async { HookStateHolder.readyDeferred.join() } - val download = async { UpdaterModule.downloadScript().join() } - awaitAll(ready, download) + if (mainScript.exists()) { + Log.i("Main script exists, updating in background...") + UpdaterModule.downloadScript() + + ready.await() + } else { + Log.i("Main script does not exist, downloading before load...") + val download = async { UpdaterModule.downloadScript().join() } + awaitAll(ready, download) + } } val loadSynchronously = args[2] From da337dfd984e0c7c1be77bbd973e003225737180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Purple=5F=CE=9Eye=E2=84=A2?= <168314373+Purple-EyeZ@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:57:01 +0100 Subject: [PATCH 10/19] feat(modules/UpdaterModule): add methods to force bundle update --- .../io/github/revenge/xposed/modules/UpdaterModule.kt | 7 +++++++ .../xposed/modules/bridge/AdditionalBridgeMethodsModule.kt | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt index 44e90eb..8a31e64 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt @@ -13,6 +13,7 @@ import io.github.revenge.xposed.Utils import io.github.revenge.xposed.Utils.Companion.JSON import io.github.revenge.xposed.Utils.Companion.reloadApp import io.github.revenge.xposed.Utils.Log +import io.github.revenge.xposed.modules.bridge.BridgeModule import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* @@ -71,6 +72,12 @@ object UpdaterModule : Module() { JSON.decodeFromString(configFile.readText()) } else LoaderConfig() }.getOrDefault(LoaderConfig()) + + BridgeModule.registerMethod("revenge.updater.clear") { + if (bundle.exists()) bundle.delete() + if (etag.exists()) etag.delete() + null + } } fun downloadScript(activity: Activity? = null): Job = scope.launch { diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt index 1637fb7..b2d9498 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/bridge/AdditionalBridgeMethodsModule.kt @@ -9,6 +9,7 @@ import android.os.Build import android.widget.Toast import io.github.revenge.xposed.Module import io.github.revenge.xposed.Utils +import io.github.revenge.xposed.Utils.Companion.reloadApp import java.io.File object AdditionalBridgeMethodsModule : Module() { @@ -47,6 +48,11 @@ object AdditionalBridgeMethodsModule : Module() { file.writeText(contents as String) } + + BridgeModule.registerMethod("revenge.app.reload") { + reloadApp() + null + } } override fun onActivity(activity: Activity) = with(activity) { From 87d4c78c67a690c369769f64c138186ee87bf579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Purple=5F=CE=9Eye=E2=84=A2?= <168314373+Purple-EyeZ@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:58:59 +0100 Subject: [PATCH 11/19] feat(modules/UpdaterModule): show update dialog when bundle is updated in background --- .../xposed/modules/HookScriptLoaderModule.kt | 5 ++--- .../revenge/xposed/modules/UpdaterModule.kt | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt index 536a6de..4e239f9 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt @@ -86,12 +86,11 @@ object HookScriptLoaderModule : Module() { if (mainScript.exists()) { Log.i("Main script exists, updating in background...") - UpdaterModule.downloadScript() - + UpdaterModule.downloadScript(showUpdateDialog = true) ready.await() } else { Log.i("Main script does not exist, downloading before load...") - val download = async { UpdaterModule.downloadScript().join() } + val download = async { UpdaterModule.downloadScript(showUpdateDialog = false).join() } awaitAll(ready, download) } } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt index 8a31e64..d5d439a 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt @@ -80,7 +80,7 @@ object UpdaterModule : Module() { } } - fun downloadScript(activity: Activity? = null): Job = scope.launch { + fun downloadScript(activity: Activity? = null, showUpdateDialog: Boolean = true): Job = scope.launch { try { HttpClient(CIO) { expectSuccess = false @@ -115,15 +115,22 @@ object UpdaterModule : Module() { Log.i("Bundle updated (${bytes.size} bytes)") - // This is a retry, so we show a dialog - if (activity != null) { - withContext(Dispatchers.Main) { - AlertDialog.Builder(activity).setTitle("Revenge Update Successful") + if (showUpdateDialog) { + val activity = activity ?: lastActivity?.get() + + activity?.runOnUiThread { + AlertDialog.Builder(activity) + .setTitle("Revenge Update Downloaded") .setMessage("A reload is required for changes to take effect.") .setPositiveButton("Reload") { dialog, _ -> reloadApp() dialog.dismiss() - }.setCancelable(false).show() + } + .setNegativeButton("Later") { dialog, _ -> + dialog.dismiss() + } + .setCancelable(false) + .show() } } } From d9603ffdb5fb382016621b177f54a066b22ef776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Purple=5F=CE=9Eye=E2=84=A2?= <168314373+Purple-EyeZ@users.noreply.github.com> Date: Fri, 30 Jan 2026 05:09:47 +0100 Subject: [PATCH 12/19] fix(modules/HookScriptLoaderModule): don't use cache when custom bundle URL is enabled --- .../xposed/modules/HookScriptLoaderModule.kt | 13 ++++++++----- .../github/revenge/xposed/modules/UpdaterModule.kt | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt index 4e239f9..930f2c8 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt @@ -83,15 +83,18 @@ object HookScriptLoaderModule : Module() { runBlocking { val ready = async { HookStateHolder.readyDeferred.join() } + val isCustomUrl = UpdaterModule.isCustomUrlEnabled - if (mainScript.exists()) { + if (!mainScript.exists() || isCustomUrl) { + val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist" + Log.i("$reason, downloading before load...") + val download = + async { UpdaterModule.downloadScript(showUpdateDialog = false).join() } + awaitAll(ready, download) + } else { Log.i("Main script exists, updating in background...") UpdaterModule.downloadScript(showUpdateDialog = true) ready.await() - } else { - Log.i("Main script does not exist, downloading before load...") - val download = async { UpdaterModule.downloadScript(showUpdateDialog = false).join() } - awaitAll(ready, download) } } diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt index d5d439a..498fc8d 100644 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt @@ -43,6 +43,8 @@ data class LoaderConfig( */ object UpdaterModule : Module() { private lateinit var config: LoaderConfig + val isCustomUrlEnabled: Boolean + get() = config.customLoadUrl.enabled private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var lastActivity: WeakReference? = null 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 13/19] 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) - } -} From 82ea78edbb7b080b656323531e2c14ffe74e0622 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:26:27 +0100 Subject: [PATCH 14/19] feat(xposed/modules/LogBox/LogBoxMenu.kt): Reload app after Disabling/Enabling bundle injection --- .../ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index 66713ec..419abff 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxMenu.kt @@ -15,6 +15,7 @@ import android.view.animation.DecelerateInterpolator import org.json.JSONObject import java.io.File import ShiggyXposed.xposed.Utils.Log +import ShiggyXposed.xposed.Utils object LogBoxMenu { @@ -95,6 +96,7 @@ object LogBoxMenu { container.addView(createM3Button(context, injectionText, colors) { dialog.dismiss() LogBoxActions.toggleBundleInjection(context) + Utils.reloadApp() }) container.addView(createM3Button(context, "Refetch Bundle", colors) { From 1f8ddf4176ff2af457d56b349e6c437483f50a83 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:28:57 +0100 Subject: [PATCH 15/19] chore: bump to 1.3.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b07cc7d..84f2e2b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1250 - versionName = "1.2.5" + versionCode = 1300 + versionName = "1.3.0" } buildTypes { From 2a223671b9d82e6aa29f31a60c7ab1f7c7975354 Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Sat, 31 Jan 2026 21:48:43 +0100 Subject: [PATCH 16/19] fix: not being able to enable bundle injection --- .../xposed/modules/LogBox/LogBoxActions.kt | 55 +++++++++++-------- .../xposed/modules/LogBox/LogBoxUtils.kt | 9 ++- 2 files changed, 39 insertions(+), 25 deletions(-) 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 index 39fd5b4..2558f30 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxActions.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxActions.kt @@ -63,31 +63,38 @@ object LogBoxActions { 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() + val currentlyDisabled = LogBoxUtils.isBundleInjectionDisabled(context) + val newState = !currentlyDisabled + + if (bundle.exists() && disabled.exists()) { + bundle.delete() } + + if (newState) { + // Disable injection + if (disabled.exists()) { + disabled.delete() + } + if (bundle.exists()) { + bundle.renameTo(disabled) + } else { + disabled.writeText("") + } + settings.put("bundleInjectionDisabled", true) + Toast.makeText(context, "Bundle injection disabled", Toast.LENGTH_SHORT).show() + } else { + // Enable injection + if (disabled.exists()) { + disabled.renameTo(bundle) + } else if (!bundle.exists()) { + bundle.writeText("") + } + settings.put("bundleInjectionDisabled", false) + Toast.makeText(context, "Bundle injection enabled", Toast.LENGTH_SHORT).show() + } + + settingsFile.writeText(settings.toString()) + } catch (e: Exception) { Log.e("Error toggling bundle injection: ${e.message}") showError(context, "Failed to toggle bundle injection", e.message) 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 index 495e66f..fc6f52f 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxUtils.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBox/LogBoxUtils.kt @@ -86,9 +86,16 @@ object LogBoxUtils { 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 (disabled.exists()) { + if (bundle.exists()) { + bundle.delete() + } + return true + } if (bundle.exists()) return false + // If neither file exists, check settings val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") if (!settingsFile.exists()) return false val json = JSONObject(settingsFile.readText()) From 8ddd3a336e7d35cda0f41d33dbff425e9807898b Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Sat, 31 Jan 2026 21:49:01 +0100 Subject: [PATCH 17/19] chore: bump to 1.3.1 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84f2e2b..946c6c8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1300 - versionName = "1.3.0" + versionCode = 1310 + versionName = "1.3.1" } buildTypes { From c6667c2307a250c3c725e6c745666eb3353d33f0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 1 Feb 2026 11:11:17 +0000 Subject: [PATCH 18/19] chore: bump version to 1.4.10 (1410) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a9d327d..ee103e0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "io.github.revenge.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1409 - versionName = "1.4.9" + versionCode = 1410 + versionName = "1.4.10" } buildTypes { From a9bac3f1cb91ed62e8674e33fc4fda965e75d3d4 Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Sat, 14 Feb 2026 16:44:29 +0100 Subject: [PATCH 19/19] chore: bump to 1.3.2 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 946c6c8..596a892 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1310 - versionName = "1.3.1" + versionCode = 1320 + versionName = "1.3.2" } buildTypes {