Compare commits

...

183 commits

Author SHA1 Message Date
OTKUSteyler
a707e6fd05
Update UpdaterModule.kt
Some checks failed
Build APK / Build and push (push) Failing after 6m51s
2026-09-12 15:55:10 +02:00
OTKUSteyler
d55ed5221c
Update LogBoxModule.kt 2026-09-12 12:52:36 +02:00
OTKUSteyler
a5af0bddb8
Update FontsModule.kt 2026-09-12 12:41:28 +02:00
OTKUSteyler
9611038048
Update FontsModule.kt 2026-09-12 12:33:34 +02:00
OTKUSteyler
06dde39cfd
Update LogBoxModule.kt 2026-09-12 12:25:15 +02:00
OTKUSteyler
69496c1f3c Revert "Fix: Add missing LogBoxModule and onActivity hook - fixes blank plugin/theme tabs"
This reverts commit 1a9fd61ec3.
2026-09-12 11:27:48 +02:00
OTKUSteyler
ce6f201fcd
Update FontsModule.kt 2026-09-12 10:13:49 +02:00
OTKUSteyler
73a93a29bb
Update Main.kt 2026-09-12 10:10:19 +02:00
OTKUSteyler
ee7e30e67c
Update FontsModule.kt 2026-09-12 10:08:55 +02:00
OTKUSteyler
57ebd3daed
Update LogBoxModule.kt 2026-09-11 22:12:32 +02:00
OTKUSteyler
82214b6ed0
Update FontsModule.kt 2026-09-11 21:48:46 +02:00
OTKUSteyler
2b9956d97b
Update LogBoxModule.kt 2026-09-10 17:47:17 +02:00
OTKUSteyler
b2c7e23a57
Update FontsModule.kt 2026-09-10 17:15:27 +02:00
OTKUSteyler
468311bbba
Update LogBoxModule.kt 2026-09-10 17:15:05 +02:00
OTKUSteyler
7f90a61ca5
Delete app/src/main/kotlin/GoonXposed/xposed/xposed/a 2026-09-10 16:58:53 +02:00
OTKUSteyler
17e1b9220f
Create a 2026-09-10 16:57:51 +02:00
OTKUSteyler
34b4a9f30c
Delete app/src/main/kotlin/GoonXposed/xposed/xposed/a 2026-09-10 16:56:10 +02:00
OTKUSteyler
abb1c6ddd8
Create a 2026-09-10 16:55:55 +02:00
OTKUSteyler
32421df002
Update Main.kt 2026-09-10 15:20:01 +02:00
OTKUSteyler
f16af054be
Update Utils.kt 2026-09-10 15:11:45 +02:00
OTKUSteyler
3a6abc9a79
Update FontsModule.kt 2026-09-10 13:54:51 +02:00
OTKUSteyler
a836fd1e73
Update FontsModule.kt 2026-09-10 12:49:20 +02:00
OTKUSteyler
0190e2d468
Update Main.kt 2026-09-10 12:18:30 +02:00
OTKUSteyler
ea21c20d18
Update Main.kt 2026-09-10 12:09:38 +02:00
OTKUSteyler
ad10c8a2a1
Update build.gradle.kts 2026-09-10 08:05:34 +02:00
OTKUSteyler
65d6ae239a
Update build.gradle.kts 2026-09-09 21:16:48 +02:00
OTKUSteyler
415f0066d7
Update build.gradle.kts 2026-09-09 21:13:44 +02:00
OTKUSteyler
4f3c599c3b
Update build.gradle.kts 2026-09-09 20:25:56 +02:00
OTKUSteyler
5e8e4538f8
Update build.gradle.kts 2026-09-09 20:17:37 +02:00
OTKUSteyler
64f83dcd5c
Update FontsModule.kt 2026-09-09 19:52:56 +02:00
OTKUSteyler
cd418c2d79
Update LogBoxActions.kt 2026-09-09 19:51:20 +02:00
OTKUSteyler
0267d095e2
Update FontsModule.kt 2026-09-09 19:42:01 +02:00
OTKUSteyler
2d62630e30
Update Main.kt 2026-09-09 17:45:20 +02:00
OTKUSteyler
750a4cd5b0
Update Main.kt 2026-09-05 20:37:19 +02:00
OTKUSteyler
baacefef54
Update FontsModule.kt 2026-09-05 19:46:48 +02:00
OTKUSteyler
f96d53db9a
Update Main.kt 2026-09-05 13:43:27 +02:00
OTKUSteyler
720e84487e
Update FontsModule.kt 2026-09-05 13:26:30 +02:00
OTKUSteyler
8616f04995
Update ThemesModule.kt 2026-09-05 13:25:51 +02:00
OTKUSteyler
7152b06265
Update Main.kt 2026-09-05 13:25:17 +02:00
OTKUSteyler
2b3510f55d
Update Utils.kt 2026-09-05 12:53:22 +02:00
OTKUSteyler
d1334d0ec0
Update FixResourcesModule.kt 2026-09-05 12:49:09 +02:00
OTKUSteyler
853564ebe9
Update FontsModule.kt 2026-09-05 12:48:11 +02:00
OTKUSteyler
3bf2f76c56
Update RecoveryMenu.kt 2026-09-05 12:47:19 +02:00
OTKUSteyler
6f707af677
Update OptionsMenu.kt 2026-09-05 12:45:27 +02:00
OTKUSteyler
1abaaff7d1
Update PayloadGlobalModule.kt 2026-09-05 12:44:49 +02:00
OTKUSteyler
3aa888bc26
Update HookScriptLoaderModule.kt 2026-09-05 12:44:22 +02:00
OTKUSteyler
d57081364b
Update SysColorsModule.kt 2026-09-05 12:43:35 +02:00
OTKUSteyler
4599c2d96f
Update Utils.kt 2026-09-05 12:43:04 +02:00
OTKUSteyler
14a8067aea
Update Module.kt 2026-09-05 12:42:17 +02:00
OTKUSteyler
d719e15cf8
Add files via upload 2026-09-05 12:25:16 +02:00
OTKUSteyler
7814cd0e82
Add files via upload 2026-09-05 12:24:51 +02:00
OTKUSteyler
469cdfb95e
Add files via upload 2026-09-05 12:24:24 +02:00
OTKUSteyler
e90c20bfc5
Add files via upload 2026-09-05 12:23:41 +02:00
OTKUSteyler
fc9dd1ee9b
Add files via upload 2026-09-05 12:22:25 +02:00
OTKUSteyler
c85761f92e
Add files via upload 2026-09-05 12:21:42 +02:00
OTKUSteyler
c342b478c1
Update gradle.properties 2026-09-05 12:16:21 +02:00
OTKUSteyler
fc6772a124
Update gradle.properties 2026-09-05 12:13:46 +02:00
OTKUSteyler
743b6283c0
Update build.gradle.kts 2026-09-04 06:25:39 +02:00
OTKUSteyler
3562b0e730
Update FontsModule.kt 2026-09-03 21:06:47 +02:00
OTKUSteyler
26a356a158
Update xposed_init 2026-09-03 20:15:40 +02:00
OTKUSteyler
a3ebf0259e
Update xposed_init 2026-09-03 20:12:29 +02:00
OTKUSteyler
df097bfc81
Update build.gradle.kts 2026-09-03 19:35:10 +02:00
OTKUSteyler
765c546c20
Update Module.kt 2026-09-03 18:29:43 +02:00
OTKUSteyler
13bdcdad03
Update FontsModule.kt 2026-09-03 18:27:03 +02:00
OTKUSteyler
b083a66afd
Update FontsModule.kt
Some checks failed
Build APK / Build and push (push) Failing after 3m49s
2026-09-03 17:45:01 +02:00
OTKUSteyler
4115c3d4df
Update build.gradle.kts
Some checks failed
Build APK / Build and push (push) Failing after 3m58s
2026-09-03 15:36:30 +02:00
OTKUSteyler
c63eaa5a12
Update FontsModule.kt 2026-09-03 14:58:44 +02:00
OTKUSteyler
531cb9281c
Update FontsModule.kt 2026-09-03 13:00:51 +02:00
OTKUSteyler
bff796d574
Update FontsModule.kt 2026-09-03 12:45:07 +02:00
OTKUSteyler
f7d1b2ea1d
Update FontsModule.kt 2026-09-03 05:58:15 +02:00
OTKUSteyler
9a620545b4
Update FontsModule.kt 2026-09-02 16:17:06 +02:00
OTKUSteyler
6e7f5302e3
Update build.gradle.kts
Some checks failed
Build APK / Build and push (push) Failing after 4m49s
2026-09-02 15:27:31 +02:00
OTKUSteyler
ec787e7cf5
Update build.gradle.kts 2026-09-02 15:27:08 +02:00
OTKUSteyler
8ef48f3608
Update FontsModule.kt 2026-09-02 14:43:48 +02:00
OTKUSteyler
2bddedb96c
Update FontsModule.kt 2026-09-02 14:41:44 +02:00
OTKUSteyler
5d39748a28 Fix overload resolution ambiguity by replacing wildcard import with specific LogBoxModule import 2026-08-26 16:46:01 +02:00
OTKUSteyler
1a9fd61ec3 Fix: Add missing LogBoxModule and onActivity hook - fixes blank plugin/theme tabs 2026-08-26 16:41:59 +02:00
OTKUSteyler
a9a1dc4ad3 chore: Bump version to 1.3.6 2026-08-26 16:29:21 +02:00
OTKUSteyler
aab9fdc101 Fix: Add @Deprecated annotation to buildPayload override 2026-08-26 16:22:22 +02:00
OTKUSteyler
21029f5f72 Fix: Add @Deprecated annotation to buildPayload override 2026-08-26 16:22:10 +02:00
OTKUSteyler
64ffdf4f32 Fix: Add @Deprecated annotation to buildPayload override 2026-08-26 16:21:48 +02:00
OTKUSteyler
68c85dd812 Fix: Skip release signing when keystore not found and suppress deprecated warnings 2026-08-26 16:21:40 +02:00
OTKUSteyler
fd84dd0320 Fix: Remove invalid method parameters from UpdaterModule calls
- Line 86: Remove `isCustomUrlEnabled` reference (doesn't exist in UpdaterModule)
- Line 92: Remove `showUpdateDialog = false` parameter from `downloadScript()` call
- Line 96: Remove `showUpdateDialog = true` parameter from `downloadScript()` call

The `UpdaterModule.downloadScript()` method only accepts an optional `activity` parameter, not `showUpdateDialog`.
The logic has been simplified to check only if the main script exists.
2026-08-26 16:15:12 +02:00
OTKUSteyler
7df631b799
Update README.md 2026-08-26 16:09:02 +02:00
OTKUSteyler
3d45a8d70f
Merge branch 'kmmiio99o:master' into master 2026-08-26 16:05:36 +02:00
kmmiio99o
af387dc48a
fix: fonts and themes pages not showing up on latest alpha 2026-08-26 15:28:26 +02:00
OTKUSteyler
2978e3b8d1
Merge branch 'kmmiio99o:master' into master 2026-08-26 06:16:01 +02:00
kmmiio99o
32794a134c
chore: bump version to 1.3.5 and manify release 2026-08-25 22:29:27 +02:00
kmmiio99o
13459ef238
fix: hook createAssetTypeface with fallback for older Discord versions 2026-08-25 22:29:16 +02:00
OTKUSteyler
ffcc29796c
Update LogBoxModule.kt 2026-08-18 10:38:53 +02:00
OTKUSteyler
79298ddf6c
Update LogBoxMenu.kt 2026-08-18 10:37:38 +02:00
OTKUSteyler
e0917110e1
Update LogBoxModule.kt 2026-08-18 10:29:36 +02:00
OTKUSteyler
1de8af79f0
Rename package from ShiggyXposed to GoonXposed 2026-08-18 10:20:54 +02:00
OTKUSteyler
5fe307a852
Update and rename Module.kt.txt to Module.kt 2026-08-18 10:19:28 +02:00
OTKUSteyler
08bfa8199d
Add files via upload 2026-08-18 10:18:42 +02:00
OTKUSteyler
2c94757947
Update CacheModule.kt 2026-08-18 10:09:31 +02:00
OTKUSteyler
85837531de
Delete app/src/main/kotlin/GoonXposed/xposed/xposed directory 2026-08-17 09:07:40 +02:00
OTKUSteyler
3360ee33f1
Rename Module.kt to GoonXposed directory 2026-08-17 09:07:18 +02:00
OTKUSteyler
d882007611
Delete app/src/main/kotlin/GoonXposed/xposed/xposed/Utils.kt 2026-08-17 09:06:16 +02:00
OTKUSteyler
bd9116e1cf
Rename Utils.kt to Utils.kt 2026-08-17 09:05:44 +02:00
OTKUSteyler
5e54c7bf61
Update LogBoxModule.kt 2026-08-17 08:58:41 +02:00
OTKUSteyler
bf6f3c0691
Refactor LogBoxModule to use urlEnabled instead of isEnabled 2026-08-17 08:45:26 +02:00
OTKUSteyler
7eb893bcc9
Update build.gradle.kts 2026-08-17 08:35:12 +02:00
OTKUSteyler
52f05764f4 Merge remote-tracking branch 'upstream/master' 2026-08-17 08:25:42 +02:00
kmmiio99o
4900b9533c
fix: put button in the right recovery options 😭 2026-08-16 18:10:12 +02:00
kmmiio99o
ced4c1ce1f
chore: bump to 1.3.4 2026-08-16 17:46:15 +02:00
kmmiio99o
05d6f5004d
feat: add option to delete Devtools.js 2026-08-16 17:46:09 +02:00
OTKUSteyler
cdbbb3a957
Update README.md 2026-05-22 21:51:39 +02:00
OTKUSteyler
457af73f88
Update OptionsMenu.kt 2026-05-18 01:10:23 +02:00
OTKUSteyler
6be3d89176
Update LogBoxUtils.kt 2026-05-18 01:10:02 +02:00
OTKUSteyler
9267cb3918
Update LogBoxTheme.kt 2026-05-18 01:09:28 +02:00
OTKUSteyler
f55d1a4a62
Rename package from ShiggyXposed to GoonXposed 2026-05-18 01:09:06 +02:00
OTKUSteyler
cc0ffe6fb2
Update LogBoxMenu.kt 2026-05-18 01:08:46 +02:00
OTKUSteyler
acca6bfe47
Rename package from ShiggyXposed to GoonXposed 2026-05-18 01:08:09 +02:00
OTKUSteyler
8148f860e5
Update LogBoxConstants.kt 2026-05-18 01:07:46 +02:00
OTKUSteyler
f0e9503354
Update LogBoxComponents.kt 2026-05-18 01:07:24 +02:00
OTKUSteyler
4f779c9af0
Update RecoveryMenu.kt 2026-05-18 01:07:00 +02:00
OTKUSteyler
202152bbf0
Update LogBoxActions.kt 2026-05-18 01:05:58 +02:00
OTKUSteyler
d55b9dcda0
Rename package from ShiggyXposed to GoonXposed 2026-05-18 01:05:28 +02:00
OTKUSteyler
6f6f2e4abc
Update CustomBundleDialog.kt 2026-05-18 01:05:06 +02:00
OTKUSteyler
c3c21a26e0 Merge remote-tracking branch 'upstream/master' 2026-05-18 01:01:03 +02:00
kmmiio99o
b62f7b4daa
chore: bump to 1.3.3 2026-05-15 22:08:37 +02:00
kmmiio99o
1e61dd9832
feat: fallback to https://bundle.shiggycord.dev
also add siging release apk's
2026-05-15 22:05:42 +02:00
OTKUSteyler
5e2fd369fe Merge remote-tracking branch 'upstream/master' 2026-02-14 21:00:53 +01:00
kmmiio99o
a9bac3f1cb
chore: bump to 1.3.2 2026-02-14 16:44:29 +01:00
kmmiio99o
f3cc5b4e2c
Merge remote-tracking branch 'upstream/master' 2026-02-14 16:34:45 +01:00
cocobo1
7f2d4991c5 Merge branch 'main' of https://github.com/revenge-mod/revenge-xposed 2026-02-10 14:43:19 +02:00
GitHub Actions
c6667c2307 chore: bump version to 1.4.10 (1410) 2026-02-01 11:11:17 +00:00
Palm
a386e1907f
perf(loader): don't block startup on bundle update if cached (#19) 2026-02-01 17:55:54 +07:00
kmmiio99o
8ddd3a336e
chore: bump to 1.3.1 2026-01-31 21:49:01 +01:00
kmmiio99o
2a223671b9
fix: not being able to enable bundle injection 2026-01-31 21:48:43 +01:00
kmmiio99o
1f8ddf4176
chore: bump to 1.3.0 2026-01-31 20:28:57 +01:00
kmmiio99o
82ea78edbb feat(xposed/modules/LogBox/LogBoxMenu.kt): Reload app after
Disabling/Enabling bundle injection
2026-01-31 20:26:27 +01:00
kmmiio99o
a85f399b29 Merge remote-tracking branch 'upstream/master' 2026-01-31 20:23:09 +01:00
kmmiio99o
be86071f4f
feat: refactor LogBoxModule file stracture 2026-01-31 20:15:11 +01:00
Purple_Ξye™
d9603ffdb5 fix(modules/HookScriptLoaderModule): don't use cache when custom bundle URL is enabled 2026-01-30 05:09:47 +01:00
Purple_Ξye™
87d4c78c67 feat(modules/UpdaterModule): show update dialog when bundle is updated in background 2026-01-29 08:39:24 +01:00
Purple_Ξye™
da337dfd98 feat(modules/UpdaterModule): add methods to force bundle update 2026-01-29 08:38:57 +01:00
Purple_Ξye™
e0c75e30af perf(modules/HookScriptLoaderModule): don't block startup on bundle update if cached 2026-01-28 22:00:59 +01:00
cocobo1
bf0389fe99 feat: hermes bytecode support 2026-01-11 16:34:38 +02:00
OTKUSteyler
7635ac1d90
Merge branch 'kmmiio99o:master' into master 2026-01-10 16:32:15 +00:00
kmmiio99o
179db56473
Merge remote-tracking branch 'upstream/master' 2026-01-10 17:29:00 +01:00
cocobo1
bc4cfa0265 chore: bump version 2026-01-10 17:47:50 +02:00
cocobo1
3ba04c366c feat: Add option to disable bundle injection (#2) 2026-01-06 17:12:55 +01:00
kmmiio99o
52bcb9912b
feat: Add option to disable bundle injection
why? People can disable bundle injection when Kettu gets stuck
2026-01-06 13:33:41 +01:00
OTKUSteyler
5769fe2211
Update CacheModule.kt 2026-01-03 00:24:07 +00:00
OTKUSteyler
04a799e7f6
Update BridgeModule.kt 2026-01-03 00:23:02 +00:00
OTKUSteyler
ed8d5fb7a8
Update BridgeModule.kt 2026-01-03 00:06:52 +00:00
OTKUSteyler
8038be3023
Update gradle.properties 2026-01-03 00:05:33 +00:00
OTKUSteyler
511fd1df4e
Update PerfPatchesModule.kt 2026-01-03 00:01:21 +00:00
OTKUSteyler
0b63dbbc9a
Update CacheModule.kt 2026-01-02 23:58:25 +00:00
OTKUSteyler
85290a619e Merge remote-tracking branch 'upstream/master' 2026-01-03 00:53:48 +01:00
kmmiio99o
04a68d623d
suppress deprecated java version 2026-01-02 15:16:18 +01:00
kmmiio99o
0805a7314d
add some more patches for faster start up 2026-01-02 15:08:43 +01:00
kmmiio99o
7726840d75
Merge remote-tracking branch 'upstream/master' 2026-01-02 15:06:47 +01:00
kmmiio99o
9895188f24
Merge branch 'master' of https://github.com/ShiggyCord-Team/ShiggyXposed 2025-12-22 17:04:13 +01:00
kmmiio99o
fdca98a994
bump to 1.2.4 2025-12-22 17:04:10 +01:00
kmmiio99o
86994eefb9 bump to 1.2.4 2025-12-20 21:28:30 +01:00
kmmiio99o
59b5d75f6f Patch some things on discord startup to make the app even faster 2025-12-20 20:41:59 +01:00
cocobo1
27e4917c34 Merge pull request 'Make Xposed Recovery Menu look better :P' (#1) from kmmiio99o/KettuXposed:master into master
Reviewed-on: https://codeberg.org/cocobo1/KettuXposed/pulls/1
2025-12-19 23:05:36 +01:00
kmmiio99o
85795c69b9 Make Xposed Recovery look better :P 2025-12-19 22:51:44 +01:00
kmmiio99o
de57766e21 rename forgotten things 2025-12-11 18:46:05 +01:00
kmmiio99o
f8fe4f03a9 Update README file
I don't know, just better documentation
2025-12-11 18:45:08 +01:00
OTKUSteyler
d1796e4c68 Merge remote-tracking branch 'upstream/master' 2025-12-11 18:29:19 +01:00
kmmiio99o
6eef736615 Merge branch 'master' of https://github.com/ShiggyCord-Team/ShiggyXposed 2025-12-11 18:18:39 +01:00
kmmiio99o
8a134d7ee1 Try to fix size of the Recovery menu first page 2025-12-11 18:18:05 +01:00
kmmiio99o
8bc7c1671d Fix width in recovery menu and remove some buttons 2025-12-11 18:03:04 +01:00
kmmiio99o
2b1a7919a3 feat: Add Themes submenu and segmented appearance selector
Introduce Themes submenu with segmented appearance selector and flavor
picker for LogBox menu. Move Refetch and Clear Cache from Recovery into
Options. Apply menu colors in-place via currentMenuContainer
2025-12-11 17:33:31 +01:00
kmmiio99o
035f8aa570 Fix: fix disabling and enabling bundle injection 2025-12-11 16:40:57 +01:00
kmmiio99o
5876b7da10 Add options menu in LogBox module and bump to 1.2.3
Add options menu in xposed settings to disabling injecting bundle,
change Xposed menu flavor, and choose theme from dark to light mode and
system detection
2025-12-11 16:30:57 +01:00
OTKUSteyler
f92e36e7d4
Merge branch 'kmmiio99o:master' into master 2025-12-10 07:01:53 +01:00
OTKUSteyler
cbf96e8b45
Update README.md 2025-12-07 23:38:33 +01:00
OTKUSteyler
4537e7d56c
Update README.md 2025-12-07 23:37:35 +01:00
kmmiio99o
33faa383b0 A DOT 2025-12-07 23:36:32 +01:00
OTKUSteyler
782c40b5d3
Merge branch 'kmmiio99o:master' into master 2025-12-07 23:36:03 +01:00
kmmiio99o
224ca9997a
Add readme
Add nice readme with useful info for Xposed Module and about it
2025-12-07 23:33:41 +01:00
GitHub Actions
5c29b1072b chore: bump version to 1.4.9 (1409) 2025-11-23 22:03:50 +00:00
PalmDevs
38c7082760
fix(modules/no_track/BlockCrashReportingModule): support 307204+
Discord pushed an update that changed some logic for the setTag method. Previously it only asserted initialization if isDisabled returns false, now it asserts initialization regardless.

The init method sets the initialization latch and returns if crash reporting is disabled, so we don't need to hook that anymore.
2025-11-22 14:49:17 +07:00
GitHub Actions
c20cf01f6b chore: bump version to 1.4.8 (1408) 2025-11-21 09:44:30 +00:00
PalmDevs
4a8df8954e
fix(modules/no_track/BlockCrashReportingModule): make backwards compatible 2025-11-21 16:36:14 +07:00
GitHub Actions
f49ea13da5 chore: bump version to 1.4.7 (1407) 2025-11-21 06:17:17 +00:00
PalmDevs
06d6778c45
fix(modules/no_track/BlockCrashReportingModule): also hook isDisabled to true
Prevents Discord from crashing when trying to set a Sentry tag.
2025-11-21 13:04:36 +07:00
GitHub Actions
9f1941e974 chore: bump version to 1.4.6 (1406) 2025-11-18 13:02:47 +00:00
35 changed files with 2937 additions and 259 deletions

1
.gitignore vendored
View file

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

40
README.md Normal file
View file

@ -0,0 +1,40 @@
# 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,8 +12,20 @@ android {
applicationId = "GoonXposed.xposed"
minSdk = 24
targetSdk = 36
versionCode = 1330
versionName = "1.3.3"
versionCode = 1358
versionName = "1.3.8"
}
signingConfigs {
create("release") {
val keystoreFile = file("../release.keystore")
if (keystoreFile.exists()) {
storeFile = keystoreFile
storePassword = "goonxposed"
keyAlias = "goonxposed"
keyPassword = "goonxposed"
}
}
}
buildTypes {
@ -24,7 +36,11 @@ android {
}
release {
isDebuggable = false
isMinifyEnabled = false
isMinifyEnabled = true
val keystoreFile = file("../release.keystore")
if (keystoreFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
}
}
compileOptions {
@ -39,6 +55,10 @@ android {
}
}
tasks.withType<org.gradle.api.tasks.compile.JavaCompile>().configureEach {
options.compilerArgs.add("-Xlint:-options")
}
dependencies {
compileOnly("de.robv.android.xposed:api:82")
@ -48,4 +68,6 @@ dependencies {
implementation("io.ktor:ktor-client-core:3.3.1")
implementation("io.ktor:ktor-client-cio:3.3.1")
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

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

View file

@ -2,171 +2,45 @@ package GoonXposed.xposed
import android.app.Activity
import android.content.Context
import android.os.Build
import android.content.pm.PackageManager
import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import kotlinx.serialization.json.JsonObjectBuilder
import java.io.File
import java.lang.reflect.Method
data class AppInfo(
val name: String,
val packageName: String,
val version: String,
val versionCode: Long,
)
abstract class Module {
/**
* Builds a JSON payload to be injected into the JavaScript context.
*/
@Deprecated("This will be removed in future versions. Payloads can be replaced via synchronous bridge methods.")
open fun buildPayload(builder: JsonObjectBuilder) {
}
/**
* Called during Zygote initialization.
/**
* 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) {}
/**
* 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()
@Deprecated("This method is deprecated in the parent class")
open fun buildPayload(builder: JsonObjectBuilder) {}
}
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.
/**
* Runtime wrapper around an [XC_MethodHook.MethodHookParam] used as the receiver of every
* `before`/`after` hook block, exposing [thisObject], [args], [result] and [param] directly.
*/
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)
class HookScope(val param: XC_MethodHook.MethodHookParam) {
val thisObject: Any?
get() = param.thisObject
val thisObject: Any? get() = param.thisObject
val args: Array<Any?> get() = param.args
val args: Array<Any?>
get() = param.args
var result: Any?
get() = param.result
@ -179,5 +53,109 @@ abstract class Module {
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(
val name: String,
val packageName: String,
val version: String,
val versionCode: Int
)
fun Context.getAppInfo(): AppInfo {
val label = applicationInfo.loadLabel(packageManager).toString()
val packageInfo = try {
packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}
return AppInfo(
name = label,
packageName = packageName,
version = packageInfo?.versionName ?: "",
versionCode = packageInfo?.versionCode ?: 0
)
}

View file

@ -1,9 +1,10 @@
package GoonXposed.xposed
import android.app.AlertDialog
import android.app.AndroidAppHelper
import android.app.Application
import android.content.Context
import android.content.Intent
import de.robv.android.xposed.XposedHelpers
import GoonXposed.xposed.modules.UpdaterModule
import kotlinx.serialization.json.Json
import java.io.File
@ -14,7 +15,9 @@ class Utils {
val JSON = Json { ignoreUnknownKeys = true }
fun reloadApp() {
val application = AndroidAppHelper.currentApplication()
val activityThread = XposedHelpers.findClass("android.app.ActivityThread", null)
val application =
XposedHelpers.callStaticMethod(activityThread, "currentApplication") as? Application ?: return
val intent = application.packageManager.getLaunchIntentForPackage(application.packageName)
application.startActivity(Intent.makeRestartActivityTask(intent!!.component))
exitProcess(0)
@ -37,11 +40,6 @@ class Utils {
reloadApp()
}
2 -> {
UpdaterModule.resetLoaderConfig(context)
reloadApp()
}
}
}.show()
}

View file

@ -4,6 +4,9 @@ import android.content.Context
import android.util.AtomicFile
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asDir
import GoonXposed.xposed.asFile
import GoonXposed.xposed.getAppInfo
import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.*
@ -35,7 +38,7 @@ import java.io.*
*/
object CacheModule : Module() {
private const val CACHE_DIR = "revenge"
private const val CACHE_DIR = "Goon"
private const val MODULES_CACHE_PREFIX = "modules"
private const val ASSETS_CACHE_PREFIX = "assets"
@ -45,18 +48,18 @@ object CacheModule : Module() {
private lateinit var assetsCache: AssetsCache
override fun onContext(context: Context) = with(context) {
val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
val ShiggyCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
val (_, _, _, versionCode) = getAppInfo()
val modulesCacheFile = File(
revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
ShiggyCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
).apply { asFile() }
val assetsCacheFile = File(
revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
ShiggyCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
).apply { asFile() }
BridgeModule.registerMethod("revenge.caches.modules.read") {
BridgeModule.registerMethod("Shiggy.caches.modules.read") {
if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile)
?.let {
modulesCache = it
@ -64,7 +67,7 @@ object CacheModule : Module() {
}
}
BridgeModule.registerMethod("revenge.caches.modules.write") {
BridgeModule.registerMethod("Shiggy.caches.modules.write") {
val (blacklist, finds) = it
@Suppress("UNCHECKED_CAST")
modulesCache = ModulesCache(
@ -73,7 +76,7 @@ object CacheModule : Module() {
Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})")
}
BridgeModule.registerMethod("revenge.caches.assets.read") {
BridgeModule.registerMethod("Shiggy.caches.assets.read") {
if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile)
?.let {
assetsCache = it
@ -81,7 +84,7 @@ object CacheModule : Module() {
}
}
BridgeModule.registerMethod("revenge.caches.assets.write") {
BridgeModule.registerMethod("Shiggy.caches.assets.write") {
val (data) = it
@Suppress("UNCHECKED_CAST")
assetsCache =

View file

@ -1,9 +1,11 @@
package GoonXposed.xposed.modules
import GoonXposed.xposed.hookMethod
import android.content.res.Resources
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
import GoonXposed.xposed.Module
import GoonXposed.xposed.hookMethod
/**
* Hooks [Resources.getIdentifier] to fix resource package name mismatch.

View file

@ -6,9 +6,15 @@ import de.robv.android.xposed.IXposedHookZygoteInit
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants
import GoonXposed.xposed.HookScope
import GoonXposed.xposed.HookStateHolder
import GoonXposed.xposed.Module
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 kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -54,11 +60,11 @@ object HookScriptLoaderModule : Module() {
private fun hook(instance: Class<*>) = runCatching {
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(
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
)
loadScriptFromAssets.hook {
@ -83,9 +89,16 @@ object HookScriptLoaderModule : Module() {
runBlocking {
val ready = async { HookStateHolder.readyDeferred.join() }
val download = async { UpdaterModule.downloadScript().join() }
if (!mainScript.exists()) {
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]
@ -100,10 +113,15 @@ object HookScriptLoaderModule : Module() {
}
try {
val disabledMarker = File(mainScript.parentFile, "${Constants.MAIN_SCRIPT_FILE}.disabled")
if (disabledMarker.exists()) {
Log.i("Script loading disabled by marker; skipping preloads, cached bundle and fallback")
} else {
preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile)
if (mainScript.exists()) runScriptFile(mainScript)
else {
if (mainScript.exists()) {
runScriptFile(mainScript)
} else {
Log.i("Main script does not exist, falling back")
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
@ -114,6 +132,7 @@ object HookScriptLoaderModule : Module() {
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
)
}
}
} catch (e: Throwable) {
Log.e("Unable to run scripts:", e)
}

View file

@ -0,0 +1,170 @@
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

@ -0,0 +1,35 @@
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

@ -0,0 +1,211 @@
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

@ -0,0 +1,257 @@
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

@ -0,0 +1,35 @@
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

@ -0,0 +1,74 @@
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

@ -0,0 +1,795 @@
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

@ -0,0 +1,205 @@
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

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

View file

@ -0,0 +1,95 @@
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

@ -0,0 +1,121 @@
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

@ -0,0 +1,57 @@
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

@ -0,0 +1,143 @@
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

@ -19,6 +19,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
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.XposedBridge
import de.robv.android.xposed.XposedHelpers
@ -26,6 +27,7 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.reloadApp
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hook
import kotlinx.coroutines.*
import org.json.JSONObject
import java.io.File
@ -416,7 +418,7 @@ object LogBoxModule : Module() {
dialog.show()
Log.e("Recovery menu shown successfully")
} catch (e: Exception) {
Log.e("Error showing recovery menu: ${e.message}", e)
Log.e("Error showing recovery menu: ${e.message}")
throw e
}
}
@ -640,14 +642,14 @@ object LogBoxModule : Module() {
val filesDir = File(context.filesDir, "pyoncord")
val configFile = File(filesDir, "loader.json")
var currentUrl: String? = null
var isEnabled = false
var urlEnabled = false
if (configFile.exists()) {
try {
val json = JSONObject(configFile.readText())
val customLoadUrl = json.optJSONObject("customLoadUrl")
if (customLoadUrl != null) {
isEnabled = customLoadUrl.optBoolean("enabled", false)
urlEnabled = customLoadUrl.optBoolean("enabled", false)
currentUrl = customLoadUrl.optString("url", "")
}
} catch (_: Exception) {
@ -699,7 +701,7 @@ object LogBoxModule : Module() {
)
}
val toggleSwitch = createM3Switch(context, colors, isEnabled)
val toggleSwitch = createM3Switch(context, colors, urlEnabled)
toggleContainer.addView(toggleLabel)
toggleContainer.addView(toggleSwitch)
@ -719,8 +721,8 @@ object LogBoxModule : Module() {
dpToPx(context, 16),
dpToPx(context, 12)
)
isEnabled
alpha = if (isEnabled) 1f else 0.5f
this.isEnabled = urlEnabled
alpha = if (urlEnabled) 1f else 0.5f
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT

View file

@ -6,8 +6,12 @@ import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module
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 kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
@ -48,7 +52,7 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
"loadScriptFromFile",
String::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
Boolean::class.javaPrimitiveType!!
), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2])
)
@ -70,11 +74,11 @@ class PayloadGlobalModule(private val modules: List<Module>) : Module() {
private fun hook(instance: Class<*>, hook: XC_MethodHook) {
instance.method(
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType,
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType!!,
).hook(hook)
instance.method(
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType!!
).hook(hook)
}
}

View file

@ -0,0 +1,317 @@
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,6 +4,7 @@ import android.content.Context
import android.util.AtomicFile
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.asFile
import GoonXposed.xposed.modules.bridge.BridgeModule
import java.io.*

View file

@ -48,6 +48,8 @@ object UpdaterModule : Module() {
private lateinit var cacheDir: File
private lateinit var bundle: File
private lateinit var etag: File
// Path to the application's data directory so we can read LogBox settings
private lateinit var appDataDir: File
private const val TIMEOUT_CACHED = 5000L
private const val TIMEOUT = 10000L
@ -55,9 +57,11 @@ object UpdaterModule : Module() {
private const val CONFIG_FILE = "loader.json"
private const val DEFAULT_BUNDLE_URL =
"https://github.com/OTKUSteyler/GoonCord/releases/download/latest/gooncord.js"
"https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js"
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
// store app data dir for later checks (LogBox settings live under files/logbox)
appDataDir = File(appInfo.dataDir)
cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() }
val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() }
@ -75,6 +79,23 @@ object UpdaterModule : Module() {
fun downloadScript(activity: Activity? = null): Job = scope.launch {
try {
// Respect LogBox setting to disable bundle injections.
// If the file "files/logbox/LOGBOX_SETTINGS" contains the flag bundleInjectionDisabled=true,
// skip attempting to download/update the bundle.
try {
val settingsFile = File(appDataDir, "files/logbox/LOGBOX_SETTINGS")
if (settingsFile.exists()) {
val txt = settingsFile.readText()
// look for a simple true marker; this avoids introducing a new JSON dependency here
if (txt.contains("\"bundleInjectionDisabled\"") && txt.contains("true")) {
Log.i("UpdaterModule: bundle injection disabled by LogBox settings — skipping download")
return@launch
}
}
} catch (ignore: Exception) {
// If anything goes wrong reading the setting, proceed with normal behaviour.
}
HttpClient(CIO) {
expectSuccess = false
install(UserAgent) { agent = Constants.USER_AGENT }

View file

@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package GoonXposed.xposed.modules.appearance
import android.content.res.AssetManager
@ -27,6 +29,14 @@ import kotlinx.serialization.json.put
import java.io.File
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
data class FontDefinition(
val name: String? = null,
@ -52,7 +62,7 @@ object FontsModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
XposedHelpers.findAndHookMethod(
"com.facebook.react.views.text.ReactFontManager\$Companion",
"com.facebook.react.common.assets.ReactFontManager\$Companion",
classLoader,
"createAssetTypeface",
String::class.java,
@ -93,28 +103,40 @@ object FontsModule : Module() {
// These files should be downloaded by the JS side, but oh well
CoroutineScope(Dispatchers.IO).launch {
val client = HttpClient(CIO) {
install(UserAgent) { agent = Constants.USER_AGENT }
install(HttpTimeout) {
requestTimeoutMillis = 15_000
connectTimeoutMillis = 10_000
socketTimeoutMillis = 15_000
}
}
try {
fontDef.main.keys.map { name ->
async {
val url = fontDef.main.getValue(name)
try {
Log.i("Downloading $name from $url")
val file =
File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() }
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
val client = HttpClient(CIO) {
install(UserAgent) { agent = Constants.USER_AGENT }
}
Log.i("Downloading $name from $url")
val response: HttpResponse = client.get(url)
if (response.status == HttpStatusCode.OK) file.writeBytes(response.body())
return@async
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)
}
}
}.awaitAll()
} finally {
client.close()
}
}
}

View file

@ -1,10 +1,10 @@
package GoonXposed.xposed.modules.appearance
import android.R.color
import android.app.AndroidAppHelper
import android.content.Context
import android.os.Build
import androidx.core.content.ContextCompat
import de.robv.android.xposed.XposedHelpers
import GoonXposed.xposed.Module
import kotlinx.serialization.json.*
import java.lang.ref.WeakReference
@ -13,8 +13,13 @@ object SysColorsModule : Module() {
private lateinit var context: WeakReference<Context>
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) {
context = WeakReference(AndroidAppHelper.currentApplication())
context = WeakReference(runCatching {
XposedHelpers.callStaticMethod(
XposedHelpers.findClass("android.app.ActivityThread", null), "currentApplication"
) as Context
}.getOrNull())
val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2")
val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
@ -37,7 +42,8 @@ object SysColorsModule : Module() {
}
private fun convertToColor(id: Int): String {
val clr = if (isSupported()) ContextCompat.getColor(context.get()!!, id) else 0
val ctx = context.get()
val clr = if (isSupported() && ctx != null) ContextCompat.getColor(ctx, id) else 0
return String.format("#%06X", 0xFFFFFF and clr)
}
}

View file

@ -5,8 +5,12 @@ import android.content.res.Resources
import androidx.core.graphics.toColorInt
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Constants
import GoonXposed.xposed.MethodHookBuilder
import GoonXposed.xposed.Module
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.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
@ -42,7 +46,7 @@ object ThemesModule : Module() {
private const val THEME_FILE = "current-theme.json"
@Deprecated("This method is deprecated in the parent class")
@ExperimentalSerializationApi
override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply {
@ -81,10 +85,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")
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
// Apply rawColors
@ -106,6 +120,7 @@ object ThemesModule : Module() {
// If there's any rawColors value, hook the color getter
if (!theme.data.rawColors.isNullOrEmpty()) {
try {
val getColorCompat = themeManager.getDeclaredMethod(
"getColorCompat",
Resources::class.java,
@ -119,11 +134,14 @@ object ThemesModule : Module() {
val patch = MethodHookBuilder().run {
before {
try {
val arg1 = args[0]
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
val name = resources.getResourceEntryName(args[1] as Int)
if (rawColorMap[name] != null) result = rawColorMap[name]
} catch (_: Throwable) {
}
}
build()
@ -131,17 +149,24 @@ object ThemesModule : Module() {
getColorCompat.hook(patch)
getColorCompatLegacy.hook(patch)
} catch (e: Throwable) {
GoonXposed.xposed.Utils.Log.e("Failed to hook getColorCompat: ${e.message}")
}
}
}
// Parse HEX colour string to INT. Takes "#RRGGBBAA" or "#RRGGBB"
private fun hexStringToColorInt(hexString: String): Int {
return if (hexString.length == 9) {
return try {
if (hexString.length == 9) {
// Rearrange RRGGBBAA -> AARRGGBB so parseColor() is happy
val alpha = hexString.substring(7, 9)
val rrggbb = hexString.substring(1, 7)
"#$alpha$rrggbb".toColorInt()
} else hexString.toColorInt()
} catch (_: Throwable) {
0
}
}
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {

View file

@ -9,6 +9,7 @@ import android.os.Build
import android.widget.Toast
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils
import GoonXposed.xposed.getAppInfo
import java.io.File
object AdditionalBridgeMethodsModule : Module() {

View file

@ -5,6 +5,8 @@ import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.method
import java.lang.reflect.Method
/**

View file

@ -4,6 +4,8 @@ import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.safeLoadClass
/**
* Hooks Discord's crash reporting to disable Sentry initialization.

View file

@ -4,6 +4,8 @@ import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.hookMethod
import GoonXposed.xposed.safeLoadClass
/**
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.

View file

@ -21,3 +21,5 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# Suppress deprecation warning when building with a newer JDK (e.g., Java 21)
android.javaCompile.suppressSourceTargetDeprecationWarning=true