From 5876b7da100a1467db75c32888cf8b6bf9b07136 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:30:57 +0100 Subject: [PATCH 1/5] Add options menu in LogBox module and bump to 1.2.3 Add options menu in xposed settings to disabling injecting bundle, change Xposed menu flavor, and choose theme from dark to light mode and system detection --- app/build.gradle.kts | 4 +- .../xposed/xposed/modules/LogBoxModule.kt | 262 +++++++++++++++++- 2 files changed, 261 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2cc3d0..ab5e7cc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "ShiggyXposed.xposed" minSdk = 24 targetSdk = 36 - versionCode = 1220 - versionName = "1.2.2" + versionCode = 1230 + versionName = "1.2.3" } buildTypes { diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt index be94ff0..80023fb 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt @@ -230,6 +230,21 @@ object LogBoxModule : Module() { } private fun isDarkMode(context: Context): Boolean { + // Read LogBox-specific appearance override first (logbox/LOGBOX_SETTINGS). + // Supported values for appearanceMode: "system" | "light" | "dark" + 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 mode == "system" or empty, fall back to system below + } + } 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 @@ -237,7 +252,9 @@ object LogBoxModule : Module() { private fun getM3Colors(context: Context): M3Colors { val isDark = isDarkMode(context) - return if (isDark) { + + // Base palettes + val base = if (isDark) { M3Colors( surface = Color.parseColor("#1C1B1F"), surfaceVariant = Color.parseColor("#49454F"), @@ -268,6 +285,45 @@ object LogBoxModule : Module() { onError = Color.parseColor("#FFFFFF") ) } + + // 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()) + // Support multiple keys that might be written by different UIs/tools: + val flavor = json.optString("menuFlavor", json.optString("flavor", json.optString("id", ""))).lowercase() + 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( @@ -393,6 +449,12 @@ object LogBoxModule : Module() { handleMenuSelection(context, 2) }) + // Options Button (opens submenu for theme, injection, flavor) + container.addView(createM3Button(context, "Options", colors) { + dialog.dismiss() + handleMenuSelection(context, 5) + }) + // Reload App Button container.addView(createM3Button(context, "Reload App", colors) { dialog.dismiss() @@ -435,6 +497,9 @@ object LogBoxModule : Module() { context, "Clear Cache & Reset", "This will clear all cached bundles and reset to default settings." ) { clearCacheAndReset(context) } + + // Options submenu + 5 -> showOptionsMenu(context) } } @@ -526,6 +591,197 @@ object LogBoxModule : Module() { dialog.show() } + // Options Submenu: Theme mode, disable bundle injection, flavor selection + private fun showOptionsMenu(context: Context) { + 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) + } + + 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)) + } + container.addView(titleView) + + // Theme Mode selection (affects LogBox menu only) + container.addView(createM3Button(context, "Follow System Theme", colors) { + dialog.dismiss() + setAppearanceMode(context, "system") + }) + container.addView(createM3Button(context, "Force Light Mode", colors) { + dialog.dismiss() + setAppearanceMode(context, "light") + }) + container.addView(createM3Button(context, "Force Dark Mode", colors) { + dialog.dismiss() + setAppearanceMode(context, "dark") + }) + + // 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) + }) + + // Flavor selection + container.addView(createM3Button(context, "Menu Color Flavor", colors) { + dialog.dismiss() + showFlavorSelection(context) + }) + + // Back / Close button + container.addView(createM3Button(context, "Close", colors) { + dialog.dismiss() + }) + + dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + dialog.window?.setBackgroundDrawable( + createM3Background(context, Color.TRANSPARENT, 28f) + ) + + dialog.show() + } + + 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()) + showM3Toast(context, "Appearance set: ${mode.replaceFirstChar { it.uppercase() }}") + reloadApp() + } catch (e: Exception) { + Log.e("Error setting appearance mode: ${e.message}") + showError(context, "Failed to set appearance", e.message) + } + } + + private fun isBundleInjectionDisabled(context: Context): Boolean { + return try { + 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 { + val pyonDir = getPyoncordDirectory(context) + val bundleFile = File(pyonDir, "bundle.js") + val disabledFile = File(pyonDir, "bundle.js.disabled") + + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + settingsFile.parentFile?.mkdirs() + val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject() + + val currentlyDisabled = settings.optBoolean("bundleInjectionDisabled", false) + + if (!currentlyDisabled) { + // disable: rename bundle if exists + if (bundleFile.exists()) { + bundleFile.renameTo(disabledFile) + } + settings.put("bundleInjectionDisabled", true) + settingsFile.writeText(settings.toString()) + showM3Toast(context, "Bundle injection disabled") + } else { + // enable: restore bundle if previously renamed + if (disabledFile.exists()) { + disabledFile.renameTo(bundleFile) + } + settings.put("bundleInjectionDisabled", false) + settingsFile.writeText(settings.toString()) + showM3Toast(context, "Bundle injection enabled") + } + + reloadApp() + } catch (e: Exception) { + Log.e("Error toggling bundle injection: ${e.message}") + showError(context, "Failed to toggle bundle injection", e.message) + } + } + + private fun showFlavorSelection(context: Context) { + val colors = getM3Colors(context) + val flavors = listOf("blue", "green", "mocha", "vanilla", "purple", "amber", "teal") + 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 = "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)) + } + container.addView(titleView) + + flavors.forEach { flavor -> + container.addView(createM3Button(context, flavor.replaceFirstChar { it.uppercase() }, colors) { + saveMenuFlavor(context, flavor) + }) + } + + val dialog = AlertDialog.Builder(context) + .setView(container) + .create() + + dialog.window?.setBackgroundDrawable( + createM3Background(context, Color.TRANSPARENT, 28f) + ) + + 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()) + showM3Toast(context, "Menu flavor set to ${flavor.replaceFirstChar { it.uppercase() }}") + reloadApp() + } catch (e: Exception) { + Log.e("Error saving menu flavor: ${e.message}") + showError(context, "Failed to save flavor", e.message) + } + } + private fun isSafeModeEnabled(context: Context): Boolean { return try { val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") @@ -541,8 +797,8 @@ object LogBoxModule : Module() { private fun toggleSafeMode(context: Context) { try { - val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") - val themeFile = File(context.filesDir, "vd_mmkv/VENDETTA_THEMES") + val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") + val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES") settingsFile.parentFile?.mkdirs() From 035f8aa570c2f78173ad1cbd5ac0302dfb1c853d Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:40:57 +0100 Subject: [PATCH 2/5] Fix: fix disabling and enabling bundle injection --- .../xposed/modules/HookScriptLoaderModule.kt | 30 ++++++++---- .../xposed/xposed/modules/LogBoxModule.kt | 48 ++++++++++++------- .../xposed/xposed/modules/UpdaterModule.kt | 21 ++++++++ 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt index 0a0dd84..4a52ab8 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/HookScriptLoaderModule.kt @@ -100,19 +100,29 @@ object HookScriptLoaderModule : Module() { } try { - preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile) + // If a disabled marker exists next to the cached bundle, treat this as a global + // disable for the entire script-loading step. In that case, we skip preloads, + // cached main script and the assets fallback so nothing is injected. + val disabledMarker = File(mainScript.parentFile, "${Constants.MAIN_SCRIPT_FILE}.disabled") + if (disabledMarker.exists()) { + Log.i("Script loading disabled by marker; skipping preloads, cached bundle and fallback") + } else { + // Normal behaviour: run preloads then cached main script or fallback to bundled asset + preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile) - if (mainScript.exists()) runScriptFile(mainScript) - else { - Log.i("Main script does not exist, falling back") + if (mainScript.exists()) { + runScriptFile(mainScript) + } else { + Log.i("Main script does not exist, falling back") - if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null) + if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null) - XposedBridge.invokeOriginalMethod( - loadScriptFromAssets, - thisObject, - arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously) - ) + XposedBridge.invokeOriginalMethod( + loadScriptFromAssets, + thisObject, + arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously) + ) + } } } catch (e: Throwable) { Log.e("Unable to run scripts:", e) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt index 80023fb..c65912e 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt @@ -24,6 +24,7 @@ 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.Constants import ShiggyXposed.xposed.Utils.Companion.reloadApp import ShiggyXposed.xposed.Utils.Log import kotlinx.coroutines.* @@ -677,6 +678,15 @@ object LogBoxModule : Module() { private fun isBundleInjectionDisabled(context: Context): Boolean { return try { + // Prefer checking the cache location used by HookScriptLoader. + // If a disabled marker exists next to the bundle in the cache dir, treat injection as disabled. + 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()) @@ -689,32 +699,36 @@ object LogBoxModule : Module() { private fun toggleBundleInjection(context: Context) { try { - val pyonDir = getPyoncordDirectory(context) - val bundleFile = File(pyonDir, "bundle.js") - val disabledFile = File(pyonDir, "bundle.js.disabled") + // 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() - val currentlyDisabled = settings.optBoolean("bundleInjectionDisabled", false) - - if (!currentlyDisabled) { - // disable: rename bundle if exists - if (bundleFile.exists()) { - bundleFile.renameTo(disabledFile) - } + 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, "Bundle injection disabled") - } else { - // enable: restore bundle if previously renamed - if (disabledFile.exists()) { - disabledFile.renameTo(bundleFile) - } + 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, "Bundle injection enabled") + 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)") } reloadApp() diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt index caf9157..9eea8f2 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/UpdaterModule.kt @@ -48,6 +48,8 @@ object UpdaterModule : Module() { private lateinit var cacheDir: File private lateinit var bundle: File private lateinit var etag: File + // Path to the application's data directory so we can read LogBox settings + private lateinit var appDataDir: File private const val TIMEOUT_CACHED = 5000L private const val TIMEOUT = 10000L @@ -58,6 +60,8 @@ object UpdaterModule : Module() { "https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/shiggycord.js" override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { + // store app data dir for later checks (LogBox settings live under files/logbox) + appDataDir = File(appInfo.dataDir) cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() } val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() } @@ -75,6 +79,23 @@ object UpdaterModule : Module() { fun downloadScript(activity: Activity? = null): Job = scope.launch { try { + // Respect LogBox setting to disable bundle injections. + // If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true, + // skip attempting to download/update the bundle. + try { + val settingsFile = File(appDataDir, "files/logbox/LOGBOX_SETTINGS") + if (settingsFile.exists()) { + val txt = settingsFile.readText() + // look for a simple true marker; this avoids introducing a new JSON dependency here + if (txt.contains("\"bundleInjectionDisabled\"") && txt.contains("true")) { + Log.i("UpdaterModule: bundle injection disabled by LogBox settings — skipping download") + return@launch + } + } + } catch (ignore: Exception) { + // If anything goes wrong reading the setting, proceed with normal behaviour. + } + HttpClient(CIO) { expectSuccess = false install(UserAgent) { agent = Constants.USER_AGENT } From 2b1a7919a307dc1ed1c60e4e01911098834b1298 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:33:31 +0100 Subject: [PATCH 3/5] feat: Add Themes submenu and segmented appearance selector Introduce Themes submenu with segmented appearance selector and flavor picker for LogBox menu. Move Refetch and Clear Cache from Recovery into Options. Apply menu colors in-place via currentMenuContainer --- .../xposed/xposed/modules/LogBoxModule.kt | 428 +++++++++++++----- 1 file changed, 322 insertions(+), 106 deletions(-) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt index c65912e..8891238 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt @@ -24,20 +24,38 @@ 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.Constants 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 @@ -231,8 +249,7 @@ object LogBoxModule : Module() { } private fun isDarkMode(context: Context): Boolean { - // Read LogBox-specific appearance override first (logbox/LOGBOX_SETTINGS). - // Supported values for appearanceMode: "system" | "light" | "dark" + // Check logbox-specific appearance override first try { val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") if (settingsFile.exists()) { @@ -240,10 +257,10 @@ object LogBoxModule : Module() { val mode = json.optString("appearanceMode", "") if (mode == "light") return false if (mode == "dark") return true - // if mode == "system" or empty, fall back to system below + // if "system" or empty, fall through to system detection } } catch (e: Exception) { - Log.e("Error reading logbox appearance settings: ${e.message}") + Log.e("Error reading LogBox appearance settings: ${e.message}") } return (context.resources.configuration.uiMode and @@ -251,9 +268,20 @@ object LogBoxModule : Module() { 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( @@ -292,7 +320,6 @@ object LogBoxModule : Module() { val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES") if (themeFile.exists()) { val json = JSONObject(themeFile.readText()) - // Support multiple keys that might be written by different UIs/tools: val flavor = json.optString("menuFlavor", json.optString("flavor", json.optString("id", ""))).lowercase() if (flavor.isNotEmpty()) { val flavorMap = mapOf( @@ -321,7 +348,7 @@ object LogBoxModule : Module() { } } } catch (e: Exception) { - Log.e("Error applying logbox flavor: ${e.message}") + Log.e("Error applying LogBox flavor: ${e.message}") } return base @@ -438,12 +465,6 @@ object LogBoxModule : Module() { handleMenuSelection(context, 0) }) - // Refetch Bundle Button - container.addView(createM3Button(context, "Refetch Bundle", colors) { - dialog.dismiss() - handleMenuSelection(context, 1) - }) - // Load Custom Bundle Button container.addView(createM3Button(context, "Load Custom Bundle", colors) { dialog.dismiss() @@ -462,11 +483,7 @@ object LogBoxModule : Module() { handleMenuSelection(context, 3) }) - // Clear Cache & Reset Button - container.addView(createM3Button(context, "Clear Cache & Reset", colors) { - dialog.dismiss() - handleMenuSelection(context, 4) - }) + dialog = AlertDialog.Builder(context) .setView(container) @@ -476,6 +493,7 @@ object LogBoxModule : Module() { createM3Background(context, Color.TRANSPARENT, 28f) ) + currentMenuContainer = container dialog.show() Log.e("Recovery menu shown successfully") } catch (e: Exception) { @@ -589,10 +607,12 @@ object LogBoxModule : Module() { createM3Background(context, Color.TRANSPARENT, 28f) ) + currentMenuContainer = container + dialog.show() } - // Options Submenu: Theme mode, disable bundle injection, flavor selection + // Options menu -> entry point for Themes submenu & other options private fun showOptionsMenu(context: Context) { val colors = getM3Colors(context) lateinit var dialog: AlertDialog @@ -600,12 +620,12 @@ object LogBoxModule : Module() { val container = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL setPadding( - dpToPx(context, 24), - dpToPx(context, 24), - dpToPx(context, 24), - dpToPx(context, 24) + dpToPx(context, 20), + dpToPx(context, 20), + dpToPx(context, 20), + dpToPx(context, 20) ) - background = createM3Background(context, colors.surface, 28f) + background = createM3Background(context, colors.surface, 24f) } val titleView = TextView(context).apply { @@ -617,18 +637,10 @@ object LogBoxModule : Module() { } container.addView(titleView) - // Theme Mode selection (affects LogBox menu only) - container.addView(createM3Button(context, "Follow System Theme", colors) { + // Themes button -> opens Themes submenu + container.addView(createM3Button(context, "Themes", colors) { dialog.dismiss() - setAppearanceMode(context, "system") - }) - container.addView(createM3Button(context, "Force Light Mode", colors) { - dialog.dismiss() - setAppearanceMode(context, "light") - }) - container.addView(createM3Button(context, "Force Dark Mode", colors) { - dialog.dismiss() - setAppearanceMode(context, "dark") + showThemesMenu(context) }) // Disable/Enable bundle injection @@ -639,15 +651,23 @@ object LogBoxModule : Module() { toggleBundleInjection(context) }) - // Flavor selection - container.addView(createM3Button(context, "Menu Color Flavor", colors) { + container.addView(createM3Button(context, "Refetch Bundle", colors) { dialog.dismiss() - showFlavorSelection(context) + showConfirmAction(context, "Refetch Bundle", "This will download the latest bundle from Github.") { + refetchBundle(context) + } }) - // Back / Close button - container.addView(createM3Button(context, "Close", colors) { + 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) @@ -658,9 +678,138 @@ object LogBoxModule : Module() { createM3Background(context, Color.TRANSPARENT, 28f) ) + 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)) + } + 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() + } + + // 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) + } + } + } + row.addView(seg) + } + + wrapper.addView(row) + return wrapper + } + private fun setAppearanceMode(context: Context, mode: String) { try { val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS") @@ -669,17 +818,80 @@ object LogBoxModule : Module() { settings.put("appearanceMode", mode) settingsFile.writeText(settings.toString()) showM3Toast(context, "Appearance set: ${mode.replaceFirstChar { it.uppercase() }}") - reloadApp() + applyMenuColors(context) } catch (e: Exception) { - Log.e("Error setting appearance mode: ${e.message}") 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)) + } + 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) + ) + + 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()) + showM3Toast(context, "Menu flavor set to ${flavor.replaceFirstChar { it.uppercase() }}") + 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. - // If a disabled marker exists next to the bundle in the cache dir, treat injection as disabled. 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") @@ -730,72 +942,13 @@ object LogBoxModule : Module() { settingsFile.writeText(settings.toString()) showM3Toast(context, "Bundle injection disabled (marker created)") } - - reloadApp() + applyMenuColors(context) } catch (e: Exception) { Log.e("Error toggling bundle injection: ${e.message}") showError(context, "Failed to toggle bundle injection", e.message) } } - private fun showFlavorSelection(context: Context) { - val colors = getM3Colors(context) - val flavors = listOf("blue", "green", "mocha", "vanilla", "purple", "amber", "teal") - 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 = "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)) - } - container.addView(titleView) - - flavors.forEach { flavor -> - container.addView(createM3Button(context, flavor.replaceFirstChar { it.uppercase() }, colors) { - saveMenuFlavor(context, flavor) - }) - } - - val dialog = AlertDialog.Builder(context) - .setView(container) - .create() - - dialog.window?.setBackgroundDrawable( - createM3Background(context, Color.TRANSPARENT, 28f) - ) - - 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()) - showM3Toast(context, "Menu flavor set to ${flavor.replaceFirstChar { it.uppercase() }}") - reloadApp() - } catch (e: Exception) { - Log.e("Error saving menu flavor: ${e.message}") - showError(context, "Failed to save flavor", e.message) - } - } - private fun isSafeModeEnabled(context: Context): Boolean { return try { val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") @@ -1064,10 +1217,12 @@ object LogBoxModule : Module() { setCustomBundleURL(context, url.ifEmpty { "http://localhost:4040/shiggycord.js" }, false) dialog.dismiss() } else { - Toast.makeText(context, "Please enter a URL", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show() } } catch (e: Exception) { - Toast.makeText(context, "Failed to set custom bundle: ${e.message}", Toast.LENGTH_LONG).show() + // Build the error message separately to avoid problematic nested/escaped quotes in the string literal + val errMsg = e.message ?: "unknown error" + Toast.makeText(context, "Failed to set custom bundle: $errMsg", Toast.LENGTH_LONG).show() } } @@ -1079,6 +1234,7 @@ object LogBoxModule : Module() { createM3Background(context, Color.TRANSPARENT, 28f) ) + currentMenuContainer = container dialog.show() } @@ -1262,6 +1418,63 @@ object LogBoxModule : Module() { 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. + */ + private fun applyMenuColors(context: Context) { + try { + val container = currentMenuContainer ?: return + val colors = getM3Colors(context) + + // update container background + container.background = createM3Background(context, colors.surface, 28f) + + // update title/subtitle/labels and buttons inside container + for (i in 0 until container.childCount) { + val child = container.getChildAt(i) + if (child is TextView) { + // titles and labels use onSurface/onSurfaceVariant where appropriate + child.setTextColor(colors.onSurface) + } else if (child is LinearLayout) { + // update button rows and nested views + for (j in 0 until child.childCount) { + val inner = child.getChildAt(j) + if (inner is TextView) { + // update label colors to remain readable + inner.setTextColor(colors.onSurface) + } else if (inner is ViewGroup) { + applyMenuColorsToGroup(inner, colors, context) + } + } + } + } + } 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) @@ -1321,6 +1534,9 @@ object LogBoxModule : Module() { createM3Background(context, Color.TRANSPARENT, 28f) ) + // Expose this container so applyMenuColors can update error dialog visuals immediately + currentMenuContainer = container + dialog.show() } } From 8bc7c1671ddef87e833e8a26f668654bbaa04674 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:03:04 +0100 Subject: [PATCH 4/5] Fix width in recovery menu and remove some buttons --- .../xposed/xposed/modules/LogBoxModule.kt | 204 +++++++++++++----- 1 file changed, 147 insertions(+), 57 deletions(-) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt index 8891238..74ab96b 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt @@ -315,36 +315,40 @@ object LogBoxModule : Module() { ) } + 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 flavor = json.optString("menuFlavor", json.optString("flavor", json.optString("id", ""))).lowercase() - 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 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 ) - 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) { @@ -471,11 +475,7 @@ object LogBoxModule : Module() { handleMenuSelection(context, 2) }) - // Options Button (opens submenu for theme, injection, flavor) - container.addView(createM3Button(context, "Options", colors) { - dialog.dismiss() - handleMenuSelection(context, 5) - }) + // Reload App Button container.addView(createM3Button(context, "Reload App", colors) { @@ -483,7 +483,17 @@ object LogBoxModule : Module() { 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) @@ -493,8 +503,11 @@ object LogBoxModule : Module() { 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) @@ -607,9 +620,12 @@ object LogBoxModule : Module() { 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 @@ -634,6 +650,7 @@ object LogBoxModule : Module() { 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) @@ -678,6 +695,8 @@ object LogBoxModule : Module() { createM3Background(context, Color.TRANSPARENT, 28f) ) + // Constrain to sane width before showing + constrainContainerWidth(context, container) currentMenuContainer = container dialog.show() } @@ -704,6 +723,7 @@ object LogBoxModule : Module() { 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) @@ -716,10 +736,6 @@ object LogBoxModule : Module() { showFlavorSelection(context) }) - - - - // Back button (return to Options) container.addView(createM3Button(context, "Back", colors) { dialog.dismiss() @@ -736,6 +752,8 @@ object LogBoxModule : Module() { currentMenuContainer = container dialog.show() + setDialogWindowWidth(dialog, context) + setDialogWindowWidth(dialog, context) } // Create a segmented selector (3 options) for appearance mode @@ -801,6 +819,9 @@ object LogBoxModule : Module() { 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) @@ -817,7 +838,6 @@ object LogBoxModule : Module() { val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject() settings.put("appearanceMode", mode) settingsFile.writeText(settings.toString()) - showM3Toast(context, "Appearance set: ${mode.replaceFirstChar { it.uppercase() }}") applyMenuColors(context) } catch (e: Exception) { showError(context, "Failed to set appearance", e.message) @@ -846,6 +866,7 @@ object LogBoxModule : Module() { 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) @@ -867,6 +888,8 @@ object LogBoxModule : Module() { createM3Background(context, Color.TRANSPARENT, 28f) ) + // Constrain to sane width before showing + constrainContainerWidth(context, container) currentMenuContainer = container dialog.show() } @@ -881,7 +904,6 @@ object LogBoxModule : Module() { } themeFile.writeText(themeJson.toString()) - showM3Toast(context, "Menu flavor set to ${flavor.replaceFirstChar { it.uppercase() }}") applyMenuColors(context) } catch (e: Exception) { Log.e("Error saving menu flavor: ${e.message}") @@ -1220,9 +1242,8 @@ object LogBoxModule : Module() { Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show() } } catch (e: Exception) { - // Build the error message separately to avoid problematic nested/escaped quotes in the string literal - val errMsg = e.message ?: "unknown error" - Toast.makeText(context, "Failed to set custom bundle: $errMsg", Toast.LENGTH_LONG).show() + val err = e.message ?: "unknown error" + Toast.makeText(context, "Failed to set custom bundle: $err", Toast.LENGTH_LONG).show() } } @@ -1234,8 +1255,12 @@ object LogBoxModule : Module() { 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 { @@ -1422,34 +1447,97 @@ object LogBoxModule : Module() { * 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(maxW, ViewGroup.LayoutParams.WRAP_CONTENT) + container.layoutParams = lp + } 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 metrics = context.resources.displayMetrics + val maxW = (metrics.widthPixels * 0.9).toInt() + val dp420 = dpToPx(context, 420) + val targetW = if (maxW < dp420) maxW else dp420 + dialog.window?.setLayout(targetW, ViewGroup.LayoutParams.WRAP_CONTENT) + } catch (_: Exception) { + } + } + private fun applyMenuColors(context: Context) { try { val container = currentMenuContainer ?: return val colors = getM3Colors(context) - // update container background - container.background = createM3Background(context, colors.surface, 28f) - - // update title/subtitle/labels and buttons inside container - for (i in 0 until container.childCount) { - val child = container.getChildAt(i) - if (child is TextView) { - // titles and labels use onSurface/onSurfaceVariant where appropriate - child.setTextColor(colors.onSurface) - } else if (child is LinearLayout) { - // update button rows and nested views - for (j in 0 until child.childCount) { - val inner = child.getChildAt(j) - if (inner is TextView) { - // update label colors to remain readable - inner.setTextColor(colors.onSurface) - } else if (inner is ViewGroup) { - applyMenuColorsToGroup(inner, colors, 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}") } @@ -1535,8 +1623,10 @@ object LogBoxModule : Module() { ) // Expose this container so applyMenuColors can update error dialog visuals immediately + constrainContainerWidth(context, container) currentMenuContainer = container dialog.show() + setDialogWindowWidth(dialog, context) } } From 8a134d7ee1db1e2c710a8f24978726f48c3d82e2 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Thu, 11 Dec 2025 18:18:05 +0100 Subject: [PATCH 5/5] Try to fix size of the Recovery menu first page --- .../xposed/xposed/modules/LogBoxModule.kt | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt index 74ab96b..48222de 100644 --- a/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt +++ b/app/src/main/kotlin/ShiggyXposed/xposed/xposed/modules/LogBoxModule.kt @@ -1454,8 +1454,11 @@ object LogBoxModule : Module() { // 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(maxW, ViewGroup.LayoutParams.WRAP_CONTENT) + val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) container.layoutParams = lp + try { + container.minimumWidth = maxW + } catch (_: Exception) {} } catch (_: Exception) { } } @@ -1466,11 +1469,29 @@ object LogBoxModule : Module() { */ private fun setDialogWindowWidth(dialog: AlertDialog, context: Context) { try { - val metrics = context.resources.displayMetrics - val maxW = (metrics.widthPixels * 0.9).toInt() - val dp420 = dpToPx(context, 420) - val targetW = if (maxW < dp420) maxW else dp420 + 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) { } }