Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
5e2fd369fe
16 changed files with 2198 additions and 9 deletions
|
|
@ -12,8 +12,8 @@ android {
|
||||||
applicationId = "ShiggyXposed.xposed"
|
applicationId = "ShiggyXposed.xposed"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1250
|
versionCode = 1320
|
||||||
versionName = "1.2.5"
|
versionName = "1.3.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
@ -52,4 +52,5 @@ dependencies {
|
||||||
implementation("io.ktor:ktor-client-core:3.3.1")
|
implementation("io.ktor:ktor-client-core:3.3.1")
|
||||||
implementation("io.ktor:ktor-client-cio:3.3.1")
|
implementation("io.ktor:ktor-client-cio:3.3.1")
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
implementation("dev.rushii:libunbound:1.0.0")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,6 @@ class Utils {
|
||||||
|
|
||||||
reloadApp()
|
reloadApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
2 -> {
|
|
||||||
UpdaterModule.resetLoaderConfig(context)
|
|
||||||
reloadApp()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,19 @@ object HookScriptLoaderModule : Module() {
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val ready = async { HookStateHolder.readyDeferred.join() }
|
val ready = async { HookStateHolder.readyDeferred.join() }
|
||||||
val download = async { UpdaterModule.downloadScript().join() }
|
val isCustomUrl = UpdaterModule.isCustomUrlEnabled
|
||||||
|
|
||||||
awaitAll(ready, download)
|
if (!mainScript.exists() || isCustomUrl) {
|
||||||
|
val reason = if (isCustomUrl) "Custom URL enabled" else "Main script does not exist"
|
||||||
|
Log.i("$reason, downloading before load...")
|
||||||
|
val download =
|
||||||
|
async { UpdaterModule.downloadScript(showUpdateDialog = false).join() }
|
||||||
|
awaitAll(ready, download)
|
||||||
|
} else {
|
||||||
|
Log.i("Main script exists, updating in background...")
|
||||||
|
UpdaterModule.downloadScript(showUpdateDialog = true)
|
||||||
|
ready.await()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadSynchronously = args[2]
|
val loadSynchronously = args[2]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
package ShiggyXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
|
import ShiggyXposed.xposed.Utils.Companion.reloadApp
|
||||||
|
import ShiggyXposed.xposed.Constants
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
object LogBoxActions {
|
||||||
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
|
||||||
|
fun toggleSafeMode(context: Context) {
|
||||||
|
try {
|
||||||
|
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
|
||||||
|
val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES")
|
||||||
|
|
||||||
|
settingsFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
val settings = if (settingsFile.exists()) {
|
||||||
|
JSONObject(settingsFile.readText())
|
||||||
|
} else {
|
||||||
|
JSONObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
val safeMode = settings.optJSONObject("safeMode") ?: JSONObject()
|
||||||
|
val currentState = safeMode.optBoolean("enabled", false)
|
||||||
|
val newState = !currentState
|
||||||
|
|
||||||
|
safeMode.put("enabled", newState)
|
||||||
|
|
||||||
|
if (newState && themeFile.exists()) {
|
||||||
|
val theme = JSONObject(themeFile.readText())
|
||||||
|
val themeId = theme.optString("id")
|
||||||
|
if (themeId.isNotEmpty()) {
|
||||||
|
safeMode.put("currentThemeId", themeId)
|
||||||
|
themeFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.put("safeMode", safeMode)
|
||||||
|
settingsFile.writeText(settings.toString())
|
||||||
|
|
||||||
|
Toast.makeText(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}", Toast.LENGTH_SHORT).show()
|
||||||
|
reloadApp()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error toggling safe mode: ${e.message}")
|
||||||
|
showError(context, "Failed to toggle safe mode", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleBundleInjection(context: Context) {
|
||||||
|
try {
|
||||||
|
val cacheDir = File(context.dataDir, Constants.CACHE_DIR)
|
||||||
|
cacheDir.mkdirs()
|
||||||
|
val bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE)
|
||||||
|
val disabled = File(cacheDir, "${Constants.MAIN_SCRIPT_FILE}.disabled")
|
||||||
|
|
||||||
|
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
|
||||||
|
settingsFile.parentFile?.mkdirs()
|
||||||
|
val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject()
|
||||||
|
|
||||||
|
val currentlyDisabled = LogBoxUtils.isBundleInjectionDisabled(context)
|
||||||
|
val newState = !currentlyDisabled
|
||||||
|
|
||||||
|
if (bundle.exists() && disabled.exists()) {
|
||||||
|
bundle.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState) {
|
||||||
|
// Disable injection
|
||||||
|
if (disabled.exists()) {
|
||||||
|
disabled.delete()
|
||||||
|
}
|
||||||
|
if (bundle.exists()) {
|
||||||
|
bundle.renameTo(disabled)
|
||||||
|
} else {
|
||||||
|
disabled.writeText("")
|
||||||
|
}
|
||||||
|
settings.put("bundleInjectionDisabled", true)
|
||||||
|
Toast.makeText(context, "Bundle injection disabled", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
// Enable injection
|
||||||
|
if (disabled.exists()) {
|
||||||
|
disabled.renameTo(bundle)
|
||||||
|
} else if (!bundle.exists()) {
|
||||||
|
bundle.writeText("")
|
||||||
|
}
|
||||||
|
settings.put("bundleInjectionDisabled", false)
|
||||||
|
Toast.makeText(context, "Bundle injection enabled", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsFile.writeText(settings.toString())
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error toggling bundle injection: ${e.message}")
|
||||||
|
showError(context, "Failed to toggle bundle injection", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refetchBundle(context: Context) {
|
||||||
|
try {
|
||||||
|
val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context)
|
||||||
|
val bundleFile = File(pyoncordDir, "bundle.js")
|
||||||
|
val backupFile = File(pyoncordDir, "bundle.js.backup")
|
||||||
|
|
||||||
|
if (bundleFile.exists()) {
|
||||||
|
backupFile.delete()
|
||||||
|
bundleFile.renameTo(backupFile)
|
||||||
|
Log.e("Bundle moved to backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadApp()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error refetching bundle: ${e.message}")
|
||||||
|
showError(context, "Failed to refetch bundle", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCacheAndReset(context: Context) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context)
|
||||||
|
val bundleFile = File(pyoncordDir, "bundle.js")
|
||||||
|
val configFile = File(pyoncordDir, "loader.json")
|
||||||
|
|
||||||
|
bundleFile.delete()
|
||||||
|
|
||||||
|
if (configFile.exists()) {
|
||||||
|
val config = JSONObject(configFile.readText())
|
||||||
|
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
|
||||||
|
customLoadUrl.put("enabled", false)
|
||||||
|
customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
|
||||||
|
config.put("customLoadUrl", customLoadUrl)
|
||||||
|
configFile.writeText(config.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(context, "Cache cleared successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
reloadApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error clearing cache: ${e.message}")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(context, "Failed to clear cache", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCustomBundleURL(context: Context, url: String, enabled: Boolean) {
|
||||||
|
try {
|
||||||
|
val pyoncordDir = LogBoxUtils.getPyoncordDirectory(context)
|
||||||
|
val configFile = File(pyoncordDir, "loader.json")
|
||||||
|
|
||||||
|
val config = if (configFile.exists()) {
|
||||||
|
JSONObject(configFile.readText())
|
||||||
|
} else {
|
||||||
|
JSONObject().apply {
|
||||||
|
put("loadReactDevTools", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
|
||||||
|
customLoadUrl.put("enabled", enabled)
|
||||||
|
customLoadUrl.put("url", url)
|
||||||
|
config.put("customLoadUrl", customLoadUrl)
|
||||||
|
|
||||||
|
configFile.writeText(config.toString())
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
File(pyoncordDir, "bundle.js").delete()
|
||||||
|
Toast.makeText(context, "Custom bundle enabled", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadApp()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error setting custom bundle URL: ${e.message}")
|
||||||
|
showError(context, "Failed to save configuration", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showError(context: Context, title: String, message: String?) {
|
||||||
|
LogBoxMenu.showErrorDialog(context, title, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,784 @@
|
||||||
|
package ShiggyXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
|
import android.animation.*
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.text.InputType
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
|
import ShiggyXposed.xposed.Utils
|
||||||
|
|
||||||
|
object LogBoxMenu {
|
||||||
|
|
||||||
|
private var currentMenuContainer: LinearLayout? = null
|
||||||
|
|
||||||
|
fun showRecoveryMenu(context: Context) {
|
||||||
|
Log.e("showRecoveryMenu called with context: $context")
|
||||||
|
try {
|
||||||
|
val colors = LogBoxTheme.getM3Colors(context)
|
||||||
|
lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
val container = createMenuContainer(context, colors)
|
||||||
|
|
||||||
|
// Title
|
||||||
|
container.addView(createTitle(context, colors, "ShiggyXposed Recovery", center = true))
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
container.addView(createSubtitle(context, colors, "Select an option to continue"))
|
||||||
|
|
||||||
|
// Safe Mode Button
|
||||||
|
val safeModeText = if (LogBoxUtils.isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
|
||||||
|
container.addView(createM3Button(context, safeModeText, colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
handleMenuSelection(context, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load Custom Bundle Button
|
||||||
|
container.addView(createM3Button(context, "Load Custom Bundle", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
handleMenuSelection(context, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reload App Button
|
||||||
|
container.addView(createM3Button(context, "Reload App", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
handleMenuSelection(context, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Options Button
|
||||||
|
val optionsBtn = createM3Button(context, "Options", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
handleMenuSelection(context, 5)
|
||||||
|
}
|
||||||
|
(optionsBtn.layoutParams as? LinearLayout.LayoutParams)?.let { lp ->
|
||||||
|
lp.setMargins(lp.leftMargin, LogBoxUtils.dpToPx(context, 16), lp.rightMargin, lp.bottomMargin)
|
||||||
|
optionsBtn.layoutParams = lp
|
||||||
|
}
|
||||||
|
container.addView(optionsBtn)
|
||||||
|
|
||||||
|
dialog = createDialog(context, container)
|
||||||
|
currentMenuContainer = container
|
||||||
|
dialog.show()
|
||||||
|
setDialogWindowWidth(dialog, context)
|
||||||
|
Log.e("Recovery menu shown successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error showing recovery menu: ${e.message}", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showOptionsMenu(context: Context) {
|
||||||
|
val colors = LogBoxTheme.getM3Colors(context)
|
||||||
|
lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
val container = createMenuContainer(context, colors, 20)
|
||||||
|
|
||||||
|
container.addView(createTitle(context, colors, "Options", center = true))
|
||||||
|
|
||||||
|
// Themes button -> opens Themes submenu
|
||||||
|
container.addView(createM3Button(context, "Themes", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showThemesMenu(context)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable/Enable bundle injection
|
||||||
|
val injectionDisabled = LogBoxUtils.isBundleInjectionDisabled(context)
|
||||||
|
val injectionText = if (injectionDisabled) "Enable Bundle Injection" else "Disable Bundle Injection"
|
||||||
|
container.addView(createM3Button(context, injectionText, colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
LogBoxActions.toggleBundleInjection(context)
|
||||||
|
Utils.reloadApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
container.addView(createM3Button(context, "Refetch Bundle", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showConfirmAction(context, "Refetch Bundle", "This will download the latest bundle from Github.") {
|
||||||
|
LogBoxActions.refetchBundle(context)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.addView(createM3Button(context, "Clear Cache & Reset", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showConfirmAction(
|
||||||
|
context,
|
||||||
|
"Clear Cache & Reset",
|
||||||
|
"This will clear all cached bundles and reset to default settings."
|
||||||
|
) {
|
||||||
|
LogBoxActions.clearCacheAndReset(context)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.addView(createM3Button(context, "Back", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showRecoveryMenu(context)
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog = createDialog(context, container)
|
||||||
|
currentMenuContainer = container
|
||||||
|
dialog.show()
|
||||||
|
setDialogWindowWidth(dialog, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showThemesMenu(context: Context) {
|
||||||
|
val colors = LogBoxTheme.getM3Colors(context)
|
||||||
|
lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
val container = createMenuContainer(context, colors, 20)
|
||||||
|
|
||||||
|
container.addView(createTitle(context, colors, "Themes", center = true))
|
||||||
|
|
||||||
|
// Segmented appearance selector
|
||||||
|
container.addView(createAppearanceSelector(context, colors))
|
||||||
|
|
||||||
|
// Flavor selection
|
||||||
|
container.addView(createM3Button(context, "Menu Color Flavor", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showFlavorSelection(context)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Back button (return to Options)
|
||||||
|
container.addView(createM3Button(context, "Back", colors) {
|
||||||
|
dialog.dismiss()
|
||||||
|
showOptionsMenu(context)
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog = createDialog(context, container)
|
||||||
|
currentMenuContainer = container
|
||||||
|
dialog.show()
|
||||||
|
setDialogWindowWidth(dialog, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showFlavorSelection(context: Context) {
|
||||||
|
val colors = LogBoxTheme.getM3Colors(context)
|
||||||
|
lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
val container = createMenuContainer(context, colors, 20)
|
||||||
|
|
||||||
|
container.addView(createTitle(context, colors, "Select Menu Flavor", center = true))
|
||||||
|
|
||||||
|
LogBoxConstants.FLAVORS.forEach { flavor ->
|
||||||
|
container.addView(createM3Button(context, flavor.replaceFirstChar { it.uppercase() }, colors) {
|
||||||
|
try {
|
||||||
|
dialog.dismiss()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
LogBoxUtils.saveMenuFlavor(context, flavor)
|
||||||
|
showThemesMenu(context)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog = createDialog(context, container)
|
||||||
|
currentMenuContainer = container
|
||||||
|
dialog.show()
|
||||||
|
setDialogWindowWidth(dialog, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
|
||||||
|
val colors = LogBoxTheme.getM3Colors(context)
|
||||||
|
|
||||||
|
val container = createMenuContainer(context, colors)
|
||||||
|
|
||||||
|
container.addView(createTitle(context, colors, title))
|
||||||
|
|
||||||
|
val messageView = TextView(context).apply {
|
||||||
|
text = message
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(colors.onSurfaceVariant)
|
||||||
|
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||||
|
lineHeight = LogBoxUtils.dpToPx(context, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.addView(messageView)
|
||||||
|
|
||||||
|
val buttonContainer = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.END
|
||||||
|
}
|
||||||
|
|
||||||
|
val cancelButton = TextView(context).apply {
|
||||||
|
text = "Cancel"
|
||||||
|
setTextColor(colors.primary)
|
||||||
|
textSize = 14f
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10),
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val confirmButton = TextView(context).apply {
|
||||||
|
text = "Confirm"
|
||||||
|
setTextColor(colors.primary)
|
||||||
|
textSize = 14f
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10),
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = createDialog(context, container)
|
||||||
|
|
||||||
|
cancelButton.setOnClickListener { dialog.dismiss() }
|
||||||
|
confirmButton.setOnClickListener {
|
||||||
|
dialog.dismiss()
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonContainer.addView(cancelButton)
|
||||||
|
buttonContainer.addView(confirmButton)
|
||||||
|
container.addView(buttonContainer)
|
||||||
|
|
||||||
|
currentMenuContainer = container
|
||||||
|
dialog.show()
|
||||||
|
setDialogWindowWidth(dialog, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showCustomBundleDialog(context: Context) {
|
||||||
|
val colors = LogBoxTheme.getM3Colors(context)
|
||||||
|
val filesDir = LogBoxUtils.getPyoncordDirectory(context)
|
||||||
|
val configFile = File(filesDir, "loader.json")
|
||||||
|
var currentUrl: String? = null
|
||||||
|
var isEnabled = false
|
||||||
|
|
||||||
|
if (configFile.exists()) {
|
||||||
|
try {
|
||||||
|
val json = JSONObject(configFile.readText())
|
||||||
|
val customLoadUrl = json.optJSONObject("customLoadUrl")
|
||||||
|
if (customLoadUrl != null) {
|
||||||
|
isEnabled = customLoadUrl.optBoolean("enabled", false)
|
||||||
|
currentUrl = customLoadUrl.optString("url", "")
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val container = createMenuContainer(context, colors)
|
||||||
|
|
||||||
|
container.addView(createTitle(context, colors, "Custom Bundle URL"))
|
||||||
|
|
||||||
|
// Toggle switch container
|
||||||
|
val toggleContainer = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 16))
|
||||||
|
}
|
||||||
|
layoutParams = params
|
||||||
|
}
|
||||||
|
|
||||||
|
val toggleLabel = TextView(context).apply {
|
||||||
|
text = "Enable Custom URL"
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(colors.onSurface)
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val toggleSwitch = createM3Switch(context, colors, isEnabled)
|
||||||
|
|
||||||
|
toggleContainer.addView(toggleLabel)
|
||||||
|
toggleContainer.addView(toggleSwitch)
|
||||||
|
container.addView(toggleContainer)
|
||||||
|
|
||||||
|
val urlInput = EditText(context).apply {
|
||||||
|
hint = "http://localhost:4040/shiggycord.js"
|
||||||
|
setTextColor(colors.onSurface)
|
||||||
|
setHintTextColor(colors.onSurfaceVariant)
|
||||||
|
background = LogBoxUtils.createM3Background(context, colors.surfaceVariant, 12f)
|
||||||
|
inputType = InputType.TYPE_TEXT_VARIATION_URI
|
||||||
|
setText(currentUrl ?: "")
|
||||||
|
textSize = 14f
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 12),
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 12)
|
||||||
|
)
|
||||||
|
alpha = if (isEnabled) 1f else 0.5f
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
setMargins(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
|
||||||
|
}
|
||||||
|
layoutParams = params
|
||||||
|
}
|
||||||
|
container.addView(urlInput)
|
||||||
|
|
||||||
|
toggleSwitch.setOnClickListener {
|
||||||
|
val currentState = toggleSwitch.tag as Boolean
|
||||||
|
val newState = !currentState
|
||||||
|
toggleSwitch.tag = newState
|
||||||
|
updateSwitchAppearance(context, toggleSwitch, colors, newState)
|
||||||
|
|
||||||
|
urlInput.animate()
|
||||||
|
.alpha(if (newState) 1f else 0.5f)
|
||||||
|
.setDuration(250)
|
||||||
|
.withEndAction {
|
||||||
|
urlInput.isEnabled = newState
|
||||||
|
}
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
val buttonContainer = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.END
|
||||||
|
}
|
||||||
|
|
||||||
|
val cancelButton = TextView(context).apply {
|
||||||
|
text = "Cancel"
|
||||||
|
setTextColor(colors.primary)
|
||||||
|
textSize = 14f
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10),
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveButton = TextView(context).apply {
|
||||||
|
text = "Save"
|
||||||
|
setTextColor(colors.primary)
|
||||||
|
textSize = 14f
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10),
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = createDialog(context, container)
|
||||||
|
|
||||||
|
cancelButton.setOnClickListener { dialog.dismiss() }
|
||||||
|
saveButton.setOnClickListener {
|
||||||
|
try {
|
||||||
|
val url = urlInput.text?.toString()?.trim() ?: ""
|
||||||
|
val enabled = toggleSwitch.tag as Boolean
|
||||||
|
|
||||||
|
if (enabled && url.isNotEmpty()) {
|
||||||
|
LogBoxActions.setCustomBundleURL(context, url, true)
|
||||||
|
dialog.dismiss()
|
||||||
|
} else if (!enabled) {
|
||||||
|
LogBoxActions.setCustomBundleURL(
|
||||||
|
context,
|
||||||
|
url.ifEmpty { "http://localhost:4040/shiggycord.js" },
|
||||||
|
false
|
||||||
|
)
|
||||||
|
dialog.dismiss()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val err = e.message ?: "unknown error"
|
||||||
|
Toast.makeText(context, "Failed to set custom bundle: $err", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonContainer.addView(cancelButton)
|
||||||
|
buttonContainer.addView(saveButton)
|
||||||
|
container.addView(buttonContainer)
|
||||||
|
|
||||||
|
currentMenuContainer = container
|
||||||
|
dialog.show()
|
||||||
|
setDialogWindowWidth(dialog, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showErrorDialog(context: Context, title: String, message: String?) {
|
||||||
|
val colors = LogBoxTheme.getM3Colors(context)
|
||||||
|
|
||||||
|
val container = createMenuContainer(context, colors)
|
||||||
|
|
||||||
|
val titleView = TextView(context).apply {
|
||||||
|
text = title
|
||||||
|
textSize = 20f
|
||||||
|
setTextColor(colors.error)
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
|
||||||
|
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 16))
|
||||||
|
}
|
||||||
|
container.addView(titleView)
|
||||||
|
|
||||||
|
val messageView = TextView(context).apply {
|
||||||
|
text = message ?: "An unknown error occurred"
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(colors.onSurfaceVariant)
|
||||||
|
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||||
|
lineHeight = LogBoxUtils.dpToPx(context, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container.addView(messageView)
|
||||||
|
|
||||||
|
val okButton = TextView(context).apply {
|
||||||
|
text = "OK"
|
||||||
|
setTextColor(colors.primary)
|
||||||
|
textSize = 14f
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
gravity = Gravity.END
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10),
|
||||||
|
LogBoxUtils.dpToPx(context, 16),
|
||||||
|
LogBoxUtils.dpToPx(context, 10)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = createDialog(context, container)
|
||||||
|
|
||||||
|
okButton.setOnClickListener { dialog.dismiss() }
|
||||||
|
container.addView(okButton)
|
||||||
|
|
||||||
|
currentMenuContainer = container
|
||||||
|
dialog.show()
|
||||||
|
setDialogWindowWidth(dialog, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
private fun createMenuContainer(context: Context, colors: M3Colors, padding: Int = 24): LinearLayout {
|
||||||
|
return LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, padding),
|
||||||
|
LogBoxUtils.dpToPx(context, padding),
|
||||||
|
LogBoxUtils.dpToPx(context, padding),
|
||||||
|
LogBoxUtils.dpToPx(context, padding)
|
||||||
|
)
|
||||||
|
background = LogBoxUtils.createM3Background(context, colors.surface, if (padding == 24) 28f else 24f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTitle(context: Context, colors: M3Colors, text: String, center: Boolean = false): TextView {
|
||||||
|
return TextView(context).apply {
|
||||||
|
this.text = text
|
||||||
|
textSize = if (center) 20f else 22f
|
||||||
|
setTextColor(colors.onSurface)
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
|
||||||
|
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, if (center) 12 else 16))
|
||||||
|
gravity = if (center) Gravity.CENTER else Gravity.START
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSubtitle(context: Context, colors: M3Colors, text: String): TextView {
|
||||||
|
return TextView(context).apply {
|
||||||
|
this.text = text
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(colors.onSurfaceVariant)
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL)
|
||||||
|
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 24))
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createM3Button(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View {
|
||||||
|
return LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
background = LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f)
|
||||||
|
setPadding(
|
||||||
|
LogBoxUtils.dpToPx(context, 24),
|
||||||
|
LogBoxUtils.dpToPx(context, 10),
|
||||||
|
LogBoxUtils.dpToPx(context, 24),
|
||||||
|
LogBoxUtils.dpToPx(context, 10)
|
||||||
|
)
|
||||||
|
clipToOutline = true
|
||||||
|
isClickable = true
|
||||||
|
isFocusable = true
|
||||||
|
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
LogBoxUtils.dpToPx(context, 40)
|
||||||
|
).apply {
|
||||||
|
setMargins(0, LogBoxUtils.dpToPx(context, 8), 0, 0)
|
||||||
|
}
|
||||||
|
layoutParams = params
|
||||||
|
|
||||||
|
addView(TextView(context).apply {
|
||||||
|
this.text = text
|
||||||
|
setTextColor(colors.onPrimaryContainer)
|
||||||
|
textSize = 14f
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
})
|
||||||
|
|
||||||
|
val rippleDrawable = android.graphics.drawable.RippleDrawable(
|
||||||
|
ColorStateList.valueOf(Color.argb(30, 255, 255, 255)),
|
||||||
|
LogBoxUtils.createM3Background(context, colors.primaryContainer, 20f),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
background = rippleDrawable
|
||||||
|
|
||||||
|
setOnClickListener { onClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAppearanceSelector(context: Context, colors: M3Colors): View {
|
||||||
|
val currentMode = LogBoxUtils.getAppearanceMode(context)
|
||||||
|
|
||||||
|
val options = listOf("System", "Light", "Dark")
|
||||||
|
val keys = listOf("system", "light", "dark")
|
||||||
|
|
||||||
|
val wrapper = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
layoutParams = params
|
||||||
|
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = TextView(context).apply {
|
||||||
|
text = "Appearance"
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(colors.onSurface)
|
||||||
|
setPadding(0, 0, 0, LogBoxUtils.dpToPx(context, 8))
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
}
|
||||||
|
wrapper.addView(label)
|
||||||
|
|
||||||
|
val row = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
val params = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
LogBoxUtils.dpToPx(context, 40)
|
||||||
|
)
|
||||||
|
layoutParams = params
|
||||||
|
}
|
||||||
|
|
||||||
|
options.forEachIndexed { idx, title ->
|
||||||
|
val key = keys[idx]
|
||||||
|
val seg = TextView(context).apply {
|
||||||
|
text = title
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
textSize = 14f
|
||||||
|
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
|
||||||
|
val segParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||||
|
if (idx > 0) segParams.setMargins(LogBoxUtils.dpToPx(context, 8), 0, 0, 0)
|
||||||
|
layoutParams = segParams
|
||||||
|
|
||||||
|
val isSelected = currentMode == key
|
||||||
|
background = LogBoxUtils.createM3Background(
|
||||||
|
context,
|
||||||
|
if (isSelected) colors.primaryContainer else colors.surfaceVariant,
|
||||||
|
12f
|
||||||
|
)
|
||||||
|
setTextColor(if (isSelected) colors.onPrimaryContainer else colors.onSurface)
|
||||||
|
setPadding(0, 0, 0, 0)
|
||||||
|
setOnClickListener {
|
||||||
|
LogBoxUtils.setAppearanceMode(context, key)
|
||||||
|
|
||||||
|
for (i in 0 until row.childCount) {
|
||||||
|
val child = row.getChildAt(i) as TextView
|
||||||
|
val sel = keys[i] == key
|
||||||
|
child.background = LogBoxUtils.createM3Background(
|
||||||
|
context,
|
||||||
|
if (sel) colors.primaryContainer else colors.surfaceVariant,
|
||||||
|
12f
|
||||||
|
)
|
||||||
|
child.setTextColor(if (sel) colors.onPrimaryContainer else colors.onSurface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.addView(seg)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.addView(row)
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createM3Switch(context: Context, colors: M3Colors, isChecked: Boolean): View {
|
||||||
|
val switchView = LinearLayout(context).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
|
tag = isChecked
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LogBoxUtils.dpToPx(context, 52),
|
||||||
|
LogBoxUtils.dpToPx(context, 32)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSwitchAppearance(context, switchView, colors, isChecked)
|
||||||
|
return switchView
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSwitchAppearance(context: Context, switchView: View, colors: M3Colors, isChecked: Boolean) {
|
||||||
|
val trackColor = if (isChecked) colors.primary else colors.surfaceVariant
|
||||||
|
val thumbColor = if (isChecked) colors.onPrimary else colors.onSurfaceVariant
|
||||||
|
|
||||||
|
val currentTrackColor = if (switchView.background is GradientDrawable) {
|
||||||
|
(switchView.background as GradientDrawable).color?.defaultColor
|
||||||
|
?: (if (!isChecked) colors.primary else colors.surfaceVariant)
|
||||||
|
} else {
|
||||||
|
if (!isChecked) colors.primary else colors.surfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
val trackColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentTrackColor, trackColor)
|
||||||
|
trackColorAnimator.duration = 250
|
||||||
|
trackColorAnimator.interpolator = DecelerateInterpolator()
|
||||||
|
trackColorAnimator.addUpdateListener { animator ->
|
||||||
|
val animatedColor = animator.animatedValue as Int
|
||||||
|
val track = GradientDrawable().apply {
|
||||||
|
setColor(animatedColor)
|
||||||
|
setCornerRadius(LogBoxUtils.dpToPx(context, 16).toFloat())
|
||||||
|
}
|
||||||
|
switchView.background = track
|
||||||
|
}
|
||||||
|
|
||||||
|
if (switchView is LinearLayout) {
|
||||||
|
val thumb = if (switchView.childCount > 0) {
|
||||||
|
switchView.getChildAt(0)
|
||||||
|
} else {
|
||||||
|
View(context).apply {
|
||||||
|
val size = LogBoxUtils.dpToPx(context, 24)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(size, size).apply {
|
||||||
|
marginStart = LogBoxUtils.dpToPx(context, 4)
|
||||||
|
marginEnd = LogBoxUtils.dpToPx(context, 24)
|
||||||
|
}
|
||||||
|
switchView.addView(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentParams = thumb.layoutParams as LinearLayout.LayoutParams
|
||||||
|
val targetMarginStart = if (isChecked) LogBoxUtils.dpToPx(context, 24) else LogBoxUtils.dpToPx(context, 4)
|
||||||
|
|
||||||
|
val marginAnimator = ValueAnimator.ofInt(currentParams.marginStart, targetMarginStart)
|
||||||
|
marginAnimator.duration = 250
|
||||||
|
marginAnimator.interpolator = DecelerateInterpolator()
|
||||||
|
marginAnimator.addUpdateListener { animator ->
|
||||||
|
val animatedMargin = animator.animatedValue as Int
|
||||||
|
val params = thumb.layoutParams as LinearLayout.LayoutParams
|
||||||
|
params.marginStart = animatedMargin
|
||||||
|
params.marginEnd = LogBoxUtils.dpToPx(context, 28) - animatedMargin
|
||||||
|
thumb.layoutParams = params
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentThumbColor = if (thumb.background is GradientDrawable) {
|
||||||
|
(thumb.background as GradientDrawable).color?.defaultColor
|
||||||
|
?: (if (!isChecked) colors.onPrimary else colors.onSurfaceVariant)
|
||||||
|
} else {
|
||||||
|
if (!isChecked) colors.onPrimary else colors.onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbColorAnimator = ValueAnimator.ofObject(ArgbEvaluator(), currentThumbColor, thumbColor)
|
||||||
|
thumbColorAnimator.duration = 250
|
||||||
|
thumbColorAnimator.interpolator = DecelerateInterpolator()
|
||||||
|
thumbColorAnimator.addUpdateListener { animator ->
|
||||||
|
val animatedColor = animator.animatedValue as Int
|
||||||
|
thumb.background = GradientDrawable().apply {
|
||||||
|
setColor(animatedColor)
|
||||||
|
shape = GradientDrawable.OVAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val animatorSet = AnimatorSet()
|
||||||
|
animatorSet.playTogether(trackColorAnimator, marginAnimator, thumbColorAnimator)
|
||||||
|
animatorSet.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDialog(context: Context, container: LinearLayout): AlertDialog {
|
||||||
|
val dialog = AlertDialog.Builder(context)
|
||||||
|
.setView(container)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.window?.setBackgroundDrawable(
|
||||||
|
LogBoxUtils.createM3Background(context, Color.TRANSPARENT, 28f)
|
||||||
|
)
|
||||||
|
|
||||||
|
constrainContainerWidth(context, container)
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun constrainContainerWidth(context: Context, container: LinearLayout) {
|
||||||
|
try {
|
||||||
|
val maxW = (context.resources.displayMetrics.widthPixels * 0.9).toInt()
|
||||||
|
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
container.layoutParams = lp
|
||||||
|
try {
|
||||||
|
container.minimumWidth = maxW
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setDialogWindowWidth(dialog: AlertDialog, context: Context) {
|
||||||
|
try {
|
||||||
|
val screenW = context.resources.displayMetrics.widthPixels
|
||||||
|
val maxByPercent = (screenW * 0.9).toInt()
|
||||||
|
val maxByDp = LogBoxUtils.dpToPx(context, 420)
|
||||||
|
val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp
|
||||||
|
|
||||||
|
dialog.window?.setLayout(targetW, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentMenuContainer?.let { container ->
|
||||||
|
val lp = container.layoutParams ?: ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
container.layoutParams = lp
|
||||||
|
try {
|
||||||
|
container.minimumWidth = targetW
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
container.post {
|
||||||
|
try {
|
||||||
|
container.requestLayout()
|
||||||
|
container.invalidate()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMenuSelection(context: Context, index: Int) {
|
||||||
|
when (index) {
|
||||||
|
0 -> LogBoxActions.toggleSafeMode(context)
|
||||||
|
1 -> showConfirmAction(
|
||||||
|
context, "Refetch Bundle",
|
||||||
|
"This will download the latest bundle from Github."
|
||||||
|
) { LogBoxActions.refetchBundle(context) }
|
||||||
|
|
||||||
|
2 -> showCustomBundleDialog(context)
|
||||||
|
3 -> ShiggyXposed.xposed.Utils.Companion.reloadApp()
|
||||||
|
4 -> showConfirmAction(
|
||||||
|
context, "Clear Cache & Reset",
|
||||||
|
"This will clear all cached bundles and reset to default settings."
|
||||||
|
) { LogBoxActions.clearCacheAndReset(context) }
|
||||||
|
|
||||||
|
// Options submenu
|
||||||
|
5 -> showOptionsMenu(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package ShiggyXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
object LogBoxNavigation {
|
||||||
|
fun showRecoveryMenu(context: Context) {
|
||||||
|
LogBoxMenu.showRecoveryMenu(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package ShiggyXposed.xposed.modules.LogBox
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import ShiggyXposed.xposed.Constants
|
||||||
|
import ShiggyXposed.xposed.Utils.Log
|
||||||
|
|
||||||
|
object LogBoxUtils {
|
||||||
|
|
||||||
|
fun dpToPx(context: Context, dp: Int): Int {
|
||||||
|
return TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
dp.toFloat(),
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable {
|
||||||
|
return GradientDrawable().apply {
|
||||||
|
setColor(color)
|
||||||
|
setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPyoncordDirectory(context: Context): File {
|
||||||
|
val dir = File(context.filesDir, "pyoncord")
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs()
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showM3Toast(context: Context, message: String) {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAppearanceMode(context: Context): String {
|
||||||
|
return try {
|
||||||
|
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
|
||||||
|
if (!settingsFile.exists()) return "system"
|
||||||
|
val json = JSONObject(settingsFile.readText())
|
||||||
|
json.optString("appearanceMode", "system")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error reading appearance mode: ${e.message}")
|
||||||
|
"system"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAppearanceMode(context: Context, mode: String) {
|
||||||
|
try {
|
||||||
|
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
|
||||||
|
settingsFile.parentFile?.mkdirs()
|
||||||
|
val settings = if (settingsFile.exists()) JSONObject(settingsFile.readText()) else JSONObject()
|
||||||
|
settings.put("appearanceMode", mode)
|
||||||
|
settingsFile.writeText(settings.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogBoxActions.showError(context, "Failed to set appearance", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMenuFlavor(context: Context, flavor: String) {
|
||||||
|
try {
|
||||||
|
val themeFile = File(context.filesDir, "logbox/LOGBOX_THEMES")
|
||||||
|
themeFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
val themeJson = JSONObject().apply {
|
||||||
|
put("menuFlavor", flavor)
|
||||||
|
}
|
||||||
|
|
||||||
|
themeFile.writeText(themeJson.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error saving menu flavor: ${e.message}")
|
||||||
|
LogBoxActions.showError(context, "Failed to save flavor", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isBundleInjectionDisabled(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val cacheDir = File(context.dataDir, Constants.CACHE_DIR)
|
||||||
|
val bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE)
|
||||||
|
val disabled = File(cacheDir, "${Constants.MAIN_SCRIPT_FILE}.disabled")
|
||||||
|
|
||||||
|
if (disabled.exists()) {
|
||||||
|
if (bundle.exists()) {
|
||||||
|
bundle.delete()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (bundle.exists()) return false
|
||||||
|
|
||||||
|
// If neither file exists, check settings
|
||||||
|
val settingsFile = File(context.filesDir, "logbox/LOGBOX_SETTINGS")
|
||||||
|
if (!settingsFile.exists()) return false
|
||||||
|
val json = JSONObject(settingsFile.readText())
|
||||||
|
json.optBoolean("bundleInjectionDisabled", false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error reading injection setting: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSafeModeEnabled(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS")
|
||||||
|
if (!settingsFile.exists()) return false
|
||||||
|
|
||||||
|
val json = JSONObject(settingsFile.readText())
|
||||||
|
json.optJSONObject("safeMode")?.optBoolean("enabled", false) ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Error checking safe mode: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue