Compare commits
No commits in common. "master" and "V1.3.3" have entirely different histories.
35 changed files with 257 additions and 2935 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,4 +15,3 @@ build
|
||||||
local.properties
|
local.properties
|
||||||
app/release/release/
|
app/release/release/
|
||||||
release
|
release
|
||||||
release.keystore
|
|
||||||
|
|
|
||||||
40
README.md
40
README.md
|
|
@ -1,40 +0,0 @@
|
||||||
# GoonXposed
|
|
||||||
|
|
||||||
GoonXposed is a root Xposed module that injects the GoonCord Discord mobile client modifications into the official Discord application.
|
|
||||||
|
|
||||||
## Info
|
|
||||||
* Injects GoonCord client into the Discord mobile app.
|
|
||||||
* Built in Kotlin.
|
|
||||||
* Material 3 like Shake menu with some discord popup accents
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
To use this module, your device must meet the following requirements:
|
|
||||||
* **Root Access**: Provided by Magisk, KernelSU, KernelSUNext, or Sukisu Ultra.
|
|
||||||
* **Xposed Framework**: Use JingMatrix Fork of [LSPosed](https://github.com/JingMatrix/LSPosed).
|
|
||||||
* **Target Application**: An official Discord application (stable, beta, or alpha version).
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
1. Download the latest version of the module from the [Releases](https://github.com/OTKUSteyler/GoonXposed/releases) page.
|
|
||||||
2. Install the downloaded APK file on your device.
|
|
||||||
3. Open the LSPosed manager application.
|
|
||||||
4. Go to the **Modules** section and enable **GoonXposed**.
|
|
||||||
5. In the module's settings, set the scope to include the **Discord** application.
|
|
||||||
6. Reboot your device for the changes to take effect.
|
|
||||||
|
|
||||||
The project is built with Kotlin and inherits its foundational structure from KettuXposed.
|
|
||||||
|
|
||||||
# Troubleshooting
|
|
||||||
|
|
||||||
- Module not working: Ensure the module is enabled in LSPosed and that Discord is correctly selected in its scope. if still nothing, head to our [discord support server](https://discord.gg/nQykFF9Ud6)
|
|
||||||
- Any other issues: Same server as above, listing them all is not worth it.
|
|
||||||
|
|
||||||
# Thanks & Credits
|
|
||||||
|
|
||||||
This project was made possible thanks to the work of:
|
|
||||||
|
|
||||||
· The LSPosed Team for the framework.
|
|
||||||
· JingMatrix for their LSPosed fork.
|
|
||||||
· [cocobo1](https://codeberg.org/cocobo1) for adjusting the Xposed module for Kettu client mod.
|
|
||||||
· [Revenge Team](https://github.com/revenge-mod) for making a really good Xposed Module to base of
|
|
||||||
|
|
||||||

|
|
||||||
|
|
@ -12,20 +12,8 @@ android {
|
||||||
applicationId = "GoonXposed.xposed"
|
applicationId = "GoonXposed.xposed"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1358
|
versionCode = 1330
|
||||||
versionName = "1.3.8"
|
versionName = "1.3.3"
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
create("release") {
|
|
||||||
val keystoreFile = file("../release.keystore")
|
|
||||||
if (keystoreFile.exists()) {
|
|
||||||
storeFile = keystoreFile
|
|
||||||
storePassword = "goonxposed"
|
|
||||||
keyAlias = "goonxposed"
|
|
||||||
keyPassword = "goonxposed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
@ -36,11 +24,7 @@ android {
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = false
|
||||||
val keystoreFile = file("../release.keystore")
|
|
||||||
if (keystoreFile.exists()) {
|
|
||||||
signingConfig = signingConfigs.getByName("release")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|
@ -55,10 +39,6 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<org.gradle.api.tasks.compile.JavaCompile>().configureEach {
|
|
||||||
options.compilerArgs.add("-Xlint:-options")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("de.robv.android.xposed:api:82")
|
compileOnly("de.robv.android.xposed:api:82")
|
||||||
|
|
||||||
|
|
@ -68,6 +48,4 @@ 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.rikka.shizuku:api:13.1.5")
|
|
||||||
implementation("dev.rikka.shizuku:provider:13.1.5")
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||||
BlockCrashReportingModule,
|
BlockCrashReportingModule,
|
||||||
LogBoxModule,
|
LogBoxModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
PerfPatchesModule,
|
|
||||||
FontsModule,
|
FontsModule,
|
||||||
ThemesModule,
|
ThemesModule,
|
||||||
SysColorsModule
|
SysColorsModule
|
||||||
|
|
|
||||||
|
|
@ -2,45 +2,171 @@ package GoonXposed.xposed
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.os.Build
|
||||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
import de.robv.android.xposed.XC_MethodHook
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.XposedHelpers
|
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
data class AppInfo(
|
||||||
|
val name: String,
|
||||||
|
val packageName: String,
|
||||||
|
val version: String,
|
||||||
|
val versionCode: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract class Module {
|
||||||
/**
|
/**
|
||||||
* Base class for every Xposed side module.
|
* Builds a JSON payload to be injected into the JavaScript context.
|
||||||
*
|
|
||||||
* The [Main] loader forwards the various [XC_LoadPackage] lifecycle callbacks to every
|
|
||||||
* registered [Module] instance. Modules may override any subset of the hooks below.
|
|
||||||
*/
|
*/
|
||||||
open class Module {
|
@Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
|
||||||
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
|
open fun buildPayload(builder: JsonObjectBuilder) {
|
||||||
|
|
||||||
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
|
|
||||||
|
|
||||||
open fun onContext(context: Context) {}
|
|
||||||
|
|
||||||
open fun onActivity(activity: Activity) {}
|
|
||||||
|
|
||||||
@Deprecated("This method is deprecated in the parent class")
|
|
||||||
open fun buildPayload(builder: JsonObjectBuilder) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every
|
* Called during Zygote initialization.
|
||||||
* `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
|
|
||||||
*/
|
*/
|
||||||
class HookScope(val param: XC_MethodHook.MethodHookParam) {
|
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
|
||||||
val thisObject: Any?
|
|
||||||
get() = param.thisObject
|
|
||||||
|
|
||||||
val args: Array<Any?>
|
/**
|
||||||
get() = param.args
|
* Called when a package is loaded.
|
||||||
|
*
|
||||||
|
* Hooks should typically be set up here.
|
||||||
|
*
|
||||||
|
* Bridge methods that do not require [Context] can also be registered here.
|
||||||
|
*/
|
||||||
|
open fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after [Context] has been attached to a [android.content.ContextWrapper]
|
||||||
|
* via [android.content.ContextWrapper.attachBaseContext].
|
||||||
|
*
|
||||||
|
* This may be called multiple times for different [Context]s, e.g. when the app is killed by the system.
|
||||||
|
*
|
||||||
|
* For bridge methods that require access to [Context], register them here.
|
||||||
|
*/
|
||||||
|
open fun onContext(context: Context) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after an [Activity.onCreate] method is executed.
|
||||||
|
*
|
||||||
|
* UI code that requires an [Activity] context can be placed here.
|
||||||
|
*
|
||||||
|
* Bridge methods must not be registered here, as it can be too late and cause crashes.
|
||||||
|
*
|
||||||
|
* If you need to register bridge methods that require an [Activity], do it in [onContext] and make it async.
|
||||||
|
*
|
||||||
|
* Once an [Activity] is available, execute the queued calls. Then from JS, call the method asynchronously.
|
||||||
|
*/
|
||||||
|
open fun onActivity(activity: Activity) {}
|
||||||
|
|
||||||
|
protected fun Context.getAppInfo(): AppInfo {
|
||||||
|
val pInfo = packageManager.getPackageInfo(packageName, 0)
|
||||||
|
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pInfo.longVersionCode
|
||||||
|
else @Suppress("DEPRECATION") pInfo.versionCode.toLong()
|
||||||
|
|
||||||
|
return AppInfo(
|
||||||
|
packageManager.getApplicationLabel(applicationInfo).toString(),
|
||||||
|
packageName,
|
||||||
|
pInfo.versionName ?: versionCode.toString(),
|
||||||
|
versionCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun File.asDir() {
|
||||||
|
if (!this.isDirectory) this.delete()
|
||||||
|
this.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun File.asFile() {
|
||||||
|
if (!this.isFile) this.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun Class<*>.method(
|
||||||
|
name: String, vararg params: Class<*>?
|
||||||
|
): Method = getDeclaredMethod(name, *params).apply {
|
||||||
|
isAccessible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun ClassLoader.safeLoadClass(name: String): Class<*>? = runCatching { loadClass(name) }.getOrNull()
|
||||||
|
|
||||||
|
protected fun Method.hook(hook: XC_MethodHook): XC_MethodHook.Unhook = XposedBridge.hookMethod(this, hook)
|
||||||
|
|
||||||
|
protected fun Method.hook(block: MethodHookBuilder.() -> Unit): XC_MethodHook.Unhook =
|
||||||
|
hook(MethodHookBuilder().apply(block).build())
|
||||||
|
|
||||||
|
protected fun Class<*>.hookMethod(
|
||||||
|
name: String, vararg params: Class<*>?, block: MethodHookBuilder.() -> Unit
|
||||||
|
): XC_MethodHook.Unhook = method(name, *params).hook(MethodHookBuilder().apply(block).build())
|
||||||
|
|
||||||
|
protected class MethodHookBuilder {
|
||||||
|
private var beforeBlock: (HookScope.() -> Unit)? = null
|
||||||
|
private var afterBlock: (HookScope.() -> Unit)? = null
|
||||||
|
|
||||||
|
fun before(block: HookScope.() -> Unit) {
|
||||||
|
beforeBlock = block
|
||||||
|
}
|
||||||
|
|
||||||
|
fun after(block: HookScope.() -> Unit) {
|
||||||
|
afterBlock = block
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): XC_MethodHook = object : XC_MethodHook() {
|
||||||
|
override fun beforeHookedMethod(param: MethodHookParam) {
|
||||||
|
val b = beforeBlock
|
||||||
|
if (b != null) {
|
||||||
|
val scope = HookScope(
|
||||||
|
param = param, proceed = { p ->
|
||||||
|
super.beforeHookedMethod(p)
|
||||||
|
})
|
||||||
|
scope.b()
|
||||||
|
} else {
|
||||||
|
super.beforeHookedMethod(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterHookedMethod(param: MethodHookParam) {
|
||||||
|
val a = afterBlock
|
||||||
|
if (a != null) {
|
||||||
|
val scope = HookScope(
|
||||||
|
param = param, proceed = { p ->
|
||||||
|
super.afterHookedMethod(p)
|
||||||
|
})
|
||||||
|
scope.a()
|
||||||
|
} else {
|
||||||
|
super.afterHookedMethod(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope object passed to before/after hook blocks.
|
||||||
|
*
|
||||||
|
* Provides:
|
||||||
|
* - Access to the [param] object
|
||||||
|
* - [proceed] to call the original XC_MethodHook super method
|
||||||
|
* - Accessors for `thisObject`, `args`, `result`, and `throwable`
|
||||||
|
*
|
||||||
|
* @property param The [XC_MethodHook.MethodHookParam] for the current hook.
|
||||||
|
* @property proceed Function that calls the super method.
|
||||||
|
*/
|
||||||
|
protected class HookScope internal constructor(
|
||||||
|
val param: XC_MethodHook.MethodHookParam, private val proceed: (XC_MethodHook.MethodHookParam) -> Unit
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Continues with the default XC_MethodHook super behavior.
|
||||||
|
* Equivalent to calling `super.beforeHookedMethod(param)` or
|
||||||
|
* `super.afterHookedMethod(param)` depending on the phase.
|
||||||
|
*/
|
||||||
|
fun proceed() = proceed(param)
|
||||||
|
|
||||||
|
val thisObject: Any? get() = param.thisObject
|
||||||
|
|
||||||
|
val args: Array<Any?> get() = param.args
|
||||||
|
|
||||||
var result: Any?
|
var result: Any?
|
||||||
get() = param.result
|
get() = param.result
|
||||||
|
|
@ -54,108 +180,4 @@ class HookScope(val param: XC_MethodHook.MethodHookParam) {
|
||||||
param.throwable = value
|
param.throwable = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* DSL builder for lazily constructed [XC_MethodHook] instances.
|
|
||||||
*
|
|
||||||
* ```kotlin
|
|
||||||
* val hook = MethodHookBuilder().run {
|
|
||||||
* before { result = null }
|
|
||||||
* after { Log.i("done") }
|
|
||||||
* build()
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
class MethodHookBuilder {
|
|
||||||
private val beforeCallbacks = mutableListOf<HookScope.() -> Unit>()
|
|
||||||
private val afterCallbacks = mutableListOf<HookScope.() -> Unit>()
|
|
||||||
|
|
||||||
fun before(callback: HookScope.() -> Unit) {
|
|
||||||
beforeCallbacks += callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun after(callback: HookScope.() -> Unit) {
|
|
||||||
afterCallbacks += callback
|
|
||||||
}
|
|
||||||
|
|
||||||
fun build(): XC_MethodHook = object : XC_MethodHook() {
|
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
|
||||||
val scope = HookScope(param)
|
|
||||||
beforeCallbacks.forEach { scope.run(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun afterHookedMethod(param: MethodHookParam) {
|
|
||||||
val scope = HookScope(param)
|
|
||||||
afterCallbacks.forEach { scope.run(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(block: MethodHookBuilder.() -> Unit): XC_MethodHook = MethodHookBuilder().run {
|
|
||||||
block()
|
|
||||||
build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ClassLoader.safeLoadClass(name: String): Class<*>? = try {
|
|
||||||
loadClass(name)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Class<*>.method(name: String, vararg parameterTypes: Class<*>): Method =
|
|
||||||
XposedHelpers.findMethodExact(this, name, *parameterTypes)
|
|
||||||
|
|
||||||
fun Method.hook(block: MethodHookBuilder.() -> Unit) {
|
|
||||||
val builder = MethodHookBuilder()
|
|
||||||
builder.block()
|
|
||||||
XposedBridge.hookMethod(this, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Method.hook(hook: XC_MethodHook) {
|
|
||||||
XposedBridge.hookMethod(this, hook)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Class<*>.hookMethod(
|
|
||||||
name: String,
|
|
||||||
vararg parameterTypes: Class<*>,
|
|
||||||
block: MethodHookBuilder.() -> Unit
|
|
||||||
): Method {
|
|
||||||
val method = method(name, *parameterTypes)
|
|
||||||
method.hook(block)
|
|
||||||
return method
|
|
||||||
}
|
|
||||||
|
|
||||||
fun File.asDir(): File {
|
|
||||||
if (!exists() || !isDirectory) mkdirs()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun File.asFile(): File {
|
|
||||||
parentFile?.mkdirs()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
data class AppInfo(
|
|
||||||
val name: String,
|
|
||||||
val packageName: String,
|
|
||||||
val version: String,
|
|
||||||
val versionCode: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Context.getAppInfo(): AppInfo {
|
|
||||||
val label = applicationInfo.loadLabel(packageManager).toString()
|
|
||||||
val packageInfo = try {
|
|
||||||
packageManager.getPackageInfo(packageName, 0)
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppInfo(
|
|
||||||
name = label,
|
|
||||||
packageName = packageName,
|
|
||||||
version = packageInfo?.versionName ?: "",
|
|
||||||
versionCode = packageInfo?.versionCode ?: 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package GoonXposed.xposed
|
package GoonXposed.xposed
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.Application
|
import android.app.AndroidAppHelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import de.robv.android.xposed.XposedHelpers
|
|
||||||
import GoonXposed.xposed.modules.UpdaterModule
|
import GoonXposed.xposed.modules.UpdaterModule
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -15,9 +14,7 @@ class Utils {
|
||||||
val JSON = Json { ignoreUnknownKeys = true }
|
val JSON = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
fun reloadApp() {
|
fun reloadApp() {
|
||||||
val activityThread = XposedHelpers.findClass("android.app.ActivityThread", null)
|
val application = AndroidAppHelper.currentApplication()
|
||||||
val application =
|
|
||||||
XposedHelpers.callStaticMethod(activityThread, "currentApplication") as? Application ?: return
|
|
||||||
val intent = application.packageManager.getLaunchIntentForPackage(application.packageName)
|
val intent = application.packageManager.getLaunchIntentForPackage(application.packageName)
|
||||||
application.startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
application.startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
|
|
@ -40,6 +37,11 @@ class Utils {
|
||||||
|
|
||||||
reloadApp()
|
reloadApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
UpdaterModule.resetLoaderConfig(context)
|
||||||
|
reloadApp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@ import android.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.asDir
|
|
||||||
import GoonXposed.xposed.asFile
|
|
||||||
import GoonXposed.xposed.getAppInfo
|
|
||||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
|
|
@ -38,7 +35,7 @@ import java.io.*
|
||||||
*/
|
*/
|
||||||
object CacheModule : Module() {
|
object CacheModule : Module() {
|
||||||
|
|
||||||
private const val CACHE_DIR = "Goon"
|
private const val CACHE_DIR = "revenge"
|
||||||
|
|
||||||
private const val MODULES_CACHE_PREFIX = "modules"
|
private const val MODULES_CACHE_PREFIX = "modules"
|
||||||
private const val ASSETS_CACHE_PREFIX = "assets"
|
private const val ASSETS_CACHE_PREFIX = "assets"
|
||||||
|
|
@ -48,18 +45,18 @@ object CacheModule : Module() {
|
||||||
private lateinit var assetsCache: AssetsCache
|
private lateinit var assetsCache: AssetsCache
|
||||||
|
|
||||||
override fun onContext(context: Context) = with(context) {
|
override fun onContext(context: Context) = with(context) {
|
||||||
val ShiggyCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
|
val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
|
||||||
val (_, _, _, versionCode) = getAppInfo()
|
val (_, _, _, versionCode) = getAppInfo()
|
||||||
|
|
||||||
val modulesCacheFile = File(
|
val modulesCacheFile = File(
|
||||||
ShiggyCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
|
revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
|
||||||
).apply { asFile() }
|
).apply { asFile() }
|
||||||
|
|
||||||
val assetsCacheFile = File(
|
val assetsCacheFile = File(
|
||||||
ShiggyCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
|
revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
|
||||||
).apply { asFile() }
|
).apply { asFile() }
|
||||||
|
|
||||||
BridgeModule.registerMethod("Shiggy.caches.modules.read") {
|
BridgeModule.registerMethod("revenge.caches.modules.read") {
|
||||||
if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile)
|
if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile)
|
||||||
?.let {
|
?.let {
|
||||||
modulesCache = it
|
modulesCache = it
|
||||||
|
|
@ -67,7 +64,7 @@ object CacheModule : Module() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BridgeModule.registerMethod("Shiggy.caches.modules.write") {
|
BridgeModule.registerMethod("revenge.caches.modules.write") {
|
||||||
val (blacklist, finds) = it
|
val (blacklist, finds) = it
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
modulesCache = ModulesCache(
|
modulesCache = ModulesCache(
|
||||||
|
|
@ -76,7 +73,7 @@ object CacheModule : Module() {
|
||||||
Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})")
|
Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})")
|
||||||
}
|
}
|
||||||
|
|
||||||
BridgeModule.registerMethod("Shiggy.caches.assets.read") {
|
BridgeModule.registerMethod("revenge.caches.assets.read") {
|
||||||
if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile)
|
if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile)
|
||||||
?.let {
|
?.let {
|
||||||
assetsCache = it
|
assetsCache = it
|
||||||
|
|
@ -84,7 +81,7 @@ object CacheModule : Module() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BridgeModule.registerMethod("Shiggy.caches.assets.write") {
|
BridgeModule.registerMethod("revenge.caches.assets.write") {
|
||||||
val (data) = it
|
val (data) = it
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
assetsCache =
|
assetsCache =
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
package GoonXposed.xposed.modules
|
package GoonXposed.xposed.modules
|
||||||
|
|
||||||
import GoonXposed.xposed.hookMethod
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.hookMethod
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
|
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,9 @@ import de.robv.android.xposed.IXposedHookZygoteInit
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.HookScope
|
|
||||||
import GoonXposed.xposed.HookStateHolder
|
import GoonXposed.xposed.HookStateHolder
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.asDir
|
|
||||||
import GoonXposed.xposed.asFile
|
|
||||||
import GoonXposed.xposed.hook
|
|
||||||
import GoonXposed.xposed.method
|
|
||||||
import GoonXposed.xposed.safeLoadClass
|
|
||||||
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
|
@ -60,11 +54,11 @@ object HookScriptLoaderModule : Module() {
|
||||||
|
|
||||||
private fun hook(instance: Class<*>) = runCatching {
|
private fun hook(instance: Class<*>) = runCatching {
|
||||||
val loadScriptFromAssets = instance.method(
|
val loadScriptFromAssets = instance.method(
|
||||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
|
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||||
)
|
)
|
||||||
|
|
||||||
val loadScriptFromFile = instance.method(
|
val loadScriptFromFile = instance.method(
|
||||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
|
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||||
)
|
)
|
||||||
|
|
||||||
loadScriptFromAssets.hook {
|
loadScriptFromAssets.hook {
|
||||||
|
|
@ -89,16 +83,9 @@ object HookScriptLoaderModule : Module() {
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val ready = async { HookStateHolder.readyDeferred.join() }
|
val ready = async { HookStateHolder.readyDeferred.join() }
|
||||||
|
|
||||||
if (!mainScript.exists()) {
|
|
||||||
Log.i("Main script does not exist, downloading before load...")
|
|
||||||
val download = async { UpdaterModule.downloadScript().join() }
|
val download = async { UpdaterModule.downloadScript().join() }
|
||||||
|
|
||||||
awaitAll(ready, download)
|
awaitAll(ready, download)
|
||||||
} else {
|
|
||||||
Log.i("Main script exists, updating in background...")
|
|
||||||
UpdaterModule.downloadScript()
|
|
||||||
ready.await()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadSynchronously = args[2]
|
val loadSynchronously = args[2]
|
||||||
|
|
@ -113,15 +100,10 @@ object HookScriptLoaderModule : Module() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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 {
|
|
||||||
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
|
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
|
||||||
|
|
||||||
if (mainScript.exists()) {
|
if (mainScript.exists()) runScriptFile(mainScript)
|
||||||
runScriptFile(mainScript)
|
else {
|
||||||
} else {
|
|
||||||
Log.i("Main script does not exist, falling back")
|
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)
|
||||||
|
|
@ -132,7 +114,6 @@ object HookScriptLoaderModule : Module() {
|
||||||
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e("Unable to run scripts:", e)
|
Log.e("Unable to run scripts:", e)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
package GoonXposed.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/gooncord.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package GoonXposed.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
package GoonXposed.xposed.modules.LogBox
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.Toast
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
|
||||||
import GoonXposed.xposed.Utils.Log
|
|
||||||
import GoonXposed.xposed.Utils.Companion.reloadApp
|
|
||||||
import GoonXposed.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/gooncord.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 deleteDevtools(context: Context) {
|
|
||||||
try {
|
|
||||||
val devtoolsFile = File(LogBoxUtils.getPyoncordDirectory(context), "preloads/reactDevtools.js")
|
|
||||||
if (devtoolsFile.exists()) {
|
|
||||||
if (devtoolsFile.delete()) {
|
|
||||||
Toast.makeText(context, "Devtools deleted", Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "Failed to delete Devtools", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "Devtools not found", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Error deleting Devtools: ${e.message}")
|
|
||||||
showError(context, "Failed to delete Devtools", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showError(context: Context, title: String, message: String?) {
|
|
||||||
LogBoxMenu.showErrorDialog(context, title, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
package GoonXposed.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package GoonXposed.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")
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package GoonXposed.xposed.modules.LogBox
|
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import GoonXposed.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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,795 +0,0 @@
|
||||||
package GoonXposed.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 GoonXposed.xposed.Utils.Log
|
|
||||||
import GoonXposed.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, "GoonXposed 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, "Delete Devtools", colors) {
|
|
||||||
dialog.dismiss()
|
|
||||||
showConfirmAction(
|
|
||||||
context,
|
|
||||||
"Delete Devtools",
|
|
||||||
"This will delete the reactDevtools.js preload file if it exists."
|
|
||||||
) {
|
|
||||||
LogBoxActions.deleteDevtools(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 -> Utils.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
package GoonXposed.xposed.modules.LogBox
|
|
||||||
|
|
||||||
import GoonXposed.xposed.Module
|
|
||||||
import GoonXposed.xposed.Utils.Log
|
|
||||||
import GoonXposed.xposed.Utils.Companion.reloadApp
|
|
||||||
import android.content.Context
|
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
|
||||||
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" }
|
|
||||||
|
|
||||||
// NOTE: This used to call a custom `.hook { before { result = true } }` DSL that
|
|
||||||
// relied on a shared extensions file which broke (unresolved references).
|
|
||||||
// Rewritten using the standard XposedBridge/XC_MethodHook API, which is already
|
|
||||||
// used elsewhere in this file, to force this method to always return true.
|
|
||||||
XposedBridge.hookMethod(getUseDeveloperSupportMethod, object : XC_MethodHook() {
|
|
||||||
override fun beforeHookedMethod(param: MethodHookParam) {
|
|
||||||
param.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")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package GoonXposed.xposed.modules.LogBox
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object LogBoxNavigation {
|
|
||||||
fun showRecoveryMenu(context: Context) {
|
|
||||||
LogBoxMenu.showRecoveryMenu(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
package GoonXposed.xposed.modules.LogBox
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
|
||||||
import GoonXposed.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
package GoonXposed.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 GoonXposed.xposed.Constants
|
|
||||||
import GoonXposed.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
package GoonXposed.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)
|
|
||||||
GoonXposed.xposed.Utils.Companion.reloadApp()
|
|
||||||
})
|
|
||||||
|
|
||||||
container.addView(LogBoxComponents.createM3Button(context, "Refetch Bundle", colors) {
|
|
||||||
dialog.dismiss()
|
|
||||||
RecoveryMenu.showConfirmAction(
|
|
||||||
context,
|
|
||||||
"Refetch Bundle",
|
|
||||||
"This will download the latest bundle from Github."
|
|
||||||
) {
|
|
||||||
LogBoxActions.refetchBundle(context)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
container.addView(LogBoxComponents.createM3Button(context, "Clear Cache & Reset", colors) {
|
|
||||||
dialog.dismiss()
|
|
||||||
RecoveryMenu.showConfirmAction(
|
|
||||||
context,
|
|
||||||
"Clear Cache & Reset",
|
|
||||||
"This will clear all cached bundles and reset to default settings."
|
|
||||||
) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
package GoonXposed.xposed.modules.LogBox
|
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import GoonXposed.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, "GoonXposed 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 -> GoonXposed.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,7 +19,6 @@ import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import de.robv.android.xposed.XC_MethodHook.MethodHookParam
|
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
|
|
@ -27,7 +26,6 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.reloadApp
|
import GoonXposed.xposed.Utils.Companion.reloadApp
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.hook
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -418,7 +416,7 @@ object LogBoxModule : Module() {
|
||||||
dialog.show()
|
dialog.show()
|
||||||
Log.e("Recovery menu shown successfully")
|
Log.e("Recovery menu shown successfully")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Error showing recovery menu: ${e.message}")
|
Log.e("Error showing recovery menu: ${e.message}", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -642,14 +640,14 @@ object LogBoxModule : Module() {
|
||||||
val filesDir = File(context.filesDir, "pyoncord")
|
val filesDir = File(context.filesDir, "pyoncord")
|
||||||
val configFile = File(filesDir, "loader.json")
|
val configFile = File(filesDir, "loader.json")
|
||||||
var currentUrl: String? = null
|
var currentUrl: String? = null
|
||||||
var urlEnabled = false
|
var isEnabled = false
|
||||||
|
|
||||||
if (configFile.exists()) {
|
if (configFile.exists()) {
|
||||||
try {
|
try {
|
||||||
val json = JSONObject(configFile.readText())
|
val json = JSONObject(configFile.readText())
|
||||||
val customLoadUrl = json.optJSONObject("customLoadUrl")
|
val customLoadUrl = json.optJSONObject("customLoadUrl")
|
||||||
if (customLoadUrl != null) {
|
if (customLoadUrl != null) {
|
||||||
urlEnabled = customLoadUrl.optBoolean("enabled", false)
|
isEnabled = customLoadUrl.optBoolean("enabled", false)
|
||||||
currentUrl = customLoadUrl.optString("url", "")
|
currentUrl = customLoadUrl.optString("url", "")
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|
@ -701,7 +699,7 @@ object LogBoxModule : Module() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val toggleSwitch = createM3Switch(context, colors, urlEnabled)
|
val toggleSwitch = createM3Switch(context, colors, isEnabled)
|
||||||
|
|
||||||
toggleContainer.addView(toggleLabel)
|
toggleContainer.addView(toggleLabel)
|
||||||
toggleContainer.addView(toggleSwitch)
|
toggleContainer.addView(toggleSwitch)
|
||||||
|
|
@ -721,8 +719,8 @@ object LogBoxModule : Module() {
|
||||||
dpToPx(context, 16),
|
dpToPx(context, 16),
|
||||||
dpToPx(context, 12)
|
dpToPx(context, 12)
|
||||||
)
|
)
|
||||||
this.isEnabled = urlEnabled
|
isEnabled
|
||||||
alpha = if (urlEnabled) 1f else 0.5f
|
alpha = if (isEnabled) 1f else 0.5f
|
||||||
val params = LinearLayout.LayoutParams(
|
val params = LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,8 @@ import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.BuildConfig
|
import GoonXposed.xposed.BuildConfig
|
||||||
import GoonXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.MethodHookBuilder
|
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Utils.Companion.JSON
|
||||||
import GoonXposed.xposed.hook
|
|
||||||
import GoonXposed.xposed.method
|
|
||||||
import GoonXposed.xposed.safeLoadClass
|
|
||||||
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
|
|
@ -52,7 +48,7 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
||||||
"loadScriptFromFile",
|
"loadScriptFromFile",
|
||||||
String::class.java,
|
String::class.java,
|
||||||
String::class.java,
|
String::class.java,
|
||||||
Boolean::class.javaPrimitiveType!!
|
Boolean::class.javaPrimitiveType
|
||||||
), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2])
|
), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -74,11 +70,11 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
|
||||||
|
|
||||||
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
|
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
|
||||||
instance.method(
|
instance.method(
|
||||||
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType!!,
|
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType,
|
||||||
).hook(hook)
|
).hook(hook)
|
||||||
|
|
||||||
instance.method(
|
instance.method(
|
||||||
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
|
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
|
||||||
).hook(hook)
|
).hook(hook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,317 +0,0 @@
|
||||||
package GoonXposed.xposed.modules
|
|
||||||
|
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import GoonXposed.xposed.Constants
|
|
||||||
import GoonXposed.xposed.Module
|
|
||||||
import GoonXposed.xposed.Utils.Log
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PerfPatchesModule
|
|
||||||
*
|
|
||||||
* Writes a small JavaScript preload into the module's `preloads` directory.
|
|
||||||
* The preload performs guarded, best-effort runtime monkeypatches to:
|
|
||||||
* - add memoization to expensive pure functions (if present)
|
|
||||||
* - cache RowGenerator.generate outputs on instances when inputs look stable
|
|
||||||
* - batch analytics/log events to avoid high-frequency synchronous work
|
|
||||||
*
|
|
||||||
* The implementation is intentionally defensive: it never throws and only applies
|
|
||||||
* patches if the targeted symbols exist.
|
|
||||||
*/
|
|
||||||
object PerfPatchesModule : Module() {
|
|
||||||
private const val FILE_NAME = "perf_patches.js"
|
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
|
|
||||||
try {
|
|
||||||
// Build preloads path inside the app data files directory
|
|
||||||
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
|
||||||
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR).apply { mkdirs() }
|
|
||||||
val out = File(preloadsDir, FILE_NAME)
|
|
||||||
|
|
||||||
// Defensive JS runtime patch. Avoid template literals with ${} to prevent Kotlin interpolation.
|
|
||||||
val js = """
|
|
||||||
(function(){
|
|
||||||
try {
|
|
||||||
if (!globalThis.__SHIGGY_PERF_PATCHES__) {
|
|
||||||
globalThis.__SHIGGY_PERF_PATCHES__ = { installedAt: Date.now(), enabled: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
function safe(fn) {
|
|
||||||
return function() {
|
|
||||||
try { return fn.apply(this, arguments); }
|
|
||||||
catch (e) { try { console && console.warn && console.warn('shiggy patch error', e); } catch(_){} }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function memoize(fn, maxEntries) {
|
|
||||||
if (typeof fn !== 'function') return fn;
|
|
||||||
var cache = new Map();
|
|
||||||
maxEntries = Number(maxEntries) || 128;
|
|
||||||
return function() {
|
|
||||||
var key;
|
|
||||||
try { key = JSON.stringify(Array.prototype.slice.call(arguments)); }
|
|
||||||
catch (e) { key = String(arguments.length) + ':' + String(arguments[0]); }
|
|
||||||
if (cache.has(key)) return cache.get(key);
|
|
||||||
var res = fn.apply(this, arguments);
|
|
||||||
cache.set(key, res);
|
|
||||||
if (cache.size > maxEntries) {
|
|
||||||
var it = cache.keys().next();
|
|
||||||
if (!it.done) cache.delete(it.value);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBatched(fn, intervalMs) {
|
|
||||||
if (typeof fn !== 'function') return fn;
|
|
||||||
var buf = [];
|
|
||||||
var timer = null;
|
|
||||||
intervalMs = Number(intervalMs) || 2000;
|
|
||||||
function flush() {
|
|
||||||
var batch = buf.splice(0, buf.length);
|
|
||||||
if (!batch.length) return;
|
|
||||||
try { fn.call(null, batch); }
|
|
||||||
catch (e) {
|
|
||||||
// Fallback: call per event asynchronously
|
|
||||||
try {
|
|
||||||
batch.forEach(function(ev){
|
|
||||||
setTimeout(function(){ try { fn(ev); } catch (_) {} }, 0);
|
|
||||||
});
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return function(event) {
|
|
||||||
try {
|
|
||||||
buf.push(event);
|
|
||||||
if (!timer) timer = setTimeout(function(){ timer = null; flush(); }, intervalMs);
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchIfExists(path, wrapper) {
|
|
||||||
try {
|
|
||||||
var parts = path.split('.');
|
|
||||||
var obj = globalThis;
|
|
||||||
for (var i = 0; i < parts.length - 1; i++) {
|
|
||||||
obj = obj && obj[parts[i]];
|
|
||||||
}
|
|
||||||
if (!obj) return false;
|
|
||||||
var key = parts[parts.length - 1];
|
|
||||||
if (typeof obj[key] !== 'function') return false;
|
|
||||||
obj[key] = wrapper(obj[key]);
|
|
||||||
return true;
|
|
||||||
} catch (e) { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try memoizing a few well-known functions
|
|
||||||
['computeHappeningNowState', 'computeNowState'].forEach(function(name){
|
|
||||||
try { patchIfExists(name, function(orig){ return memoize(orig, 128); }); } catch(_) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try caching RowGenerator.prototype.generate by instance signature
|
|
||||||
try {
|
|
||||||
var RG = globalThis && globalThis.RowGenerator;
|
|
||||||
if (RG && RG.prototype && typeof RG.prototype.generate === 'function') {
|
|
||||||
(function(){
|
|
||||||
var orig = RG.prototype.generate;
|
|
||||||
RG.prototype.generate = function() {
|
|
||||||
try {
|
|
||||||
var sig;
|
|
||||||
if (arguments && arguments.length) {
|
|
||||||
var a0 = arguments[0];
|
|
||||||
if (Array.isArray(a0)) {
|
|
||||||
sig = 'arr:' + a0.length + ':' + a0.slice(0,8).map(function(x){ return x && x.id ? x.id : ''; }).join(',');
|
|
||||||
} else if (a0 && a0.id) {
|
|
||||||
sig = 'id:' + a0.id;
|
|
||||||
} else {
|
|
||||||
try { sig = JSON.stringify(a0).slice(0,200); } catch(e){ sig = String(a0); }
|
|
||||||
}
|
|
||||||
} else sig = 'noargs';
|
|
||||||
|
|
||||||
if (this.__shiggy_sig === sig && typeof this.__shiggy_cache !== 'undefined') {
|
|
||||||
return this.__shiggy_cache;
|
|
||||||
}
|
|
||||||
var res = orig.apply(this, arguments);
|
|
||||||
this.__shiggy_sig = sig;
|
|
||||||
this.__shiggy_cache = res;
|
|
||||||
return res;
|
|
||||||
} catch (e) { return orig.apply(this, arguments); }
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
// Batch analytics-like methods (best-effort)
|
|
||||||
['app_ui_viewed', 'logEvent', 'Analytics.log', 'Analytics.track', 'analytics.trackEvent'].forEach(function(name){
|
|
||||||
try {
|
|
||||||
var parts = name.split('.');
|
|
||||||
var obj = globalThis;
|
|
||||||
for (var i = 0; i < parts.length - 1; i++) obj = obj && obj[parts[i]];
|
|
||||||
if (!obj) return;
|
|
||||||
var key = parts[parts.length - 1];
|
|
||||||
if (typeof obj[key] === 'function') obj[key] = createBatched(obj[key], 2000);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make console methods async to avoid blocking hot-paths during startup
|
|
||||||
(function(){
|
|
||||||
try {
|
|
||||||
if (typeof console !== 'undefined') {
|
|
||||||
['log','info','warn','error','debug','trace'].forEach(function(m){
|
|
||||||
try {
|
|
||||||
if (typeof console[m] === 'function') {
|
|
||||||
var orig = console[m];
|
|
||||||
console[m] = function() {
|
|
||||||
var args = Array.prototype.slice.call(arguments);
|
|
||||||
setTimeout(function(){ try { orig.apply(console, args); } catch(_){} }, 0);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch(e){}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch(e){}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Deduplicate and slightly delay repeated kv calls to reduce IO contention on startup
|
|
||||||
(function(){
|
|
||||||
try {
|
|
||||||
var pending = new Map();
|
|
||||||
// Lightweight stats for dedupe wrapper to help telemetry/debugging in-case we need to tune delays.
|
|
||||||
try {
|
|
||||||
globalThis.__SHIGGY_PERF_DEDUPE_STATS__ = globalThis.__SHIGGY_PERF_DEDUPE_STATS__ || {
|
|
||||||
methods: {},
|
|
||||||
totalRequests: 0,
|
|
||||||
totalHits: 0,
|
|
||||||
totalMisses: 0,
|
|
||||||
pending: 0,
|
|
||||||
lastUpdated: Date.now()
|
|
||||||
};
|
|
||||||
} catch(e) {}
|
|
||||||
|
|
||||||
function _incStats(name, key) {
|
|
||||||
try {
|
|
||||||
var s = globalThis.__SHIGGY_PERF_DEDUPE_STATS__;
|
|
||||||
if (!s) return;
|
|
||||||
s.lastUpdated = Date.now();
|
|
||||||
var m = s.methods[name] || (s.methods[name] = { requests: 0, hits: 0, misses: 0, pending: 0 });
|
|
||||||
if (key === 'requests') { m.requests++; s.totalRequests++; }
|
|
||||||
else if (key === 'hits') { m.hits++; s.totalHits++; }
|
|
||||||
else if (key === 'misses') { m.misses++; s.totalMisses++; }
|
|
||||||
else if (key === 'pending_inc') { m.pending++; s.pending++; }
|
|
||||||
else if (key === 'pending_dec') { m.pending = Math.max(0, m.pending - 1); s.pending = Math.max(0, s.pending - 1); }
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapAsyncDedupe(kvObj, methodName, delayMs){
|
|
||||||
try {
|
|
||||||
var orig = kvObj[methodName];
|
|
||||||
if (typeof orig !== 'function') return;
|
|
||||||
kvObj[methodName] = function() {
|
|
||||||
var argsArr = Array.prototype.slice.call(arguments);
|
|
||||||
var key;
|
|
||||||
try { key = JSON.stringify([methodName, argsArr]); } catch(e){ key = methodName + ':' + String(argsArr[0]); }
|
|
||||||
_incStats(methodName, 'requests');
|
|
||||||
if (pending.has(key)) { _incStats(methodName, 'hits'); return pending.get(key); }
|
|
||||||
_incStats(methodName, 'misses');
|
|
||||||
var self = this;
|
|
||||||
var p = new Promise(function(resolve, reject){
|
|
||||||
setTimeout(function(){
|
|
||||||
try {
|
|
||||||
var res = orig.apply(self, argsArr);
|
|
||||||
if (res && typeof res.then === 'function') res.then(resolve, reject);
|
|
||||||
else resolve(res);
|
|
||||||
} catch (e) { reject(e); }
|
|
||||||
}, Number(delayMs) || 20);
|
|
||||||
});
|
|
||||||
pending.set(key, p);
|
|
||||||
_incStats(methodName, 'pending_inc');
|
|
||||||
var cleanup = function(){ pending.delete(key); _incStats(methodName, 'pending_dec'); };
|
|
||||||
p.then(cleanup, cleanup);
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
} catch(e){}
|
|
||||||
}
|
|
||||||
if (globalThis.kv) {
|
|
||||||
if (globalThis.kv.get_many) wrapAsyncDedupe(globalThis.kv, 'get_many', 20);
|
|
||||||
if (globalThis.kv.get_kv_entries) wrapAsyncDedupe(globalThis.kv, 'get_kv_entries', 20);
|
|
||||||
}
|
|
||||||
} catch(e){}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Defer heavy filesystem/storage calls slightly to prioritize UI rendering
|
|
||||||
(function(){
|
|
||||||
try {
|
|
||||||
function makeDeferredIfAsync(obj, name, delayMs) {
|
|
||||||
try {
|
|
||||||
var orig = obj[name];
|
|
||||||
if (typeof orig !== 'function') return;
|
|
||||||
var isAsync = orig.constructor && orig.constructor.name === 'AsyncFunction';
|
|
||||||
var src = String(orig);
|
|
||||||
if (!isAsync && src.indexOf('return new Promise') === -1 && src.indexOf('.then(') === -1) {
|
|
||||||
// Likely synchronous - skip deferral to avoid changing semantics
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
obj[name] = function() {
|
|
||||||
var args = Array.prototype.slice.call(arguments);
|
|
||||||
var self = this;
|
|
||||||
return new Promise(function(resolve, reject){
|
|
||||||
setTimeout(function(){
|
|
||||||
try {
|
|
||||||
var res = orig.apply(self, args);
|
|
||||||
if (res && typeof res.then === 'function') res.then(resolve, reject);
|
|
||||||
else resolve(res);
|
|
||||||
} catch (e) { reject(e); }
|
|
||||||
}, Number(delayMs) || 100);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} catch(e){}
|
|
||||||
}
|
|
||||||
if (globalThis.db && typeof globalThis.db.fs_info === 'function') {
|
|
||||||
makeDeferredIfAsync(globalThis.db, 'fs_info', 100);
|
|
||||||
}
|
|
||||||
if (globalThis.Storage && typeof globalThis.Storage.refresh === 'function') {
|
|
||||||
makeDeferredIfAsync(globalThis.Storage, 'refresh', 50);
|
|
||||||
}
|
|
||||||
} catch(e){}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Expose lightweight API
|
|
||||||
try {
|
|
||||||
if (!globalThis.__SHIGGY_PERF_API__) {
|
|
||||||
globalThis.__SHIGGY_PERF_API__ = {
|
|
||||||
installedAt: Date.now(),
|
|
||||||
enabled: true,
|
|
||||||
uninstall: function() { try { globalThis.__SHIGGY_PERF_API__.enabled = false; } catch(e) {} }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
try { console && console.warn && console.warn('PerfPatches: error', e); } catch(_) {}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
// Write file if absent or changed
|
|
||||||
try {
|
|
||||||
if (!out.exists() || out.readText() != js) {
|
|
||||||
out.writeText(js)
|
|
||||||
Log.i("PerfPatchesModule: wrote patch to " + out.absolutePath)
|
|
||||||
} else {
|
|
||||||
Log.i("PerfPatchesModule: perf patch already up-to-date")
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("PerfPatchesModule: failed writing patch", e)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("PerfPatchesModule:onLoad failed", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Overrides deprecated Module.buildPayload")
|
|
||||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
|
||||||
builder.put("perfPatches", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.asFile
|
|
||||||
import GoonXposed.xposed.modules.bridge.BridgeModule
|
import GoonXposed.xposed.modules.bridge.BridgeModule
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,6 @@ object UpdaterModule : Module() {
|
||||||
private lateinit var cacheDir: File
|
private lateinit var cacheDir: File
|
||||||
private lateinit var bundle: File
|
private lateinit var bundle: File
|
||||||
private lateinit var etag: 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_CACHED = 5000L
|
||||||
private const val TIMEOUT = 10000L
|
private const val TIMEOUT = 10000L
|
||||||
|
|
@ -57,11 +55,9 @@ object UpdaterModule : Module() {
|
||||||
private const val CONFIG_FILE = "loader.json"
|
private const val CONFIG_FILE = "loader.json"
|
||||||
|
|
||||||
private const val DEFAULT_BUNDLE_URL =
|
private const val DEFAULT_BUNDLE_URL =
|
||||||
"https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js"
|
"https://github.com/OTKUSteyler/GoonCord/releases/download/latest/gooncord.js"
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
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() }
|
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
|
||||||
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
|
||||||
|
|
||||||
|
|
@ -79,23 +75,6 @@ object UpdaterModule : Module() {
|
||||||
|
|
||||||
fun downloadScript(activity: Activity? = null): Job = scope.launch {
|
fun downloadScript(activity: Activity? = null): Job = scope.launch {
|
||||||
try {
|
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) {
|
HttpClient(CIO) {
|
||||||
expectSuccess = false
|
expectSuccess = false
|
||||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package GoonXposed.xposed.modules.appearance
|
package GoonXposed.xposed.modules.appearance
|
||||||
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
|
|
@ -29,14 +27,6 @@ import kotlinx.serialization.json.put
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
// NOTE: These extensions used to be resolved from a shared Utils/Extensions file,
|
|
||||||
// but that reference broke (renamed/removed/relocated), causing "Unresolved reference"
|
|
||||||
// build failures. Defining them locally here removes the dependency on that file.
|
|
||||||
// asFile(): ensures the parent directory of a file exists.
|
|
||||||
// asDir(): ensures the directory itself exists.
|
|
||||||
private fun File.asFile(): File = apply { parentFile?.mkdirs() }
|
|
||||||
private fun File.asDir(): File = apply { mkdirs() }
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class FontDefinition(
|
data class FontDefinition(
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
|
|
@ -62,7 +52,7 @@ object FontsModule : Module() {
|
||||||
|
|
||||||
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
|
||||||
XposedHelpers.findAndHookMethod(
|
XposedHelpers.findAndHookMethod(
|
||||||
"com.facebook.react.common.assets.ReactFontManager\$Companion",
|
"com.facebook.react.views.text.ReactFontManager\$Companion",
|
||||||
classLoader,
|
classLoader,
|
||||||
"createAssetTypeface",
|
"createAssetTypeface",
|
||||||
String::class.java,
|
String::class.java,
|
||||||
|
|
@ -103,40 +93,28 @@ object FontsModule : Module() {
|
||||||
|
|
||||||
// These files should be downloaded by the JS side, but oh well
|
// These files should be downloaded by the JS side, but oh well
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val client = HttpClient(CIO) {
|
|
||||||
install(UserAgent) { agent = Constants.USER_AGENT }
|
|
||||||
install(HttpTimeout) {
|
|
||||||
requestTimeoutMillis = 15_000
|
|
||||||
connectTimeoutMillis = 10_000
|
|
||||||
socketTimeoutMillis = 15_000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fontDef.main.keys.map { name ->
|
fontDef.main.keys.map { name ->
|
||||||
async {
|
async {
|
||||||
val url = fontDef.main.getValue(name)
|
val url = fontDef.main.getValue(name)
|
||||||
try {
|
try {
|
||||||
val extension = FILE_EXTENSIONS.firstOrNull { url.substringBefore('?').endsWith(it) }
|
Log.i("Downloading $name from $url")
|
||||||
?: FILE_EXTENSIONS[0]
|
val file =
|
||||||
val file = File(fontsDir, "$name$extension").apply { asFile() }
|
File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() }
|
||||||
if (file.exists()) return@async
|
if (file.exists()) return@async
|
||||||
|
|
||||||
Log.i("Downloading $name from $url")
|
val client = HttpClient(CIO) {
|
||||||
val response: HttpResponse = client.get(url)
|
install(UserAgent) { agent = Constants.USER_AGENT }
|
||||||
if (response.status == HttpStatusCode.OK) {
|
|
||||||
file.writeBytes(response.body())
|
|
||||||
} else {
|
|
||||||
Log.e("Failed to download fonts ($name from $url): HTTP ${response.status}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val response: HttpResponse = client.get(url)
|
||||||
|
if (response.status == HttpStatusCode.OK) file.writeBytes(response.body())
|
||||||
|
|
||||||
|
return@async
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e("Failed to download fonts ($name from $url)", e)
|
Log.e("Failed to download fonts ($name from $url)", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
} finally {
|
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package GoonXposed.xposed.modules.appearance
|
package GoonXposed.xposed.modules.appearance
|
||||||
|
|
||||||
import android.R.color
|
import android.R.color
|
||||||
|
import android.app.AndroidAppHelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import de.robv.android.xposed.XposedHelpers
|
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
@ -13,13 +13,8 @@ object SysColorsModule : Module() {
|
||||||
private lateinit var context: WeakReference<Context>
|
private lateinit var context: WeakReference<Context>
|
||||||
private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
|
|
||||||
@Deprecated("This method is deprecated in the parent class")
|
|
||||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
override fun buildPayload(builder: JsonObjectBuilder) {
|
||||||
context = WeakReference(runCatching {
|
context = WeakReference(AndroidAppHelper.currentApplication())
|
||||||
XposedHelpers.callStaticMethod(
|
|
||||||
XposedHelpers.findClass("android.app.ActivityThread", null), "currentApplication"
|
|
||||||
) as Context
|
|
||||||
}.getOrNull())
|
|
||||||
val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2")
|
val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2")
|
||||||
val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
|
val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
|
||||||
|
|
||||||
|
|
@ -42,8 +37,7 @@ object SysColorsModule : Module() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertToColor(id: Int): String {
|
private fun convertToColor(id: Int): String {
|
||||||
val ctx = context.get()
|
val clr = if (isSupported()) ContextCompat.getColor(context.get()!!, id) else 0
|
||||||
val clr = if (isSupported() && ctx != null) ContextCompat.getColor(ctx, id) else 0
|
|
||||||
return String.format("#%06X", 0xFFFFFF and clr)
|
return String.format("#%06X", 0xFFFFFF and clr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,8 @@ import android.content.res.Resources
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.MethodHookBuilder
|
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Companion.JSON
|
import GoonXposed.xposed.Utils.Companion.JSON
|
||||||
import GoonXposed.xposed.asFile
|
|
||||||
import GoonXposed.xposed.hook
|
|
||||||
import GoonXposed.xposed.method
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
|
|
@ -46,7 +42,7 @@ object ThemesModule : Module() {
|
||||||
|
|
||||||
private const val THEME_FILE = "current-theme.json"
|
private const val THEME_FILE = "current-theme.json"
|
||||||
|
|
||||||
@Deprecated("This method is deprecated in the parent class")
|
|
||||||
@ExperimentalSerializationApi
|
@ExperimentalSerializationApi
|
||||||
override fun buildPayload(builder: JsonObjectBuilder) {
|
override fun buildPayload(builder: JsonObjectBuilder) {
|
||||||
builder.apply {
|
builder.apply {
|
||||||
|
|
@ -85,20 +81,10 @@ object ThemesModule : Module() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hookTheme() {
|
fun hookTheme() {
|
||||||
try {
|
|
||||||
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt")
|
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt")
|
||||||
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme")
|
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme")
|
||||||
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme")
|
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme")
|
||||||
|
|
||||||
hookThemeInternal(themeManager, darkTheme, lightTheme)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hookThemeInternal(
|
|
||||||
themeManager: Class<*>, darkTheme: Class<*>, lightTheme: Class<*>
|
|
||||||
) {
|
|
||||||
val theme = this.theme ?: return
|
val theme = this.theme ?: return
|
||||||
|
|
||||||
// Apply rawColors
|
// Apply rawColors
|
||||||
|
|
@ -120,7 +106,6 @@ object ThemesModule : Module() {
|
||||||
|
|
||||||
// If there's any rawColors value, hook the color getter
|
// If there's any rawColors value, hook the color getter
|
||||||
if (!theme.data.rawColors.isNullOrEmpty()) {
|
if (!theme.data.rawColors.isNullOrEmpty()) {
|
||||||
try {
|
|
||||||
val getColorCompat = themeManager.getDeclaredMethod(
|
val getColorCompat = themeManager.getDeclaredMethod(
|
||||||
"getColorCompat",
|
"getColorCompat",
|
||||||
Resources::class.java,
|
Resources::class.java,
|
||||||
|
|
@ -134,14 +119,11 @@ object ThemesModule : Module() {
|
||||||
|
|
||||||
val patch = MethodHookBuilder().run {
|
val patch = MethodHookBuilder().run {
|
||||||
before {
|
before {
|
||||||
try {
|
|
||||||
val arg1 = args[0]
|
val arg1 = args[0]
|
||||||
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
|
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
|
||||||
val name = resources.getResourceEntryName(args[1] as Int)
|
val name = resources.getResourceEntryName(args[1] as Int)
|
||||||
|
|
||||||
if (rawColorMap[name] != null) result = rawColorMap[name]
|
if (rawColorMap[name] != null) result = rawColorMap[name]
|
||||||
} catch (_: Throwable) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build()
|
build()
|
||||||
|
|
@ -149,24 +131,17 @@ object ThemesModule : Module() {
|
||||||
|
|
||||||
getColorCompat.hook(patch)
|
getColorCompat.hook(patch)
|
||||||
getColorCompatLegacy.hook(patch)
|
getColorCompatLegacy.hook(patch)
|
||||||
} catch (e: Throwable) {
|
|
||||||
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
|
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
|
||||||
private fun hexStringToColorInt(hexString: String): Int {
|
private fun hexStringToColorInt(hexString: String): Int {
|
||||||
return try {
|
return if (hexString.length == 9) {
|
||||||
if (hexString.length == 9) {
|
|
||||||
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
|
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
|
||||||
val alpha = hexString.substring(7, 9)
|
val alpha = hexString.substring(7, 9)
|
||||||
val rrggbb = hexString.substring(1, 7)
|
val rrggbb = hexString.substring(1, 7)
|
||||||
"#$alpha$rrggbb".toColorInt()
|
"#$alpha$rrggbb".toColorInt()
|
||||||
} else hexString.toColorInt()
|
} else hexString.toColorInt()
|
||||||
} catch (_: Throwable) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
|
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils
|
import GoonXposed.xposed.Utils
|
||||||
import GoonXposed.xposed.getAppInfo
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object AdditionalBridgeMethodsModule : Module() {
|
object AdditionalBridgeMethodsModule : Module() {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import GoonXposed.xposed.BuildConfig
|
||||||
import GoonXposed.xposed.Constants
|
import GoonXposed.xposed.Constants
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.hookMethod
|
|
||||||
import GoonXposed.xposed.method
|
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import android.content.Context
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.hookMethod
|
|
||||||
import GoonXposed.xposed.safeLoadClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks Discord's crash reporting to disable Sentry initialization.
|
* Hooks Discord's crash reporting to disable Sentry initialization.
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import android.content.Context
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import GoonXposed.xposed.Module
|
import GoonXposed.xposed.Module
|
||||||
import GoonXposed.xposed.Utils.Log
|
import GoonXposed.xposed.Utils.Log
|
||||||
import GoonXposed.xposed.hookMethod
|
|
||||||
import GoonXposed.xposed.safeLoadClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
|
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,3 @@ kotlin.code.style=official
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
# Suppress deprecation warning when building with a newer JDK (e.g., Java 21)
|
|
||||||
android.javaCompile.suppressSourceTargetDeprecationWarning=true
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue