Compare commits

..

No commits in common. "master" and "V1.3.1" have entirely different histories.

38 changed files with 426 additions and 2992 deletions

View file

@ -1,67 +1,79 @@
name: Build APK name: Build APK
on: [push] on:
push:
branches: ["**"]
pull_request:
permissions:
contents: read
env:
JAVA_VERSION: 17
jobs: jobs:
build: build:
name: Build and push
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
with: uses: actions/checkout@v4
fetch-depth: 0
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: "temurin" distribution: temurin
java-version: "17" java-version: ${{ env.JAVA_VERSION }}
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Make Gradle executable - name: Make Gradle executable
run: chmod +x ./gradlew run: chmod +x ./gradlew
- name: Build APKs - name: Optional Keystore (if provided)
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
run: | run: |
# Retry the Gradle build up to 3 times to mitigate transient cache/service errors if [ -n "$KEYSTORE" ]; then
ATTEMPT=0 echo "$KEYSTORE" | base64 -d > signing-key.jks
until [ $ATTEMPT -ge 3 ] ls -l signing-key.jks
do else
./gradlew build --no-daemon --stacktrace && break echo "No keystore provided, skipping..."
ATTEMPT=$((ATTEMPT+1))
echo "Gradle build failed, retrying ($ATTEMPT/3)..."
sleep 5
done
if [ $ATTEMPT -ge 3 ]; then
echo "Gradle build failed after 3 attempts"
exit 1
fi fi
- name: Setup directories - name: Assemble Debug and Release
run: env:
mkdir -p /tmp/ShiggyXposedbuild/apks KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
mkdir -p /tmp/ShiggyXposedbuild/tools KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
GRADLE_OPTS: -Dorg.gradle.daemon=false
run: |
if [ -f signing-key.jks ]; then
./gradlew assembleDebug assembleRelease \
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
else
./gradlew assembleDebug assembleRelease
fi
- name: Move debug APK - name: Collect APKs
run: mv ./app/build/outputs/apk/debug/app-debug.apk /tmp/ShiggyXposedbuild/apks run: |
mkdir -p dist
- name: Download uber-apk-signer cp app/build/outputs/apk/debug/*.apk dist/ || true
run: wget -nv "https://github.com/patrickfav/uber-apk-signer/releases/download/v1.2.1/uber-apk-signer-1.2.1.jar" -O /tmp/ShiggyXposedbuild/tools/uber-apk-signer.jar # Prefer signed release if produced
if ls app/build/outputs/apk/release/*-signed.apk >/dev/null 2>&1; then
- name: Sign release APK cp app/build/outputs/apk/release/*-signed.apk dist/
run: java -jar /tmp/ShiggyXposedbuild/tools/uber-apk-signer.jar --apks ./app/build/outputs/apk/release/app-release-unsigned.apk --out /tmp/ShiggyXposedbuild/apks/ else
cp app/build/outputs/apk/release/*.apk dist/
- name: Rename release APK fi
run: mv /tmp/ShiggyXposedbuild/apks/app-release-aligned-debugSigned.apk /tmp/ShiggyXposedbuild/apks/app-release.apk ls -1 dist
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug
path: /tmp/ShiggyXposedbuild/apks/app-debug.apk
- name: Upload All APKs - name: Upload All APKs
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: app-release name: apks
path: /tmp/ShiggyXposedbuild/apks/app-release.apk path: dist/*.apk
if-no-files-found: error

105
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,105 @@
name: Build Release
on:
workflow_dispatch:
inputs:
versionName:
required: true
description: This release's version name
default: "1.0.0"
versionCode:
required: true
description: This release's version code
default: "1000"
permissions:
contents: write
env:
JAVA_VERSION: 17
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: temurin
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Set Version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle.kts
versionCode: ${{ github.event.inputs.versionCode }}
versionName: ${{ github.event.inputs.versionName }}
- name: Commit Version Bump (if changed)
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
git add app/build.gradle.kts
if git diff --cached --quiet; then
echo "No version change."
else
git commit -m "chore: bump version to ${{ github.event.inputs.versionName }} (${{ github.event.inputs.versionCode }})"
git push
fi
- name: Prepare Keystore
run: |
echo "${{ secrets.KEYSTORE }}" | base64 -d > signing-key.jks
ls -l signing-key.jks
- name: Build Universal APKs
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
GRADLE_OPTS: -Dorg.gradle.daemon=false
run: |
./gradlew packageDebugUniversalApk packageReleaseUniversalApk \
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/signing-key.jks \
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
- name: Collect APKs
run: |
mkdir dist
mv app/build/outputs/apk_from_bundle/debug/app-debug-universal.apk dist/app-debug.apk
mv app/build/outputs/apk_from_bundle/release/app-release-universal.apk dist/app-release.apk
ls -l dist
- name: Upload APK Artifacts
uses: actions/upload-artifact@v4
with:
name: release-apks
path: dist/*.apk
if-no-files-found: error
- name: Create Tag and Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag=${{ github.event.inputs.versionCode }}
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
echo "Tag $tag already exists, skipping creation."
else
git tag "$tag"
git push origin "$tag"
fi
gh release view "$tag" >/dev/null 2>&1 || gh release create "$tag" \
--title "${{ github.event.inputs.versionName }}" \
--generate-notes \
dist/app-debug.apk dist/app-release.apk

1
.gitignore vendored
View file

@ -15,4 +15,3 @@ build
local.properties local.properties
app/release/release/ app/release/release/
release release
release.keystore

View file

@ -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
![OTKUSteyler](https://count.getloli.com/@OTKUSteyler?name=OTKUSteyler&theme=miku&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=1)

View file

@ -12,20 +12,8 @@ android {
applicationId = "GoonXposed.xposed" applicationId = "GoonXposed.xposed"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 1358 versionCode = 1220
versionName = "1.3.8" versionName = "1.2.2"
}
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")
} }

View file

@ -13,7 +13,7 @@
<meta-data android:name="xposedmodule" android:value="true" /> <meta-data android:name="xposedmodule" android:value="true" />
<meta-data <meta-data
android:name="xposeddescription" android:name="xposeddescription"
android:value="Xposed module to inject GoonCord into discord" android:value="Xposed module to inject ShiggyCord into discord"
/> />
<meta-data android:name="xposedminversion" android:value="82" /> <meta-data android:name="xposedminversion" android:value="82" />
<meta-data android:name="xposedscope" android:value="com.discord" /> <meta-data android:name="xposedscope" android:value="com.discord" />

View file

@ -46,7 +46,6 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
BlockCrashReportingModule, BlockCrashReportingModule,
LogBoxModule, LogBoxModule,
CacheModule, CacheModule,
PerfPatchesModule,
FontsModule, FontsModule,
ThemesModule, ThemesModule,
SysColorsModule SysColorsModule

View file

@ -2,160 +2,182 @@ 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
/**
* Base class for every Xposed side module.
*
* 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 {
open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
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
* `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
*/
class HookScope(val param: XC_MethodHook.MethodHookParam) {
val thisObject: Any?
get() = param.thisObject
val args: Array<Any?>
get() = param.args
var result: Any?
get() = param.result
set(value) {
param.result = value
}
var throwable: Throwable?
get() = param.throwable
set(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( data class AppInfo(
val name: String, val name: String,
val packageName: String, val packageName: String,
val version: String, val version: String,
val versionCode: Int val versionCode: Long,
) )
fun Context.getAppInfo(): AppInfo { abstract class Module {
val label = applicationInfo.loadLabel(packageManager).toString() /**
val packageInfo = try { * Builds a JSON payload to be injected into the JavaScript context.
packageManager.getPackageInfo(packageName, 0) */
} catch (e: PackageManager.NameNotFoundException) { @Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
null open fun buildPayload(builder: JsonObjectBuilder) {
} }
return AppInfo( /**
name = label, * Called during Zygote initialization.
packageName = packageName, */
version = packageInfo?.versionName ?: "", open fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) {}
versionCode = packageInfo?.versionCode ?: 0
) /**
* 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?
get() = param.result
set(value) {
param.result = value
}
var throwable: Throwable?
get() = param.throwable
set(value) {
param.throwable = value
}
}
} }

View file

@ -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()
} }

View file

@ -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 =

View file

@ -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.

View file

@ -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() }
val download = async { UpdaterModule.downloadScript().join() }
if (!mainScript.exists()) { awaitAll(ready, download)
Log.i("Main script does not exist, downloading before load...")
val download = async { UpdaterModule.downloadScript().join() }
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,25 +100,19 @@ object HookScriptLoaderModule : Module() {
} }
try { try {
val disabledMarker = File(mainScript.parentFile, "${Constants.MAIN_SCRIPT_FILE}.disabled") preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
if (disabledMarker.exists()) {
Log.i("Script loading disabled by marker; skipping preloads, cached bundle and fallback")
} else {
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)
XposedBridge.invokeOriginalMethod( XposedBridge.invokeOriginalMethod(
loadScriptFromAssets, loadScriptFromAssets,
thisObject, thisObject,
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)

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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")
}

View file

@ -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) {
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
}

View file

@ -1,9 +0,0 @@
package GoonXposed.xposed.modules.LogBox
import android.content.Context
object LogBoxNavigation {
fun showRecoveryMenu(context: Context) {
LogBoxMenu.showRecoveryMenu(context)
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -7,7 +7,6 @@ import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
@ -19,7 +18,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 +25,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
@ -356,7 +353,7 @@ object LogBoxModule : Module() {
// Title // Title
val titleView = TextView(context).apply { val titleView = TextView(context).apply {
text = "GoonXposed Recovery" text = "ShiggyXposed Recovery"
textSize = 22f textSize = 22f
setTextColor(colors.onSurface) setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD) typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
@ -418,7 +415,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
} }
} }
@ -468,9 +465,7 @@ object LogBoxModule : Module() {
textSize = 14f textSize = 14f
setTextColor(colors.onSurfaceVariant) setTextColor(colors.onSurfaceVariant)
setPadding(0, 0, 0, dpToPx(context, 24)) setPadding(0, 0, 0, dpToPx(context, 24))
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { lineHeight = dpToPx(context, 20)
lineHeight = dpToPx(context, 20)
}
} }
container.addView(messageView) container.addView(messageView)
@ -618,7 +613,7 @@ object LogBoxModule : Module() {
val config = JSONObject(configFile.readText()) val config = JSONObject(configFile.readText())
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
customLoadUrl.put("enabled", false) customLoadUrl.put("enabled", false)
customLoadUrl.put("url", "http://localhost:4040/gooncord.js") customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
config.put("customLoadUrl", customLoadUrl) config.put("customLoadUrl", customLoadUrl)
configFile.writeText(config.toString()) configFile.writeText(config.toString())
} }
@ -642,14 +637,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,14 +696,14 @@ 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)
container.addView(toggleContainer) container.addView(toggleContainer)
val urlInput = EditText(context).apply { val urlInput = EditText(context).apply {
hint = "http://localhost:4040/gooncord.js" hint = "http://localhost:4040/shiggycord.js"
setTextColor(colors.onSurface) setTextColor(colors.onSurface)
setHintTextColor(colors.onSurfaceVariant) setHintTextColor(colors.onSurfaceVariant)
background = createM3Background(context, colors.surfaceVariant, 12f) background = createM3Background(context, colors.surfaceVariant, 12f)
@ -721,8 +716,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
@ -793,7 +788,7 @@ object LogBoxModule : Module() {
setCustomBundleURL(context, url, true) setCustomBundleURL(context, url, true)
dialog.dismiss() dialog.dismiss()
} else if (!enabled) { } else if (!enabled) {
setCustomBundleURL(context, url.ifEmpty { "http://localhost:4040/gooncord.js" }, false) setCustomBundleURL(context, url.ifEmpty { "http://localhost:4040/shiggycord.js" }, false)
dialog.dismiss() dialog.dismiss()
} else { } else {
Toast.makeText(context, "Please enter a URL", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Please enter a URL", Toast.LENGTH_SHORT).show()
@ -952,7 +947,7 @@ object LogBoxModule : Module() {
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
customLoadUrl.put("enabled", false) customLoadUrl.put("enabled", false)
customLoadUrl.put("url", "http://localhost:4040/gooncord.js") customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
config.put("customLoadUrl", customLoadUrl) config.put("customLoadUrl", customLoadUrl)
configFile.writeText(config.toString()) configFile.writeText(config.toString())
@ -1022,9 +1017,7 @@ object LogBoxModule : Module() {
textSize = 14f textSize = 14f
setTextColor(colors.onSurfaceVariant) setTextColor(colors.onSurfaceVariant)
setPadding(0, 0, 0, dpToPx(context, 24)) setPadding(0, 0, 0, dpToPx(context, 24))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { lineHeight = dpToPx(context, 20)
lineHeight = dpToPx(context, 20)
}
} }
container.addView(messageView) container.addView(messageView)

View file

@ -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)
} }
} }

View file

@ -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)
}
}

View file

@ -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.*

View file

@ -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 }

View file

@ -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) { fontDef.main.keys.map { name ->
install(UserAgent) { agent = Constants.USER_AGENT } async {
install(HttpTimeout) { val url = fontDef.main.getValue(name)
requestTimeoutMillis = 15_000 try {
connectTimeoutMillis = 10_000 Log.i("Downloading $name from $url")
socketTimeoutMillis = 15_000 val file =
} File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() }
} if (file.exists()) return@async
try { val client = HttpClient(CIO) {
fontDef.main.keys.map { name -> install(UserAgent) { agent = Constants.USER_AGENT }
async {
val url = fontDef.main.getValue(name)
try {
val extension = FILE_EXTENSIONS.firstOrNull { url.substringBefore('?').endsWith(it) }
?: FILE_EXTENSIONS[0]
val file = File(fontsDir, "$name$extension").apply { asFile() }
if (file.exists()) return@async
Log.i("Downloading $name from $url")
val response: HttpResponse = client.get(url)
if (response.status == HttpStatusCode.OK) {
file.writeBytes(response.body())
} else {
Log.e("Failed to download fonts ($name from $url): HTTP ${response.status}")
}
} catch (e: Throwable) {
Log.e("Failed to download fonts ($name from $url)", e)
} }
val response: HttpResponse = client.get(url)
if (response.status == HttpStatusCode.OK) file.writeBytes(response.body())
return@async
} catch (e: Throwable) {
Log.e("Failed to download fonts ($name from $url)", e)
} }
}.awaitAll() }
} finally { }.awaitAll()
client.close()
}
} }
} }

View file

@ -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)
} }
} }

View file

@ -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,53 +106,42 @@ 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, Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType, Resources.Theme::class.java,
Resources.Theme::class.java, )
)
val getColorCompatLegacy = themeManager.getDeclaredMethod( val getColorCompatLegacy = themeManager.getDeclaredMethod(
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType "getColorCompat", Context::class.java, Int::class.javaPrimitiveType
) )
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()
} }
getColorCompat.hook(patch) build()
getColorCompatLegacy.hook(patch)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
} }
getColorCompat.hook(patch)
getColorCompatLegacy.hook(patch)
} }
} }
// 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) {

View file

@ -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() {

View file

@ -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
/** /**

View file

@ -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.

View file

@ -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.

View file

@ -20,6 +20,4 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the # Enables namespacing of each library's R class so that its R class includes only the
# 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