fix: modern Discord themes, fonts, package hook filter, and bundle update
Some checks failed
Build APK / Build and push (push) Failing after 4m10s

This commit is contained in:
Rosie Val 2026-09-19 19:00:04 -04:00
parent b9980f5cb9
commit 8055dbf3ff
5 changed files with 111 additions and 95 deletions

File diff suppressed because one or more lines are too long

View file

@ -60,10 +60,15 @@ class Main : Module(), IXposedHookLoadPackage, IXposedHookZygoteInit {
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
for (module in modules) module.onInit(startupParam)
}
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) = with(param) {
if (packageName != Constants.TARGET_PACKAGE) return
if (processName != Constants.TARGET_PACKAGE) return
val isTargetApp = packageName == Constants.TARGET_PACKAGE
|| packageName.startsWith("com.discord")
|| packageName == "dev.shiggy.cord"
|| packageName.contains("cord")
|| packageName.contains("discord")
if (!isTargetApp) return
if (processName != packageName) return
if (hooked) return
val reactActivity = try {

View file

@ -59,24 +59,9 @@ object LogBoxModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
this@LogBoxModule.packageParam = packageParam
runCatching {
val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost")
val getUseDeveloperSupportMethod =
dcdReactNativeHostClass.declaredMethods.firstOrNull { it.name == "getUseDeveloperSupport" }
?: dcdReactNativeHostClass.methods.firstOrNull { it.name == "getUseDeveloperSupport" }
if (getUseDeveloperSupportMethod != null) {
getUseDeveloperSupportMethod.isAccessible = true
XposedBridge.hookMethod(getUseDeveloperSupportMethod, object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
param.result = true
}
})
Log.i("Successfully hooked DCDReactNativeHost")
}
}.onFailure { e ->
Log.w("Could not hook DCDReactNativeHost: ${e.message}")
}
// Do not force getUseDeveloperSupport = true, as it causes React Native to loop
// attempting to connect to a Metro bundler on localhost:8081 and hang the app loading screen.
// Shake detector and recovery menu work independently without it.
return@with
}

View file

@ -61,9 +61,15 @@ object FontsModule : Module() {
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val fontManagerClass = runCatching {
classLoader.loadClass("com.facebook.react.common.assets.ReactFontManager\$Companion")
}.getOrNull() ?: runCatching {
classLoader.loadClass("com.facebook.react.views.text.ReactFontManager\$Companion")
}.getOrNull()
if (fontManagerClass != null) {
XposedHelpers.findAndHookMethod(
"com.facebook.react.common.assets.ReactFontManager\$Companion",
classLoader,
fontManagerClass,
"createAssetTypeface",
String::class.java,
Int::class.java,
@ -76,6 +82,9 @@ object FontsModule : Module() {
return createAssetTypeface(fontFamilyName, style, assetManager)
}
})
} else {
Log.w("FontsModule: ReactFontManager\$Companion class not found")
}
val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() }
if (!fontDefFile.exists()) return@with
@ -149,13 +158,17 @@ object FontsModule : Module() {
// for use in the CustomFallbackBuilder below.
for (fontFamilyName in fontFamilyNames) {
try {
if (fontFamilyName.contains(":")) {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
if (file.exists()) {
val font = Font.Builder(file).build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
}
}
}
} catch (_: Throwable) {
}
@ -218,12 +231,15 @@ object FontsModule : Module() {
val extension = EXTENSIONS[style]
try {
if (fontFamilyName.contains(":")) {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
if (!file.exists()) throw Exception()
if (file.exists()) {
return Typeface.createFromFile(file.absolutePath)
}
}
}
} catch (_: Throwable) {
}

View file

@ -86,18 +86,20 @@ object ThemesModule : Module() {
fun hookTheme() {
try {
val themeManager = param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt")
val darkTheme = param.classLoader.loadClass("com.discord.theme.DarkerTheme")
val lightTheme = param.classLoader.loadClass("com.discord.theme.LightTheme")
val themeManager = runCatching { param.classLoader.loadClass("com.discord.theme.utils.ColorUtilsKt") }.getOrNull()
val darkTheme = runCatching { param.classLoader.loadClass("com.discord.theme.DarkTheme") }.getOrNull()
?: runCatching { param.classLoader.loadClass("com.discord.theme.DarkerTheme") }.getOrNull()
val onyxTheme = runCatching { param.classLoader.loadClass("com.discord.theme.OnyxTheme") }.getOrNull()
val lightTheme = runCatching { param.classLoader.loadClass("com.discord.theme.LightTheme") }.getOrNull()
hookThemeInternal(themeManager, darkTheme, lightTheme)
hookThemeInternal(themeManager, darkTheme, onyxTheme, lightTheme)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook theme: ${e.message}")
}
}
private fun hookThemeInternal(
themeManager: Class<*>, darkTheme: Class<*>, lightTheme: Class<*>
themeManager: Class<*>?, darkTheme: Class<*>?, onyxTheme: Class<*>?, lightTheme: Class<*>?
) {
val theme = this.theme ?: return
@ -111,26 +113,36 @@ object ThemesModule : Module() {
// TEXT_NORMAL -> getTextNormal
val methodName = "get${key.fromScreamingSnakeToCamelCase()}"
value.forEachIndexed { index, v ->
val colorInt = hexStringToColorInt(v)
when (index) {
0 -> hookThemeMethod(darkTheme, methodName, hexStringToColorInt(v))
1 -> hookThemeMethod(lightTheme, methodName, hexStringToColorInt(v))
0 -> {
if (darkTheme != null) hookThemeMethod(darkTheme, methodName, colorInt)
if (onyxTheme != null) hookThemeMethod(onyxTheme, methodName, colorInt)
}
1 -> {
if (lightTheme != null) hookThemeMethod(lightTheme, methodName, colorInt)
}
}
}
}
// If there's any rawColors value, hook the color getter
if (!theme.data.rawColors.isNullOrEmpty()) {
if (!theme.data.rawColors.isNullOrEmpty() && themeManager != null) {
try {
val getColorCompat = themeManager.getDeclaredMethod(
val getColorCompat = runCatching {
themeManager.getDeclaredMethod(
"getColorCompat",
Resources::class.java,
Int::class.javaPrimitiveType,
Resources.Theme::class.java,
)
}.getOrNull()
val getColorCompatLegacy = themeManager.getDeclaredMethod(
val getColorCompatLegacy = runCatching {
themeManager.getDeclaredMethod(
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
)
}.getOrNull()
val patch = MethodHookBuilder().run {
before {
@ -147,8 +159,8 @@ object ThemesModule : Module() {
build()
}
getColorCompat.hook(patch)
getColorCompatLegacy.hook(patch)
getColorCompat?.hook(patch)
getColorCompatLegacy?.hook(patch)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
}