Compare commits

...

95 commits

Author SHA1 Message Date
OTKUSteyler
1547421074
Update index.ts
Some checks failed
Node.js CI / CD (Bun + Forgejo) / buildAndRelease (push) Has been cancelled
2026-09-12 10:39:07 +02:00
OTKUSteyler
f3b7935ab0
Update index.ts 2026-09-12 10:36:20 +02:00
OTKUSteyler
2fca83c60a
Merge branch 'kmmiio99o:main' into main 2026-09-11 09:31:49 +02:00
kmmiio99o
c663a3724c
Merge pull request #14 from ItsTripleSix/fix/account-switch-splash-hang
fix: prevent account-switch cold launch splash hang
2026-09-11 08:48:19 +02:00
OTKUSteyler
94e651a50e
Update index.ts 2026-09-11 08:28:51 +02:00
OTKUSteyler
40726d5b9a
Delete src/core/plugins/CustomBackgroundMusic directory 2026-09-11 08:27:50 +02:00
OTKUSteyler
d8b35b5501
Update updater.ts 2026-09-11 08:26:59 +02:00
OTKUSteyler
32d32d3f02
Update index.ts 2026-09-11 08:02:23 +02:00
OTKUSteyler
3914eb8e11
Create Settings.tsx 2026-09-11 08:00:13 +02:00
OTKUSteyler
ad9963ade2
Create index.ts 2026-09-11 07:59:27 +02:00
OTKUSteyler
363f9984f6
Delete src/core/plugins/custom background music directory 2026-09-11 07:58:22 +02:00
OTKUSteyler
f5c9edd1fd
Rename Index.ts to index.ts 2026-09-11 07:56:06 +02:00
OTKUSteyler
c1b2498009
Update index.ts 2026-09-11 07:43:27 +02:00
OTKUSteyler
1e381e4940
Update Settings.tsx 2026-09-11 07:38:52 +02:00
OTKUSteyler
e87aff3801
Update Index.ts 2026-09-11 07:38:00 +02:00
OTKUSteyler
0ed3508d7c
Update Index.ts 2026-09-11 07:23:46 +02:00
OTKUSteyler
c1b8f5969e
Create Settings.tsx 2026-09-11 07:22:42 +02:00
OTKUSteyler
24ebe343bd
Create Index.ts 2026-09-11 07:21:18 +02:00
ItsTripleSix
d0b633fa17
chore: bump ShiggyCord version to 1.4.1.8 2026-09-10 16:10:48 -06:00
ItsTripleSix
9dfe5ca671
fix: prevent account-switch cold launch splash hang 2026-09-10 16:09:53 -06:00
OTKUSteyler
bdb448eb94
Update index.ts 2026-09-09 08:39:24 +02:00
OTKUSteyler
2c91da18ea
Update index.ts 2026-09-09 08:38:39 +02:00
OTKUSteyler
1d630e1e3a
Update PluginCard.tsx 2026-09-05 16:47:28 +02:00
OTKUSteyler
2c0c24cb86
Update index.ts 2026-09-04 20:36:27 +02:00
OTKUSteyler
a42fb7ce36
Update About.tsx 2026-09-04 15:17:42 +02:00
OTKUSteyler
f48aea843f
Update debug.ts 2026-09-03 21:50:47 +02:00
OTKUSteyler
8430031566
Update reload.ts 2026-09-03 21:10:07 +02:00
OTKUSteyler
fa8ebe30bd
Update debug.ts 2026-09-03 21:09:44 +02:00
OTKUSteyler
e6e5728d25
Update index.ts 2026-09-03 20:43:33 +02:00
OTKUSteyler
5f41539026
Update index.tsx 2026-09-03 20:37:21 +02:00
OTKUSteyler
1b99480b82
Update build.mjs 2026-09-03 18:03:48 +02:00
OTKUSteyler
151b07434e
Update package.json 2026-09-03 18:02:28 +02:00
OTKUSteyler
11a6e4d1c4
Update package.json 2026-09-03 18:00:03 +02:00
OTKUSteyler
cbcbd53cc0
Add files via upload 2026-09-03 17:15:26 +02:00
OTKUSteyler
d26de6be8e
Update index.ts 2026-09-03 17:14:22 +02:00
OTKUSteyler
b51ae0677d
Update index.tsx 2026-09-03 17:13:38 +02:00
OTKUSteyler
5cdb7d9969
Update index.ts 2026-09-03 17:13:09 +02:00
OTKUSteyler
687f47318b
Update default.json 2026-09-03 17:12:03 +02:00
OTKUSteyler
1071349bbc
Add files via upload 2026-09-03 17:10:56 +02:00
OTKUSteyler
72ce900cd8
Update README.md 2026-09-03 17:09:30 +02:00
OTKUSteyler
ecef05061b
Update release.yml 2026-09-03 16:04:20 +02:00
kmmiio99o
15915a08b9
chore: update embed URLs for Instagram and Reddit 2026-09-02 19:41:07 +00:00
kmmiio99o
ad46e065c9
Merge branch 'C0C0B01:main' into main 2026-08-27 13:23:18 +02:00
cocobo1
f5bbcecade Merge pull request 'fix: properly resolve design components' (#31) from Purple-EyeZ/Kettu:dev into dev
Reviewed-on: https://codeberg.org/cocobo1/Kettu/pulls/31
2026-08-27 09:08:29 +02:00
kmmiio99o
3f5643931c
Merge branch 'dev' 2026-08-27 00:39:25 +02:00
Purple_Ξye™
28fe79ade4
fix: properly resolve design components (#12) 2026-08-27 00:33:00 +02:00
Purple_Ξye™
5858340d04 fix: properly resolve design components 2026-08-26 23:36:44 +02:00
kmmiio99o
36e4bccc6a
Merge branch 'dev' 2026-08-13 11:43:59 +02:00
kmmiio99o
1d73ac91e5
chore: bump to 1.4.1.7 2026-08-09 23:47:02 +02:00
kmmiio99o
1f3ad73135
fix(ErrorBoundaryScreen): text color 2026-08-09 23:44:54 +02:00
kmmiio99o
4bb190b81a
fix(General): strings 2026-08-09 23:41:35 +02:00
kmmiio99o
723815e4c5
fix(band-aid): crashing in settings pages 2026-08-09 23:41:24 +02:00
kmmiio99o
4b64d99ac4
Merge branch 'dev' 2026-07-31 11:48:38 +02:00
kmmiio99o
cb877a2a99
feat: iOS Tweak support
because we are basing it off RainTweak, we need to add such stuff
2026-07-31 10:51:51 +02:00
kmmiio99o
a60f527f3c
fix: pin @gullerya/object-observer alias to bypass broken
exports.require in CI
2026-07-30 19:54:18 +02:00
kmmiio99o
fc6e5bccfd
chore: bump to 1.4.1.6 2026-07-30 19:51:16 +02:00
kmmiio99o
185f4ab6f3
fix: loading plugin's on 341.3+ 2026-07-30 19:49:37 +02:00
kmmiio99o
0dc4e8cdf7
chore: bump dependencies 2026-07-30 19:46:18 +02:00
kmmiio99o
a628abc128
chore: bump to 1.4.1.5 2026-07-27 22:40:01 +02:00
kmmiio99o
01a00fc98d
Merge remote-tracking branch 'upstream/dev' into dev 2026-07-27 22:38:03 +02:00
kmmiio99o
4c2c6a3058
Merge remote-tracking branch 'upstream' into dev 2026-07-27 22:21:29 +02:00
cocobo1
4f4a84ebce fix: remove broken settings section on android and broken device info command 2026-07-27 16:50:02 +00:00
cocobo1
791956daac chore: bump version 2026-07-27 16:38:25 +00:00
cocobo1
3c009cc46e feat: fix colouring on ui text 2026-07-27 16:37:42 +00:00
cocobo1
e345bb14d3 Merge branch 'dev' into github 2026-07-04 08:48:00 +00:00
kmmiio99o
3637ba9963
fix: crash in Themes page
when using other languages than english, it was just crashing
2026-05-29 18:44:58 +02:00
kmmiio99o
2155a2a055
feat: fix crash in Themes page 2026-05-15 21:41:32 +02:00
kmmiio99o
dd91831481
Merge remote-tracking branch 'upstream/main' 2026-05-15 21:37:50 +02:00
cocobo1
61751664c5 fix: settings on alpha 330.1+ 2026-05-14 20:06:34 +00:00
kmmiio99o
06c8853399
fix crashing in Themes tab
don't ask me how it did it
2026-05-09 15:24:22 +02:00
cocobo1
727603ff46 fix: settings on 329.3 2026-05-09 08:09:09 +00:00
kmmiio99o
de84964ff1
bump to 1.4.1.4 2026-05-07 14:38:05 +02:00
kmmiio99o
03ccabfb5c
fix: settings crashing when opening them
happens on 329203+ versions
2026-05-07 14:34:03 +02:00
kmmiio99o
752d1c9b11
feat: remove messagefix
was causing issues with freemoji plugin
2026-05-07 13:32:11 +02:00
kmmiio99o
af64cda7fb
fix icon in settings 2026-04-26 11:32:22 +02:00
kmmiio99o
d697150982
Revert "feat: drop fixembed plugin and fix icon in main settings"
This reverts commit 84f6592ecf.
2026-04-26 11:28:06 +02:00
kmmiio99o
4587de15b0
bump to 1.4.1.3 2026-04-25 20:54:53 +02:00
kmmiio99o
ffd84c017e
small fixes 2026-04-25 20:54:18 +02:00
kmmiio99o
84f6592ecf
feat: drop fixembed plugin and fix icon in main settings 2026-04-25 20:45:06 +02:00
kmmiio99o
676f1a19ba
feat: some speedups 2026-04-05 21:20:30 +02:00
kmmiio99o
18dccc9e17
chore: bump dependencies 2026-04-05 21:20:20 +02:00
kmmiio99o
63bfe21eb7
Merge remote-tracking branch 'upstream/dev' into dev 2026-03-21 13:08:05 +01:00
cocobo1
d31739d8d8 Merge branch 'dev' into github 2026-03-18 22:13:16 +02:00
cocobo1
1cf2787f3a fix: make badges work again 2026-03-18 22:12:52 +02:00
cocobo1
ec97bbb3be Merge branch 'dev' into github 2026-03-18 17:54:03 +02:00
cocobo1
cb6a242fe8 feat: make i18n work (thanks serstars) 2026-03-18 17:53:26 +02:00
cocobo1
d9ba90e6ef Merge branch 'main' into github 2026-03-08 18:47:36 +02:00
cocobo1
43a4a5b665 docs: update readme 2026-03-08 18:45:16 +02:00
kmmiio99o
612483fbea
feat: Optimize object iteration and module counting 2026-03-06 18:32:05 +01:00
kmmiio99o
ad93bfa6c6
fix: build file 2026-03-06 18:09:13 +01:00
kmmiio99o
2f1702f5a4
Merge remote-tracking branch 'upstream/dev' into dev 2026-03-06 18:06:55 +01:00
cocobo1
1876c4a4a6 fix: make themes work 2026-03-06 18:51:46 +02:00
kmmiio99o
1fa558e6a0
fix: crashing on chat 2026-02-27 16:43:24 +01:00
kmmiio99o
a4c079ac30
fix: backgrounds not working 2026-02-25 23:48:29 +01:00
cocobo1
3bae44e833 Merge branch 'dev' into github 2025-12-12 20:09:13 +02:00
41 changed files with 1769 additions and 2170 deletions

View file

@ -98,7 +98,7 @@ jobs:
tag_name: ${{ steps.prepare.outputs.tag }} tag_name: ${{ steps.prepare.outputs.tag }}
name: Release ${{ steps.prepare.outputs.tag }} name: Release ${{ steps.prepare.outputs.tag }}
files: | files: |
dist/shiggycord.js dist/gooncord.js
dist/shiggycord.min.js dist/gooncord.min.js
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,39 +1,41 @@
# ShiggyCord [![Discord](https://img.shields.io/discord/1427396621905432699?style=social&logo=discord&label=ShiggyCord)](https://discord.shiggycord.dev) # GoonCord [![Discord](https://img.shields.io/discord/1427396621905432699?style=social&logo=discord&label=ShiggyCord)](https://discord.shiggycord.dev)
Bundle based on Kettu, made just for fun Bundle based on ShiggyCord, made just for fun
## Installing ## Installing
### Android ### Android
- Rooted: [Shiggy Xposed](https://github.com/kmmiio99o/ShiggyXposed) - Rooted: [Goon Xposed](https://github.com/OTKUSteyler/GoonXposed)
- Non-Rooted: [Shiggy Manager](https://github.com/kmmiio99o/ShiggyManager) - Non-Rooted: [Goon Manager](https://github.com/OTKUSteyler/GoonManager)
### Android/iOS ### Android/iOS
- **Injecting bundle:** - **Injecting bundle:**
```url ```url
https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/shiggycord.js https://github.com/OTKUSteyler/GoonCord/releases/latest/download/gooncord.js
``` ```
## Building ## Building
1. Install a ShiggyCord loader with loader config support (any mentioned in the [Installing](#installing) section). 1. Install a GoonCord loader with loader config support (any mentioned in the [Installing](#installing) section).
1. Go to Settings > General and enable Developer Settings. 1. Go to Settings > General and enable Developer Settings.
1. Clone the repo: 1. Clone the repo:
``` ```
git clone https://github.com/kmmiio99o/Shiggycord.git git clone https://github.com/OTKUSteyler/gooncord.git
``` ```
1. Install dependencies: 1. Install dependencies:
``` ```
bun i bun i
``` ```
1. Build ShiggyCord's code: 1. Build GoonCord's code:
``` ```
bun run build bun run build
``` ```
1. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server). 1. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server).
1. Go to Settings > Developer enabled earlier. Enable `Load from custom url` and input the IP address and port of the server (e.g. `http://192.168.1.236:4040/shiggycord.js`) in the new input box labelled `ShiggyCord URL`. 1. Go to Settings > Developer enabled earlier. Enable `Load from custom url` and input the IP address and port of the server (e.g. `http://192.168.1.236:4040/gooncord.js`) in the new input box labelled `GoonCord URL`.
1. Restart Discord. Upon reload, you should notice that your device will download ShiggyCord's bundled code from your server, rather than GitHub. 1. Restart Discord. Upon reload, you should notice that your device will download GoonCord's bundled code from your server, rather than GitHub.
1. Make your changes, rebuild, reload, go wild! 1. Make your changes, rebuild, reload, go wild!
Alternatively, you can directly *serve* the bundled code by running `bun run serve`. `shiggycord.js` will be served on your local address under the port 4040. You will then insert `http://<local ip address>:4040/shiggycord.js` as a custom url and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it. Alternatively, you can directly *serve* the bundled code by running `bun run serve`. `gooncord.js` will be served on your local address under the port 4040. You will then insert `http://<local ip address>:4040/gooncord.js` as a custom url and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it.
![OTKUSteyler](https://count.getloli.com/@OTKUSteyler?name=OTKUSteyler&theme=miku&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=1)

1989
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
{ {
"name": "ShiggyCord", "name": "GoonCord",
"private": true, "private": true,
"author": { "author": {
"name": "kmmiio99o", "name": "OTKUSteyler",
"url": "https://github.com/kmmiio99o" "url": "https://github.com/OTKUSteyler"
}, },
"scripts": { "scripts": {
"build": "node scripts/build.mjs", "build": "node scripts/build.mjs",
@ -13,45 +13,40 @@
}, },
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-backmerge": "^4.0.1", "@swc/core": "^1.15.47",
"@semantic-release/changelog": "^6.0.3", "@swc/helpers": "^0.5.23",
"@semantic-release/exec": "^7.1.0", "@types/node": "^26.1.2",
"@semantic-release/git": "^10.0.1",
"semantic-release": "^25.0.3",
"@swc/core": "^1.15.13",
"@swc/helpers": "^0.5.19",
"@types/node": "^25.3.0",
"@types/yargs-parser": "^21.0.3", "@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^8.56.0", "@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.56.0", "@typescript-eslint/parser": "^8.65.0",
"@typescript-eslint/typescript-estree": "^8.56.0", "@typescript-eslint/typescript-estree": "^8.65.0",
"chalk": "^5.6.2", "chalk": "^6.0.0",
"esbuild": "^0.27.3", "esbuild": "^0.28.1",
"esbuild-plugin-globals": "^0.2.0", "esbuild-plugin-globals": "^0.2.0",
"eslint": "^10.0.1", "eslint": "^10.8.0",
"eslint-plugin-import-alias": "^1.2.0", "eslint-plugin-import-alias": "^1.2.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^14.0.0",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"typescript": "^5.9.3", "typescript": "^7.0.2",
"yargs-parser": "^22.0.0" "yargs-parser": "^22.0.0"
}, },
"dependencies": { "dependencies": {
"@gullerya/object-observer": "^6.1.4", "@gullerya/object-observer": "^7.0.0",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.101.4",
"@types/chroma-js": "~3.1.2", "@types/chroma-js": "~3.1.2",
"@types/lodash": "~4.17.24", "@types/lodash": "~4.17.24",
"@types/react": "19.2.14", "@types/react": "19.2.17",
"es-toolkit": "^1.44.0", "es-toolkit": "^1.50.0",
"fuzzysort": "^3.1.0", "fuzzysort": "^3.1.0",
"intl-messageformat": "^11.1.2", "intl-messageformat": "^11.2.12",
"moment": "2.30.1", "moment": "2.30.1",
"react-devtools": "^7.0.1", "react-devtools": "^7.0.1",
"react-native": "0.84.0", "react-native": "0.86.2",
"react-native-reanimated": "^4.2.2", "react-native-reanimated": "^4.5.3",
"react-native-webview": "^13.16.0", "react-native-webview": "^14.0.1",
"spitroast": "^2.1.6", "spitroast": "^2.1.6",
"type-fest": "^5.4.4" "type-fest": "^5.8.0"
}, },
"trustedDependencies": [ "trustedDependencies": [
"@swc/core", "@swc/core",

View file

@ -30,7 +30,7 @@ let context = null;
const config = { const config = {
entryPoints: ["src/entry.ts"], entryPoints: ["src/entry.ts"],
bundle: true, bundle: true,
outfile: "dist/shiggycord.js", outfile: "dist/gooncord.js",
format: "iife", format: "iife",
splitting: false, splitting: false,
// Enable minification by default for release builds (when a release-branch is provided). // Enable minification by default for release builds (when a release-branch is provided).
@ -56,6 +56,8 @@ const config = {
"!ShiggyCord-deps-shim!": "./shims/depsModule.ts", "!ShiggyCord-deps-shim!": "./shims/depsModule.ts",
spitroast: "./node_modules/spitroast", spitroast: "./node_modules/spitroast",
"react/jsx-runtime": "./shims/jsxRuntime", "react/jsx-runtime": "./shims/jsxRuntime",
"@gullerya/object-observer":
path.resolve("./node_modules/@gullerya/object-observer/dist/object-observer.min.js"),
}, },
plugins: [ plugins: [
globalPlugin({ globalPlugin({
@ -75,7 +77,7 @@ const config = {
constModules: { constModules: {
globals: { globals: {
"bunny-build-info": { "bunny-build-info": {
version: `"1.4.1.2"`, version: `"1.4.1.8"`,
}, },
}, },
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -8,7 +8,7 @@ import { messageUtil } from "@metro/common";
export default () => export default () =>
<ApplicationCommand>{ <ApplicationCommand>{
name: "debug", name: "Goon",
description: Strings.COMMAND_DEBUG_DESC, description: Strings.COMMAND_DEBUG_DESC,
options: [ options: [
{ {
@ -19,14 +19,18 @@ export default () =>
], ],
execute([ephemeral], ctx) { execute([ephemeral], ctx) {
const info = getDebugInfo(); const info = getDebugInfo();
if(info.device.codename === undefined) { info.device.codename = ""}
else{ info.device.codename = "(" + info.device.codename + ")" }
const content = [ const content = [
"**ShiggyCord Debug Info**", "**💦GoonCord💦 Debug Info**",
`> ShiggyCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`, `> GoonCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`, `> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`, `> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`, `> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(), `> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
`> Device: ${info.device.model}`, `> Device: ${info.device.model} ${info.device.codename}`,
].join("\n"); ].join("\n");
if (ephemeral?.value) { if (ephemeral?.value) {

View file

@ -4,7 +4,7 @@ import { BundleUpdaterManager } from "@lib/api/native/modules";
export default () => export default () =>
<ApplicationCommand>{ <ApplicationCommand>{
name: "reload", name: "ReGoon",
description: Strings.COMMAND_RELOAD_DESC, description: Strings.COMMAND_RELOAD_DESC,
options: [], options: [],
execute() { execute() {

View file

@ -11,8 +11,8 @@
"IMAGE_FILES": "Image Files", "IMAGE_FILES": "Image Files",
"TEXT_FILES": "Text Files", "TEXT_FILES": "Text Files",
"BRAND": "Brand", "BRAND": "Brand",
"SHIGGYCORD": "ShiggyCord", "SHIGGYCORD": "GoonCord",
"SHIGGYCORD_URL": "ShiggyCord URL", "SHIGGYCORD_URL": "GoonCord URL",
"BROWSER": "Browser", "BROWSER": "Browser",
"BYTECODE": "Bytecode", "BYTECODE": "Bytecode",
"CANCEL": "Cancel", "CANCEL": "Cancel",
@ -24,7 +24,7 @@
"CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}", "CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}",
"CODEBERG": "Codeberg", "CODEBERG": "Codeberg",
"CODENAME": "Codename", "CODENAME": "Codename",
"COMMAND_DEBUG_DESC": "Send ShiggyCord debug info.", "COMMAND_DEBUG_DESC": "Send 💦GoonCord💦 debug info.",
"COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.", "COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.",
"COMMAND_EVAL_DESC": "Evaluate JavaScript code.", "COMMAND_EVAL_DESC": "Evaluate JavaScript code.",
"COMMAND_EVAL_OPT_ASYNC": "Whether to support 'await' in code. Must explicitly return for result (default: false)", "COMMAND_EVAL_OPT_ASYNC": "Whether to support 'await' in code. Must explicitly return for result (default: false)",
@ -70,7 +70,7 @@
"LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme", "LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme",
"LINKS": "Links", "LINKS": "Links",
"LOAD_FROM_CUSTOM_URL": "Load from custom URL", "LOAD_FROM_CUSTOM_URL": "Load from custom URL",
"LOAD_FROM_CUSTOM_URL_DEC": "Load ShiggyCord from a custom endpoint.", "LOAD_FROM_CUSTOM_URL_DEC": "Load GoonCord from a custom endpoint.",
"LOAD_REACT_DEVTOOLS": "Load React DevTools", "LOAD_REACT_DEVTOOLS": "Load React DevTools",
"SHIGGY_URL": "ShiggyCord URL", "SHIGGY_URL": "ShiggyCord URL",
"LOADER": "Loader", "LOADER": "Loader",
@ -78,7 +78,6 @@
"MANUFACTURER": "Manufacturer", "MANUFACTURER": "Manufacturer",
"MESSAGE": "Message", "MESSAGE": "Message",
"MISCELLANEOUS": "Miscellaneous", "MISCELLANEOUS": "Miscellaneous",
"MODAL_RELOAD_THEME_CHANGE": "Reload the app to apply the theme change?",
"MODAL_RELOAD_REQUIRED": "Reload app?", "MODAL_RELOAD_REQUIRED": "Reload app?",
"MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?", "MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?",
"MODAL_THEME_REFETCHED": "Theme refetched", "MODAL_THEME_REFETCHED": "Theme refetched",
@ -95,7 +94,7 @@
"PLUGINS": "Plugins", "PLUGINS": "Plugins",
"REFETCH": "Refetch", "REFETCH": "Refetch",
"RELOAD": "Reload", "RELOAD": "Reload",
"RELOAD_DISCORD": "Reshiggy your Discord", "RELOAD_DISCORD": "ReGoon your Discord",
"RELOAD_IN_NORMAL_MODE": "Reload in Normal Mode", "RELOAD_IN_NORMAL_MODE": "Reload in Normal Mode",
"RELOAD_IN_NORMAL_MODE_DESC": "Safe mode currently enabled, tap to reload in normal mode", "RELOAD_IN_NORMAL_MODE_DESC": "Safe mode currently enabled, tap to reload in normal mode",
"RELOAD_IN_SAFE_MODE": "Reload in Safe Mode", "RELOAD_IN_SAFE_MODE": "Reload in Safe Mode",
@ -124,7 +123,7 @@
"UH_OH": "Uh Oh", "UH_OH": "Uh Oh",
"UNINSTALL": "Uninstall", "UNINSTALL": "Uninstall",
"UNINSTALL_TITLE": "Uninstall {title}", "UNINSTALL_TITLE": "Uninstall {title}",
"URL_PLACEHOLDER": "https://github.com/kmmiio99o/ShiggyCord", "URL_PLACEHOLDER": "https://github.com/OTKUSteyler/GoonCord",
"VERSION": "Version", "VERSION": "Version",
"VERSIONS": "Versions" "VERSIONS": "Versions"
} }

View file

@ -1,5 +1,6 @@
import { logger } from "@lib/utils/logger";
import { FluxDispatcher } from "@metro/common"; import { FluxDispatcher } from "@metro/common";
import { findByNameLazy } from "@metro/wrappers"; import { findByNameLazy, findByStoreName } from "@metro/wrappers";
import { PrimitiveType } from "intl-messageformat"; import { PrimitiveType } from "intl-messageformat";
import langDefault from "./default.json"; import langDefault from "./default.json";
@ -28,50 +29,122 @@ export const Strings = new Proxy(
}, },
) as Record<I18nKey, string>; ) as Record<I18nKey, string>;
export function initFetchI18nStrings() { const languageMap: Record<string, string> = {
const cb = ({ locale }: { locale: string }) => { "ar-SA": "ar",
const languageMap = { "bn-BD": "bn",
"ca-ES": "ca",
"de-DE": "de",
"es-ES": "es", "es-ES": "es",
"es-419": "es_419", "es-419": "es",
"zh-TW": "zh-Hant", "fa-IR": "fa",
"zh-CN": "zh-Hans", "fi-FI": "fi",
"pt-PT": "pt", "fr-FR": "fr",
"hi-IN": "hi",
"hr-HR": "hr",
"hu-HU": "hu",
"id-ID": "id",
"it-IT": "it",
"ja-JP": "ja",
"pl-PL": "pl",
"pt-BR": "pt_BR", "pt-BR": "pt_BR",
"ru-RU": "ru",
"sk-SK": "sk",
"sv-SE": "sv", "sv-SE": "sv",
} as Record<string, string>; "tr-TR": "tr",
"vi-VN": "vi",
};
function fetchLocale(locale: string) {
const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale); const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale);
if (resolvedLocale.startsWith("en-")) { logger.log("[i18n] fetchLocale called:", locale, "->", resolvedLocale);
_currentLocale = null;
return;
}
if (!_loadedLocale.has(resolvedLocale)) { if (!_loadedLocale.has(resolvedLocale)) {
_loadedLocale.add(resolvedLocale); _loadedLocale.add(resolvedLocale);
fetch( if (resolvedLocale.toLowerCase().startsWith("en")) {
`https://raw.githubusercontent.com/kmmiio99o/i18n/main/resources/${resolvedLocale}/shiggycord.json`, // use local default.json for english
) logger.log("[i18n] Using local default.json for English locale");
.then((r) => r.json()) _loadedStrings[resolvedLocale] = langDefault;
.then((strings) => (_loadedStrings[resolvedLocale] = strings)) _currentLocale = resolvedLocale;
.then( } else {
() => fetch(`https://codeberg.org/cocobo1/kettu-i18n/raw/branch/main/base/${resolvedLocale}.json`)
resolvedLocale === _lastSetLocale && .then(r => r.json())
(_currentLocale = resolvedLocale), .then(strings => {
) logger.log("[i18n] Loaded strings for:", resolvedLocale);
.catch((e) => _loadedStrings[resolvedLocale] = strings;
console.error( _currentLocale = resolvedLocale;
`An error occured while fetching strings for ${resolvedLocale}: ${e}`, })
), .catch(e =>
logger.error(`[i18n] Error fetching strings for ${resolvedLocale}: ${e}`),
); );
}
} else { } else {
_currentLocale = resolvedLocale; _currentLocale = resolvedLocale;
} }
}
export function initFetchI18nStrings() {
let attempts = 0;
const checkAndFetch = () => {
attempts++;
try {
const LocaleStore = findByStoreName("LocaleStore");
logger.log("[i18n] Attempt", attempts, "- LocaleStore:", !!LocaleStore);
if (!LocaleStore) {
logger.log("[i18n] LocaleStore not found yet");
return false;
}
if (LocaleStore?._isInitialized !== true) {
logger.log("[i18n] LocaleStore not initialized yet");
return false;
}
const locale = LocaleStore.locale;
if (locale) {
logger.log("[i18n] Using LocaleStore:", locale);
fetchLocale(locale);
return true;
}
} catch (e) {
logger.log("[i18n] Error:", e);
}
return false;
}; };
FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb); const tryTimes = () => {
return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb); if (checkAndFetch()) return;
if (attempts < 15) {
setTimeout(tryTimes, 500);
}
};
tryTimes();
const cb = (e: any) => {
if (e?.settings?.changes?.loading) {
logger.log("[i18n] Settings loading, skipping...");
return;
}
const locale = e?.settings?.changes?.protoToSave?.localization?.locale?.value;
logger.log("[i18n] Locale changed:", locale);
if (locale) {
logger.log("[i18n] Found locale in event:", locale);
fetchLocale(locale);
}
};
FluxDispatcher.subscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb);
return () => {
FluxDispatcher.unsubscribe("USER_SETTINGS_PROTO_UPDATE_EDIT_INFO", cb);
};
} }
type FormatStringRet<T> = T extends PrimitiveType type FormatStringRet<T> = T extends PrimitiveType
@ -83,6 +156,16 @@ export function formatString<T = void>(
val: Record<string, T>, val: Record<string, T>,
): FormatStringRet<T> { ): FormatStringRet<T> {
const str = Strings[key]; const str = Strings[key];
if (!str) return str as FormatStringRet<T>;
try {
// @ts-ignore // @ts-ignore
return new IntlMessageFormat(str).format(val); return new IntlMessageFormat(str).format(val);
} catch (e: any) {
if (e?.name === "SyntaxError") {
logger.warn(`[i18n] Failed to parse message for key "${String(key)}":`, e.message);
return str as FormatStringRet<T>;
}
logger.warn(`[i18n] Failed to format message for key "${String(key)}":`, e?.message ?? e);
return str as FormatStringRet<T>;
}
} }

View file

@ -1,20 +1,47 @@
import { after } from "@lib/api/patcher"; import { after } from "@lib/api/patcher";
import { onJsxCreate } from "@lib/api/react/jsx"; import { onJsxCreate } from "@lib/api/react/jsx";
import { findByName } from "@metro"; import { findByNameLazy } from "@metro";
import { useEffect, useState } from "react";
import { defineCorePlugin } from ".."; import { defineCorePlugin } from "..";
import { FluxDispatcher } from "@metro/common";
interface BunnyBadge { interface Badge {
label: string; label: string;
url: string; url: string;
} }
const useBadgesModule = findByName("useBadges", false); interface UserBadgeData {
roles?: string[];
custom?: Badge[];
}
interface BadgeData {
[userId: string]: UserBadgeData;
}
interface RoleData {
label: string;
url: string;
}
interface RolesData {
[roleName: string]: RoleData;
}
interface EquicordBadge {
tooltip: string;
badge: string;
}
const useBadgesModule = findByNameLazy("useBadges", false);
const badgesCache = new Map<string, Badge[]>();
const badgeProps = new Map<string, Record<string, any>>();
const pendingRequests = new Set<string>();
export default defineCorePlugin({ export default defineCorePlugin({
manifest: { manifest: {
id: "bunny.badges", id: "bunny.badges",
version: "1.1.0", version: "1.2.0",
type: "plugin", type: "plugin",
spec: 3, spec: 3,
main: "", main: "",
@ -26,12 +53,9 @@ export default defineCorePlugin({
}, },
start() { start() {
let allBadges: { [x: string]: any } | null = null;
const badgeProps = {} as Record<string, any>;
onJsxCreate("ProfileBadge", (component, ret) => { onJsxCreate("ProfileBadge", (component, ret) => {
if (ret.props.id?.startsWith("bunny-")) { if (ret.props.id?.startsWith("rain-")) {
const cachedProps = badgeProps[ret.props.id]; const cachedProps = badgeProps.get(ret.props.id);
if (cachedProps) { if (cachedProps) {
ret.props.source = cachedProps.source; ret.props.source = cachedProps.source;
ret.props.label = cachedProps.label; ret.props.label = cachedProps.label;
@ -41,54 +65,137 @@ export default defineCorePlugin({
}); });
onJsxCreate("RenderedBadge", (component, ret) => { onJsxCreate("RenderedBadge", (component, ret) => {
if (ret.props.id?.startsWith("bunny-")) { if (ret.props.id?.startsWith("rain-")) {
const cachedProps = badgeProps[ret.props.id]; const cachedProps = badgeProps.get(ret.props.id);
if (cachedProps) { if (cachedProps) {
Object.assign(ret.props, cachedProps); Object.assign(ret.props, cachedProps);
} }
} }
}); });
after("default", useBadgesModule, ([user], result) => { // Small helper so one failing/optional source can't take down the others.
const [badges, setBadges] = useState<BunnyBadge[]>([]); const safeFetchJson = async <T,>(url: string, fallback: T): Promise<T> => {
try {
useEffect(() => { const res = await fetch(url);
if (!user) return; if (!res.ok) return fallback;
return (await res.json()) as T;
if (!allBadges) { } catch {
fetch( return fallback;
"https://codeberg.org/raincord/badges/raw/branch/main/badges.json",
)
.then((r) => r.json())
.then((data) => {
allBadges = data;
//@ts-expect-error
setBadges(allBadges[user.userId] || []);
});
} else {
//user has no badges, but maybe they should get some by contributing
setBadges(allBadges[user.userId] || []);
} }
}, [user?.userId]); };
if (user && badges.length > 0) { const fetchAndProcessBadges = async (userId: string) => {
badges.forEach((badge, i) => { if (pendingRequests.has(userId)) return;
const badgeId = `bunny-${user.userId}-${i}`; pendingRequests.add(userId);
badgeProps[badgeId] = { try {
const [
goonBadgesData,
goonRolesData,
rainBadgesData,
rainRolesData,
equicordData,
] = await Promise.all([
safeFetchJson<BadgeData>(
"https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/badges.json",
{}
),
safeFetchJson<RolesData>(
"https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/assets/roles/roles.json",
{}
),
safeFetchJson<BadgeData>(
"https://codeberg.org/raincord/badges/raw/branch/main/badges.json",
{}
),
safeFetchJson<RolesData>(
"https://codeberg.org/raincord/badges/raw/branch/main/assets/roles/roles.json",
{}
),
safeFetchJson<Record<string, EquicordBadge[]>>(
"https://badge.equicord.org/badges.json",
{}
),
]);
const allBadges: Badge[] = [];
// GoonCord and raincord badge/role sets are keyed independently,
// so each source is resolved against its own roles file.
const badgeSources: Array<{ badges: BadgeData; roles: RolesData }> = [
{ badges: goonBadgesData, roles: goonRolesData },
{ badges: rainBadgesData, roles: rainRolesData },
];
badgeSources.forEach(({ badges, roles }) => {
const userBadgeData = badges[userId];
if (!userBadgeData) return;
// process role badges
userBadgeData.roles?.forEach(roleName => {
const roleData = roles[roleName];
if (roleData) {
allBadges.push({
label: roleData.label,
url: roleData.url,
});
}
});
// process custom badges
if (userBadgeData.custom) {
allBadges.push(...userBadgeData.custom);
}
});
// process equicord badges
const equicordUserBadges = equicordData[userId] ?? [];
equicordUserBadges.forEach(b => {
allBadges.push({ label: b.tooltip, url: b.badge });
});
badgesCache.set(userId, allBadges);
allBadges.forEach((badge, i) => {
const badgeId = `rain-${userId}-${i}`;
badgeProps.set(badgeId, {
id: badgeId, id: badgeId,
source: { uri: badge.url }, source: { uri: badge.url },
label: badge.label, label: badge.label,
userId: user.userId, userId,
});
});
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: { id: userId } });
} catch (err) {
console.error("[bunny.badges] Failed to fetch/process badges:", err);
} finally {
pendingRequests.delete(userId);
}
}; };
result.push({ return after("default", useBadgesModule, ([user], result) => {
if (!user) return;
const userId = user.userId;
const cached = badgesCache.get(userId);
if (!cached) {
if (!pendingRequests.has(userId)) {
fetchAndProcessBadges(userId);
}
return;
}
cached.forEach((badge, i) => {
const badgeId = `rain-${userId}-${i}`;
result.unshift({
id: badgeId, id: badgeId,
description: badge.label, description: badge.label,
icon: " _", icon: " _",
}); });
}); });
}
}); });
}, },
}); });

View file

@ -0,0 +1,13 @@
import { Forms } from "@vendetta/ui/components";
const { FormSection, FormText } = Forms;
export default () => (
<FormSection title="FavouriteAnything">
<FormText>
Adds the favourite button to ALL media in the image viewer, not just GIFs!
</FormText>
<FormText style={{ marginTop: 8, color: "#b9bbbe" }}>
Images and videos can now be saved to your GIF favourites picker. Works by patching Discord's GIFFavButton to accept all media types.
</FormText>
</FormSection>
)

View file

@ -0,0 +1,169 @@
// src/core/plugins/favouriteanything/index.ts
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { before } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
type PatchCleanupFn = () => void;
let origType: Function | null = null;
let memoWrapper: any = null;
let retryTimeout: ReturnType<typeof setTimeout> | null = null;
let unpatchAddFavorite: PatchCleanupFn | null = null;
let origUseFavoriteGIFsMobile: Function | null = null;
let favMobileModule: any = null;
const VIDEO_EXT = [".mp4", ".webm", ".mov", ".avi", ".mkv", ".flv", ".wmv", ".m4v", ".gifv"];
const processed = new WeakSet<object>();
function isVideo(url: string): boolean {
if (!url) return false;
const path = url.split("?")[0].toLowerCase();
return VIDEO_EXT.some(e => path.endsWith(e));
}
function makeVideoThumbnail(url: string): string {
if (!url) return url;
let out = url.replace("cdn.discordapp.com", "media.discordapp.net");
if (out.includes("media.discordapp.net") || out.includes("images-ext")) {
return out + (out.includes("?") ? "&" : "?") + "format=jpeg";
}
return url;
}
function findGIFFavButton(): any {
const modules = (globalThis as any).__bunny?.metro?.modules ?? (globalThis as any).modules;
if (!modules) return null;
for (const id in modules) {
try {
const mod = modules[id]?.publicModule?.exports;
if (!mod) continue;
const def = mod.default;
if (def?.$$typeof?.toString().includes("memo") && def.type) {
if (def.type.displayName === "GIFFavButton" || def.type.name === "GIFFavButton") return def;
}
if (typeof mod === "function" && (mod.displayName === "GIFFavButton" || mod.name === "GIFFavButton")) return mod;
} catch {}
}
return null;
}
function patchSource(source: any): any {
if (source.isGIFV) return source;
return {
...source,
isGIFV: true,
embedURI: source.embedURI || source.sourceURI || source.uri,
videoURI: source.videoURI || source.uri,
embedProviderName: source.embedProviderName || "",
};
}
function applyPatch(): boolean {
memoWrapper = findGIFFavButton();
if (!memoWrapper) return false;
origType = memoWrapper.type;
memoWrapper.type = function PatchedGIFFavButton(props: any) {
const p = { ...props };
if (p.source && !p.source.isGIFV) p.source = patchSource(p.source);
return (origType as Function)(p);
};
(memoWrapper.type as any).displayName = "GIFFavButton";
return true;
}
function patchAddFavorite(): void {
const favModule = findByProps("addFavoriteGIF");
if (!favModule) return;
unpatchAddFavorite = before("addFavoriteGIF", favModule, (args: any[]) => {
const data = args[0];
if (!data || typeof data !== "object") return;
const url = data.url || "";
const src = data.src || "";
if (isVideo(url) || isVideo(src)) {
data.format = 2;
} else if (data.format === 2) {
data.format = 1;
}
});
}
function patchMobileFavorites(): void {
const mod = findByProps("useFavoriteGIFsMobile");
if (!mod) return;
favMobileModule = mod;
origUseFavoriteGIFsMobile = mod.useFavoriteGIFsMobile;
let lastFavs: any = null;
mod.useFavoriteGIFsMobile = function (...args: any[]) {
const result = (origUseFavoriteGIFsMobile as Function).apply(this, args);
if (result?.favorites && Array.isArray(result.favorites) && result.favorites !== lastFavs) {
lastFavs = result.favorites;
for (const item of result.favorites) {
if (!item || processed.has(item)) continue;
processed.add(item);
if (isVideo(item.url) || isVideo(item.src)) {
item.src = makeVideoThumbnail(item.src || item.url);
}
}
}
return result;
};
}
export default defineCorePlugin({
manifest: {
id: "bunny.favouriteanything",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "FavouriteAnything",
description: "Adds the favourite button to ALL media, not just GIFs",
authors: [{ name: "TheUnrealZaka" }],
},
},
start() {
if (applyPatch()) {
logger.log("[FavouriteAnything] Patched GIFFavButton.");
} else {
let retries = 0;
const tryPatch = () => {
if (applyPatch()) {
logger.log("[FavouriteAnything] Patched GIFFavButton (after retry).");
retryTimeout = null;
} else if (retries++ < 50) {
retryTimeout = setTimeout(tryPatch, 300);
} else {
logger.error("[FavouriteAnything] GIFFavButton not found after retries.");
}
};
retryTimeout = setTimeout(tryPatch, 300);
}
patchAddFavorite();
patchMobileFavorites();
},
stop() {
if (retryTimeout) { clearTimeout(retryTimeout); retryTimeout = null; }
if (memoWrapper && origType) {
memoWrapper.type = origType;
origType = null;
memoWrapper = null;
}
if (unpatchAddFavorite) { unpatchAddFavorite(); unpatchAddFavorite = null; }
if (favMobileModule && origUseFavoriteGIFsMobile) {
favMobileModule.useFavoriteGIFsMobile = origUseFavoriteGIFsMobile;
origUseFavoriteGIFsMobile = null;
favMobileModule = null;
}
},
});

View file

@ -42,19 +42,19 @@ function transformLinks(content: string, config: FixEmbedSettings): string {
if (config.instagram) { if (config.instagram) {
result = result.replace( result = result.replace(
/https?:\/\/(?:www\.)?instagram\.com(\/[^\s]+)/gi, /https?:\/\/(?:www\.)?instagram\.com(\/[^\s]+)/gi,
"https://uuinstagram.com$1", "https://kkinstagram.com$1",
); );
} }
// TikTok: Replace domain, keep path // TikTok: Replace domain, keep path
if (config.tiktok) { if (config.tiktok) {
result = result.replace( result = result.replace(
/https?:\/\/(?:www\.)?tiktok\.com(\/[^\s]+)/gi, /https?:\/\/(?:www\.)?tiktok\.com(\/[^\s]+)/gi,
"https://tnktok.com$1", "https://tiktokez.com$1",
); );
// TikTok short links (vm.tiktok.com) // TikTok short links (vm.tiktok.com)
result = result.replace( result = result.replace(
/https?:\/\/vm\.tiktok\.com\/([A-Za-z0-9]+)\/?/gi, /https?:\/\/vm\.tiktok\.com\/([A-Za-z0-9]+)\/?/gi,
"https://vm.tnktok.com/$1", "https://vm.tiktokez.com/$1",
); );
} }
// Reddit: Replace domain, keep path // Reddit: Replace domain, keep path

View file

@ -34,8 +34,12 @@ export const getCorePlugins = (): CorePluginRecord => ({
"bunny.quickinstall": makeLazyPlugin(() => require("./quickinstall"), true), "bunny.quickinstall": makeLazyPlugin(() => require("./quickinstall"), true),
"bunny.badges": makeLazyPlugin(() => require("./badges")), "bunny.badges": makeLazyPlugin(() => require("./badges")),
"bunny.notrack": makeLazyPlugin(() => require("./notrack")), "bunny.notrack": makeLazyPlugin(() => require("./notrack")),
"bunny.messagefix": makeLazyPlugin(() => require("./messagefix")),
"bunny.fixembed": makeLazyPlugin(() => require("./fixembed")), "bunny.fixembed": makeLazyPlugin(() => require("./fixembed")),
"bunny.favouriteanything": makeLazyPlugin(() => require("./favouriteanything")),
"bunny.nodelete": makeLazyPlugin(() => require("./nodelete")),
"bunny.maxverstappen": makeLazyPlugin(() => require("./maxverstappen")),
}); });
/** /**

Binary file not shown.

View file

@ -0,0 +1,145 @@
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { logger } from "@lib/utils/logger";
const { ReactNative } = window as any;
const { DCDSoundManager } = ReactNative?.NativeModules || {};
const AUDIO_URL = "https://github.com/OTKUSteyler/GoonCord/raw/refs/heads/main/src/core/plugins/maxverstappen/Max%20Verstappen.mp3";
const SOUND_ID = 6972;
const TRIGGER_CHANCE = 0.33;
const MAX_PATTERN = /\bmax\s*verst[ae]pp?en\b/i;
let isPlaying = false;
let isPrepared = false;
let isPreparing = false;
let soundDuration = 30000;
let timeoutId: NodeJS.Timeout | null = null;
let onMessageCreate: ((e: any) => void) | null = null;
let onMessageSend: ((e: any) => void) | null = null;
function shouldTrigger(): boolean {
return Math.random() < TRIGGER_CHANCE;
}
function checkText(text: string): boolean {
if (!text) return false;
return MAX_PATTERN.test(text) && shouldTrigger();
}
function ensurePrepared(): Promise<number> {
return new Promise((resolve) => {
if (isPrepared) return resolve(soundDuration);
if (isPreparing) return resolve(-1);
isPreparing = true;
DCDSoundManager.prepare(AUDIO_URL, "music", SOUND_ID, (error: any, sound: any) => {
isPreparing = false;
if (error) {
logger.error("[MaxVerstappen] Failed to prepare:", error);
return resolve(-1);
}
isPrepared = true;
soundDuration = sound?.duration || 30000;
resolve(soundDuration);
});
});
}
async function playAudio() {
if (!DCDSoundManager) return;
if (isPlaying) return;
const duration = await ensurePrepared();
if (duration === -1) return;
isPlaying = true;
try {
await DCDSoundManager.play(SOUND_ID);
timeoutId = setTimeout(() => {
DCDSoundManager.stop(SOUND_ID);
isPlaying = false;
timeoutId = null;
}, soundDuration);
} catch (e) {
logger.error("[MaxVerstappen] Playback error:", e);
isPlaying = false;
}
}
export default defineCorePlugin({
manifest: {
id: "bunny.maxverstappen",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Max Verstappen",
description: "Plays audio when Max Verstappen is mentioned (33% chance)",
authors: [{ name: "GoonCord Team" }],
},
},
start() {
if (!DCDSoundManager) {
logger.error("[MaxVerstappen] DCDSoundManager not found");
return;
}
const FluxDispatcher = findByProps("dispatch", "subscribe");
if (!FluxDispatcher) {
logger.error("[MaxVerstappen] FluxDispatcher not found");
return;
}
onMessageCreate = ({ message }: any) => {
try {
if (!message?.content) return;
if (checkText(message.content)) playAudio();
} catch (e) {
logger.error("[MaxVerstappen] MESSAGE_CREATE error:", e);
}
};
onMessageSend = ({ message }: any) => {
try {
if (!message?.content) return;
if (checkText(message.content)) playAudio();
} catch (e) {
logger.error("[MaxVerstappen] MESSAGE_SEND error:", e);
}
};
FluxDispatcher.subscribe("MESSAGE_CREATE", onMessageCreate);
FluxDispatcher.subscribe("MESSAGE_SEND", onMessageSend);
logger.log("[MaxVerstappen] Enabled");
},
stop() {
if (DCDSoundManager && isPlaying) {
DCDSoundManager.stop(SOUND_ID);
isPlaying = false;
}
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
const FluxDispatcher = findByProps("dispatch", "subscribe");
if (FluxDispatcher) {
if (onMessageCreate) FluxDispatcher.unsubscribe("MESSAGE_CREATE", onMessageCreate);
if (onMessageSend) FluxDispatcher.unsubscribe("MESSAGE_SEND", onMessageSend);
}
onMessageCreate = null;
onMessageSend = null;
isPrepared = false;
isPreparing = false;
soundDuration = 30000;
logger.log("[MaxVerstappen] Disabled");
},
});

View file

@ -1,56 +0,0 @@
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { logger } from "@lib/utils/logger";
// Find the MessageActions module
const MessageActions = findByProps("sendMessage");
let originalSendMessage: Function;
export default defineCorePlugin({
manifest: {
id: "bunny.messagefix",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "MessageFix",
description: "Ensures messages include the required nonce parameter",
authors: [{ name: "Win8.1VMUser (plugin by ShiggyCord Team)" }],
},
},
start() {
// Save original function
originalSendMessage = MessageActions.sendMessage;
// Replace with our implementation
MessageActions.sendMessage = function (
channelId,
message,
replyRef,
options,
) {
// Ensure options exists and has a nonce
options = options || {};
options.nonce = options.nonce || (BigInt(Date.now() - 1420070400000) << 22n).toString();
// Call original with fixed parameters
return originalSendMessage.call(
this,
channelId,
message,
replyRef,
options,
);
};
logger.log("MessageFix: Enabled - adding nonce to all messages");
},
stop() {
// Restore original function
if (originalSendMessage) MessageActions.sendMessage = originalSendMessage;
logger.log("MessageFix: Disabled");
},
});

View file

@ -0,0 +1,76 @@
// src/core/plugins/NoDelete/index.ts
import { defineCorePlugin } from "..";
import { findByStoreName } from "@metro";
import { before } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
import { FluxDispatcher } from "@metro/common";
type PatchCleanupFn = () => void;
let patches: PatchCleanupFn[] = [];
let MessageStore: any = null;
let deleteable: string[] = [];
function patchDispatcher(): PatchCleanupFn {
return before("dispatch", FluxDispatcher, (args: any[]) => {
try {
if (!MessageStore) MessageStore = findByStoreName("MessageStore");
const event = args[0];
if (!event || event?.type !== "MESSAGE_DELETE") return;
if (!event?.id || !event?.channelId) return;
if (deleteable.includes(event.id)) {
deleteable.splice(deleteable.indexOf(event.id), 1);
return;
}
deleteable.push(event.id);
args[0] = {
type: "MESSAGE_EDIT_FAILED_AUTOMOD",
messageData: {
type: 1,
message: {
channelId: event.channelId,
messageId: event.id,
},
},
errorResponseBody: {
code: 200000,
message: "[deleted]",
},
};
} catch (e) {
logger.error("NoDelete: dispatcher patch failed", e);
}
});
}
export default defineCorePlugin({
manifest: {
id: "bunny.nodelete",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "NoDelete",
description: "Prevents messages from being deleted from your view",
authors: [{ name: "meqativ" }],
},
},
start() {
patches = [patchDispatcher()].filter(Boolean);
logger.log("NoDelete: Enabled");
},
stop() {
patches.forEach((p) => p?.());
patches = [];
deleteable = [];
MessageStore = null;
logger.log("NoDelete: Disabled");
},
});

View file

@ -0,0 +1,90 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage";
import { showConfirmationAlert } from "@vendetta/ui/alerts";
import { Forms } from "@vendetta/ui/components";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { getTranslation } from "./translations.js";
import ItemWithRemove from "../../common/ui/ItemWithRemove.jsx";
import { findByStoreName, findByProps } from "@vendetta/metro";
let UserStore, UncachedUserManager, Profiles;
export default (props) => {
UserStore ??= findByStoreName("UserStore");
UncachedUserManager ??= findByProps("fetchProfile", "getUser", "setFlag");
Profiles ??= findByProps("showUserProfile");
async function openProfile(userId) {
const show = Profiles.showUserProfile;
UserStore.getUser(userId) ? show({ userId }) : UncachedUserManager.getUser(userId).then(({ id }) => show({ userId: id }));
}
useProxy(storage);
const [users, setUsers] = React.useState(storage["ignore"]["users"]);
const handleRemoveUser = (userId) => {
const newArr = users.filter((id) => id !== userId);
storage["ignore"].users = newArr;
setUsers(newArr);
};
const handleClearUsers = () => {
storage["ignore"].users = [];
setUsers([]);
};
let uncached = 0;
return (
<ReactNative.ScrollView style={{ flex: 1 }}>
<Forms.FormSection title={getTranslation("settings.titles.settings")} titleStyleType="no_border">
<Forms.FormRow label={getTranslation("settings.showTimestamps")} trailing={<Forms.FormSwitch value={storage.timestamps} onValueChange={(v) => (storage.timestamps = v)} />} />
<Forms.FormRow label={getTranslation("settings.ewTimestampFormat")} trailing={<Forms.FormSwitch value={storage["ew"]} onValueChange={(v) => (storage.ew = v)} />} />
<Forms.FormDivider />
<Forms.FormRow label={getTranslation("settings.youDeletedItWarning")} />
</Forms.FormSection>
<Forms.FormSection title={getTranslation("settings.titles.filters")}>
<Forms.FormRow label={getTranslation("settings.ignoreBots")} trailing={<Forms.FormSwitch value={storage["ignore"].bots} onValueChange={(value) => (storage["ignore"].bots = value)} />} />
<Forms.FormRow
label={getTranslation("settings.clearUsersLabel", true)?.make?.(users.length)}
trailing={<Forms.FormRow.Icon source={getAssetIDByName("ic_trash_24px")} />}
onPress={() => {
if (users.length !== 0)
showConfirmationAlert({
title: getTranslation("settings.confirmClear.title"),
content: getTranslation("settings.confirmClear.description", true)?.make?.(users.length),
confirmText: getTranslation("settings.confirmClear.yes"),
cancelText: getTranslation("settings.confirmClear.no"),
confirmColor: "brand",
onConfirm: handleClearUsers,
});
}}
/>
<ReactNative.ScrollView style={{ flex: 1, gap: 3, marginLeft: 15 }}>
{users.map((id) => {
const User = UserStore.getUser(id) ?? {};
let pfp = User?.getAvatarURL?.(null,26)?.replace?.(/\.(gif|webp)/, ".png");
if (!pfp) {
pfp = "https://cdn.discordapp.com/embed/avatars/1.png?size=48";
User.username = `${id} Uncached`;
User.discriminator = "0";
if (uncached === 0) User.username += ", press the avatar";
uncached++;
}
return (
<ItemWithRemove
imageSource={{ uri: pfp }}
onImagePress={() => {
openProfile(id);
}}
onRemove={() => handleRemoveUser(id)}
label={User.username + (User.discriminator == 0 ? "" : `#${User.discriminator}`)}
labelRemove={getTranslation("settings.removeUserButton")}
/>
);
})}
</ReactNative.ScrollView>
<Forms.FormDivider />
<Forms.FormRow label={getTranslation("settings.addUsersInfo")} />
</Forms.FormSection>
</ReactNative.ScrollView>
);
};

View file

@ -0,0 +1,158 @@
function slavicNoun(number, $1, $2, $5) {
let n = Math.abs(number);
n %= 100;
if (n >= 5 && n <= 20) return $5;
n %= 10;
if (n === 1) return $1;
if (n >= 2 && n <= 4) return $2;
return $5;
}
function latinNoun(number, $1, $x) {
return number === 1 ? $1 : $x;
}
export let massive = {};
massive = {
settings: {
titles: {
settings: {
"en-GB": "Settings",
uk: "Налаштування",
ru: "Настройки",
},
filters: {
"en-GB": "Filters",
uk: "Фільтри",
ru: "Фильтры",
},
},
showTimestamps: {
"en-GB": "Show the time of deletion",
uk: "Показувати час видалення",
ru: "Показывать время удаления",
},
ewTimestampFormat: {
"en-GB": "Use 12-hour format",
uk: "Використовувати 12-годинний формат",
ru: "Использовать 12-часовой формат",
},
youDeletedItWarning: {
"en-GB": "The messages YOU deleted - are not saved",
uk: "Повідомлення які видалили ВИ - не зберігаются",
ru: "Сообщения удаленные ВАМИ - не сохраняются",
},
addUsersInfo: {
"en-GB": () =>
`To add or remove users from the ignore list, follow these steps:\n` +
`1. open their profile\n` +
`2. press the •••\n` +
`3. press "${massive.optionLabels[0]["en-GB"]}"\n` +
`4. 🎉`,
uk: () =>
`Щоб добавити когось до списку ігнорованих користувачів, виконайте ці дії:\n` +
`1. відкрите їх профіль\n` +
`2. натисніть •••\n` +
`3. натисніть "${massive.optionLabels[0]["uk"]}"\n` +
`4. 🎉`,
ru: () =>
`Чтобы добавить кого-то в список игнорированных пользователей - следуйте этим шагам\n` +
`1. откройте их профиль\n` +
`2. нажмите •••\n` +
`3. нажмите "${massive.optionLabels[0]["ru"]}"\n` +
`4. 🎉`,
},
ignoreBots: {
"en-GB": "Ignore bots",
uk: "Ігнорувати ботів",
ru: "Игнорировать ботов",
},
clearUsersLabel: {
"en-GB": (amount) =>
`You have ${amount} user${latinNoun(amount, "", "s")} in the ignored users list`,
uk: (amount) =>
`Ви маєте ${amount} користувач${slavicNoun(amount, "а", "а", "ів")} у списку ігнорованих користувачів`,
ru: (amount) =>
`Вы имеете ${amount} пользовател${slavicNoun(amount, "я", "я", "ей")} в списке игнорированных пользователей`,
},
confirmClear: {
title: {
"en-GB": "Hold on!",
uk: "Почекай-но!",
ru: "Положди-ка!",
},
description: {
"en-GB": (amount) =>
`This will remove ${amount} user${latinNoun(amount, "", "s")} from the ignored users list.\nDo you you really want to do that?`,
uk: (amount) =>
`Це прибере ${amount} користувач${slavicNoun(amount, "а", "а", "ів")} зі списку ігнорованих користувачів.\nВи дійсно хочете продовжити?`,
ru: (amount) =>
`Это уберет ${amount} пользовател${slavicNoun(amount, "я", "я", "ей")} из списка игнорированных пользователей.\nВы действительно хотите продолжить?`,
},
yes: {
"en-GB": "Yes",
uk: "Так",
ru: "Да",
},
no: {
"en-GB": "Cancel",
uk: "Відминити",
ru: "Отменить",
},
},
removeUserButton: {
"en-GB": "REMOVE",
uk: "ПРИБРАТИ",
ru: "УБРАТЬ",
},
},
optionLabels: [
{
"en-GB": "Add to NoDelete ignored users list",
uk: "Добавити до списку ігнорованих користувачів у NoDelete",
ru: "Добавить в список игнорированных пользователей в NoDelete",
},
{
"en-GB": "Remove from the NoDelete ignore list",
uk: "Прибрати з списку ігнорованих користувачів у NoDelete",
ru: "Убрать из списка игнорированных пользователей в NoDelete",
},
],
toastLabels: [
{
"en-GB": "Added {user} to the ignored users list",
uk: "{user} добавлено до списку ігнорованих користувачів",
ru: "{user} добавлены в список игнорированных пользователей",
},
{
"en-GB": "Removed {user} from the ignored users list",
uk: "{user} прибрано зі списку ігнорованих користувачів",
ru: "{user} убраны из списка игнорированных пользователей",
},
],
thisMessageWasDeleted: {
"en-GB": "This message was deleted",
uk: "Це повідомлення було видалено",
ru: "Это сообщение было удалено",
},
};
export const locale = () =>
vendetta.metro.findByStoreName("LocaleStore").locale;
const defaultLocale = "en-GB";
export function getTranslation(find, fn) {
let value = massive;
for (const key of find.split(".")) {
// biome-ignore lint/suspicious/noPrototypeBuiltins: meow
if (value?.hasOwnProperty(key)) {
value = value[key];
} else {
value = massive;
}
}
if (value === massive) return find;
let localized = value[locale()] ?? value[defaultLocale] ?? find;
if (typeof localized === "function" && !fn) localized = localized();
return fn ? { make: localized } : localized;
}

View file

@ -26,48 +26,51 @@ const useStyles = createStyles({
card: { card: {
backgroundColor: semanticColors?.CARD_SECONDARY_BG, backgroundColor: semanticColors?.CARD_SECONDARY_BG,
borderRadius: 12, borderRadius: 12,
overflow: "hidden", overflow: "hidden"
}, },
header: { header: {
padding: 0, padding: 0,
}, },
headerLeading: { headerLeading: {
flex: 1,
flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
scale: 1.2, scale: 1.2
}, },
headerTrailing: { headerTrailing: {
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
gap: 15, gap: 15,
alignItems: "center", alignItems: "center"
}, },
headerLabel: { headerLabel: {
...TextStyleSheet["heading-md/semibold"], ...TextStyleSheet["heading-lg/semibold"],
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY, },
headerLabelCompact: {
...TextStyleSheet["text-md/semibold"],
}, },
headerSubtitle: { headerSubtitle: {
...TextStyleSheet["text-md/semibold"], ...TextStyleSheet["text-sm/semibold"],
color: semanticColors.TEXT_MUTED, color: semanticColors.TEXT_MUTED,
}, },
descriptionLabel: { descriptionLabel: {
...TextStyleSheet["text-md/semibold"], ...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_NORMAL, color: "text-strong",
}, },
actions: { actions: {
flexDirection: "row-reverse", flexDirection: "row-reverse",
alignItems: "center", alignItems: "center",
gap: 5, gap: 5
}, },
iconStyle: { iconStyle: {
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT, tintColor: semanticColors.LOGO_PRIMARY,
opacity: 0.2, opacity: 0.2,
height: 64, height: 64,
width: 64, width: 64,
left: void 0, left: void 0,
right: "30%", right: "30%",
top: "-10%", top: "-10%"
}, }
}); });
interface Action { interface Action {
@ -108,9 +111,9 @@ export default function AddonCard(props: CardProps) {
<Stack spacing={16}> <Stack spacing={16}>
<View style={{ flexDirection: "row", alignItems: "center" }}> <View style={{ flexDirection: "row", alignItems: "center" }}>
<View style={styles.headerLeading}> <View style={styles.headerLeading}>
<Text style={styles.headerLabel}>{props.headerLabel}</Text> <Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerLabel}>{props.headerLabel}</Text>
{props.headerSublabel && ( {props.headerSublabel && (
<Text style={styles.headerSubtitle}>{props.headerSublabel}</Text> <Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerSubtitle}>{props.headerSublabel}</Text>
)} )}
</View> </View>
<View style={[styles.headerTrailing, { marginLeft: "auto" }]}> <View style={[styles.headerTrailing, { marginLeft: "auto" }]}>

View file

@ -19,7 +19,6 @@ import {
TableSwitchRow, TableSwitchRow,
Stack, Stack,
} from "@metro/common/components"; } from "@metro/common/components";
import { semanticColors } from "@ui/color";
import { ScrollView, View } from "react-native"; import { ScrollView, View } from "react-native";
import ErrorComponentStackCard from "./ErrorComponentStackCard"; import ErrorComponentStackCard from "./ErrorComponentStackCard";
@ -74,8 +73,8 @@ export default function ErrorBoundaryScreen(props: {
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}> <ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
<Text <Text
variant="heading-md/extrabold" variant="heading-md/extrabold"
color="text-default"
style={{ style={{
color: semanticColors.HEADER_PRIMARY,
marginBottom: 8, marginBottom: 8,
textAlign: "center", textAlign: "center",
}} }}

View file

@ -11,7 +11,7 @@ export { shiggycordIcon };
export default function initSettings() { export default function initSettings() {
registerSection({ registerSection({
name: "ShiggyCord", name: "GoonCord",
items: [ items: [
{ {
key: "SHIGGYCORD", key: "SHIGGYCORD",

View file

@ -97,7 +97,7 @@ export default function About() {
variant="text-md/medium" variant="text-md/medium"
style={{ textAlign: "center", marginTop: 4 }} style={{ textAlign: "center", marginTop: 4 }}
> >
Shiggy your discord client! 💦Goon💦 your discord client!
</Text> </Text>
</View> </View>

View file

@ -125,7 +125,7 @@ export default function General() {
> >
<TableRowGroup title="App Information"> <TableRowGroup title="App Information">
<TableRow <TableRow
label="ShiggyCord" label="GoonCord"
icon={<TableRow.Icon source={{ uri: shiggycordIcon ?? "" }} />} icon={<TableRow.Icon source={{ uri: shiggycordIcon ?? "" }} />}
trailing={<TableRow.TrailingText text={debugInfo.bunny.version} />} trailing={<TableRow.TrailingText text={debugInfo.bunny.version} />}
/> />
@ -144,7 +144,7 @@ export default function General() {
subLabel={`${debugInfo.bunny.loader.name} loader`} subLabel={`${debugInfo.bunny.loader.name} loader`}
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />} icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
trailing={ trailing={
<TableRow.TrailingText text={debugInfo.bunny.loader.version} /> <TableRow.TrailingText text={debugInfo.bunny.loader.version ?? ""} />
} }
/> />
</TableRowGroup> </TableRowGroup>
@ -200,7 +200,7 @@ export default function General() {
<TableSwitchRow <TableSwitchRow
label={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS} label={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS}
subLabel={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC} subLabel={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC}
icon={<TableRow.Icon source={findAssetId("FlaskIcon")} />} icon={<TableRow.Icon source={findAssetId("StaffBadgeIcon")} />}
value={settings.enableDiscordDeveloperSettings} value={settings.enableDiscordDeveloperSettings}
onValueChange={(v: boolean) => { onValueChange={(v: boolean) => {
settings.enableDiscordDeveloperSettings = v; settings.enableDiscordDeveloperSettings = v;

View file

@ -33,8 +33,6 @@ import {
Text, Text,
} from "@metro/common/components"; } from "@metro/common/components";
import { View, Image } from "react-native"; import { View, Image } from "react-native";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { BundleUpdaterManager } from "@lib/api/native/modules";
const { useReducer } = React; const { useReducer } = React;
@ -352,58 +350,12 @@ function BrowseThemePage(props: BrowseThemePageProps) {
); );
} }
/**
* Theme options have been changed from radio groups to individual switches
* for better UX. Each option can now be toggled independently.
*/
export default function Themes() { export default function Themes() {
useProxy(settings); useProxy(settings);
useProxy(themes); useProxy(themes);
const navigation = NavigationNative.useNavigation(); const navigation = NavigationNative.useNavigation();
const [mode, setMode] = React.useState<"installed" | "browse">("installed"); const [mode, setMode] = React.useState<"installed" | "browse">("installed");
// Track the theme that was active when the user entered the Themes page.
// If it changes while the user stays on this page, we'll prompt to reload when they leave.
const initialThemeIdRef = React.useRef<string | null>(getCurrentTheme()?.id ?? null);
React.useEffect(() => {
const beforeRemoveHandler = (e: any) => {
try {
const currentId = getCurrentTheme()?.id ?? null;
if (currentId !== initialThemeIdRef.current) {
// Notify the user that a reload is required for full application of theme changes
showConfirmationAlert({
title: Strings.MODAL_RELOAD_THEME_CHANGE,
content: Strings.MODAL_THEME_REFETCHED_DESC,
confirmText: Strings.RELOAD,
cancelText: Strings.CANCEL,
confirmColor: "red",
onConfirm: () => {
try {
BundleUpdaterManager.reload();
} catch (err) {
try {
(global as any).NativeModules?.BundleUpdaterManager?.reload?.();
} catch (_) {}
}
},
});
}
} catch (_) {
// swallow any unexpected errors — we don't want to block navigation
}
};
const unsub = navigation?.addListener ? navigation.addListener("beforeRemove", beforeRemoveHandler) : undefined;
return () => {
try {
if (unsub && typeof unsub === "function") unsub();
else if (unsub && typeof unsub.remove === "function") unsub.remove();
} catch (_) {}
};
}, [navigation]);
const [themesList, setThemesList] = React.useState<ThemeData[]>([]); const [themesList, setThemesList] = React.useState<ThemeData[]>([]);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);

View file

@ -11,7 +11,7 @@ async function initializeShiggyCord() {
Object.freeze = Object.seal = Object; Object.freeze = Object.seal = Object;
await require("@metro/internals/caches").initMetroCache(); await require("@metro/internals/caches").initMetroCache();
await require(".").default(); require(".").default();
} catch (e) { } catch (e) {
const { ClientInfoManager } = require("@lib/api/native/modules"); const { ClientInfoManager } = require("@lib/api/native/modules");
const stack = e instanceof Error ? e.stack : undefined; const stack = e instanceof Error ? e.stack : undefined;

View file

@ -11,13 +11,8 @@ import { injectFluxInterceptor } from "@lib/api/flux";
import { patchJsx } from "@lib/api/react/jsx"; import { patchJsx } from "@lib/api/react/jsx";
import { logger } from "@lib/utils/logger"; import { logger } from "@lib/utils/logger";
import { patchSettings } from "@ui/settings"; import { patchSettings } from "@ui/settings";
import { updaterSettings } from "@lib/api/settings";
import { InteractionManager } from "react-native"; import { InteractionManager } from "react-native";
import { getDebugInfo, initDebugger } from "@lib/api/debug"; import { getDebugInfo, initDebugger } from "@lib/api/debug";
// Debug toggle helper (temporary runtime fallback). The helper is dynamically
// imported when needed (to avoid bundling it permanently) and removed after use.
import * as lib from "./lib"; import * as lib from "./lib";
import { timings } from "@lib/utils/timings"; import { timings } from "@lib/utils/timings";
@ -66,52 +61,19 @@ export default async () => {
const { VdPluginManager } = await import("@core/vendetta/plugins"); const { VdPluginManager } = await import("@core/vendetta/plugins");
const { initPlugins, updatePlugins } = await import("@lib/addons/plugins"); const { initPlugins, updatePlugins } = await import("@lib/addons/plugins");
// Initialize Vendetta plugins (may start many plugins) — do not block UI. await Promise.all([
VdPluginManager.initPlugins() VdPluginManager.initPlugins()
.then((u) => lib.unload.push(u)) .then((u) => u && lib.unload.push(u))
.catch((e) => logger.log("Vendetta init failed:", e)); .catch((e) => logger.log("Vendetta init failed:", e)),
initPlugins()
.catch((e) => logger.log("initPlugins failed:", e)),
]);
// Start ShiggyCord (Bunny) plugins now without forcing repository updates. updatePlugins()
// Plugin repository fetching is deferred so the app can finish launching first. .catch((e) => logger.log("updatePlugins failed:", e));
// Stagger plugin startup to reduce CPU/memory spikes: use smaller batches and a small interval.
// This keeps the UI responsive while plugins initialize in the background.
initPlugins({ staggerInterval: 500, batchSize: 2 });
// Attempt a lightweight recovery toggle if some core plugins failed to start.
try {
// Dynamically import the helper (if present) but suppress verbose errors.
const mod = await import("@core/debug/toggleCorePlugins").catch(
() => null,
);
if (mod?.default) {
// Run helper with minimal noise; ignore failures.
mod.default({ offDuration: 1500 }).catch(() => {});
}
// Try to remove the helper source file (best-effort, ignore failures).
await import("@lib/api/native/fs")
.then((fs) => fs.removeFile("src/core/debug/toggleCorePlugins.ts"))
.catch(() => {});
} catch {
// suppressed
}
// Update fonts in background // Update fonts in background
updateFonts().catch((e) => logger.log("updateFonts failed:", e)); updateFonts().catch((e) => logger.log("updateFonts failed:", e));
// Schedule plugin repository update after a delay (5 minutes) so update work
// does not impact initial launch performance.
setTimeout(
() => {
updatePlugins().catch((e) =>
logger.log("updatePlugins failed (deferred 5min):", e),
);
},
5 * 60 * 1000,
);
// Note: we intentionally moved the call to `updatePlugins()` above so core
// plugins are registered prior to calling `initPlugins()`.
}; };
// Preferred: wait until interactions finish (animations / navigation). // Preferred: wait until interactions finish (animations / navigation).

View file

@ -752,27 +752,54 @@ export async function updatePlugins() {
}), }),
); );
} }
const enabledIds = [...registeredPlugins.keys()].filter(
(id) => isPluginEnabled(id) && !pluginInstances.has(id),
);
await Promise.allSettled(
enabledIds.map(async (id) => {
try {
await startPlugin(id).catch((e) => {
console.error(`Failed to start plugin ${id}:`, e);
});
} catch (error) {
console.error(`Unexpected error while starting plugin ${id}:`, error);
}
}),
);
} }
/** /**
* Initialize plugins but stagger startup to avoid CPU spikes during app start. * Register all installed plugins from local storage manifests without any
* network fetch. This lets initPlugins() start plugins immediately using
* previously-cached scripts, keeping startup snappy.
* *
* This replaces the original "start all enabled plugins in parallel" approach * Safe to call multiple times — skips already-registered plugins.
* with a configurable staggered startup: plugins are started in small batches and
* we await a small interval between batches. This reduces short-lived CPU and I/O contention
* that causes jank on low-end devices.
*
* Backwards-compatible: callers can still call initPlugins() with no arguments.
*
* Options:
* - staggerInterval (ms) - wait time between batches (default: 100)
* - batchSize - how many plugins to start concurrently in a single batch (default: 3)
*/ */
export async function initPlugins( export async function registerInstalledPlugins() {
options: { staggerInterval?: number; batchSize?: number } = {}, await awaitStorage(pluginRepositories, pluginSettings);
) {
const { staggerInterval = 100, batchSize = 3 } = options;
const ids = Object.keys(pluginSettings ?? {});
if (ids.length === 0) return;
// Load all manifest files from local storage in parallel.
const results = await Promise.allSettled(
ids.map(async (id) => {
if (registeredPlugins.has(id) || isCorePlugin(id)) return;
const loaded = await preloadStorageIfExists(`plugins/manifests/${id}.json`);
if (loaded) {
const manifest = getPreloadedStorage<t.BunnyPluginManifest>(
`plugins/manifests/${id}.json`,
);
if (manifest) {
registeredPlugins.set(id, manifest);
}
}
}),
);
}
export async function initPlugins() {
// Ensure core plugins are registered prior to any plugin startup logic so // Ensure core plugins are registered prior to any plugin startup logic so
// settings pages and UI that read registered/core plugins show them. // settings pages and UI that read registered/core plugins show them.
// registerCorePlugins is synchronous and idempotent, but call it here as a // registerCorePlugins is synchronous and idempotent, but call it here as a
@ -784,6 +811,7 @@ export async function initPlugins(
} }
await awaitStorage(pluginRepositories, pluginSettings); await awaitStorage(pluginRepositories, pluginSettings);
await registerInstalledPlugins();
// Collect enabled plugin ids // Collect enabled plugin ids
const enabledIds = [...registeredPlugins.keys()].filter((id) => const enabledIds = [...registeredPlugins.keys()].filter((id) =>
@ -792,21 +820,13 @@ export async function initPlugins(
if (enabledIds.length === 0) return; if (enabledIds.length === 0) return;
// Helper sleep // Start all enabled plugins in parallel
const sleep = (ms: number) => new Promise<void>((res) => setTimeout(res, ms));
// Start plugins in batches to avoid starting all at once.
for (let i = 0; i < enabledIds.length; i += batchSize) {
const batch = enabledIds.slice(i, i + batchSize);
// Start the batch in parallel but don't let exceptions bubble to halt everything.
await Promise.allSettled( await Promise.allSettled(
batch.map(async (id) => { enabledIds.map(async (id) => {
try { try {
// Only attempt to start if still enabled and not already started // Only attempt to start if still enabled and not already started
if (!isPluginEnabled(id) || pluginInstances.has(id)) return; if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
await startPlugin(id).catch((e) => { await startPlugin(id).catch((e) => {
// startPlugin may throw; ensure it doesn't crash the whole batch
console.error(`Failed to start plugin ${id}:`, e); console.error(`Failed to start plugin ${id}:`, e);
}); });
} catch (error) { } catch (error) {
@ -814,10 +834,4 @@ export async function initPlugins(
} }
}), }),
); );
// If there are more plugins to start, wait before starting the next batch.
if (i + batchSize < enabledIds.length) {
await sleep(staggerInterval);
}
}
} }

View file

@ -13,35 +13,51 @@ const Messages = findByFilePathLazy("modules/messages/native/Messages.tsx", true
function ThemeBackground({ children }: { children: React.ReactNode; }) { function ThemeBackground({ children }: { children: React.ReactNode; }) {
useObservable([colorsPref]); useObservable([colorsPref]);
if (!_colorRef.current const background = _colorRef.current?.background;
|| colorsPref.customBackground === "hidden" const url = background?.url;
|| !_colorRef.current.background?.url const blur = background?.blur;
|| _colorRef.current.background?.blur && (typeof _colorRef.current.background?.blur !== "number")
if (
!_colorRef.current ||
colorsPref.customBackground === "hidden" ||
!url
) { ) {
return children; return children;
} }
return <ImageBackground if (blur !== undefined && typeof blur !== "number") {
return children;
}
return (
<ImageBackground
style={{ flex: 1, height: "100%" }} style={{ flex: 1, height: "100%" }}
source={{ uri: _colorRef.current.background?.url }} source={{ uri: url }}
blurRadius={_colorRef.current.background?.blur} blurRadius={typeof blur === "number" ? blur : 0}
> >
{children} {children}
</ImageBackground>; </ImageBackground>
);
} }
export default function patchChatBackground() { export default function patchChatBackground() {
try { try {
if (!Messages) return;
const patches = [ const patches = [
after("render", Messages, (_, ret) => { after("render", Messages, (_, ret) => {
if (!_colorRef.current || !_colorRef.current.background?.url) return; if (!ret) return ret;
try {
if (!_colorRef.current || !_colorRef.current.background?.url) return ret;
const messagesComponent = findInReactTree( const messagesComponent = findInReactTree(
ret, ret,
x => x && "HACK_fixModalInteraction" in x.props && x?.props?.style x => x && "HACK_fixModalInteraction" in (x.props ?? {}) && x?.props?.style
); );
if (messagesComponent) { if (messagesComponent) {
try {
const flattened = StyleSheet.flatten(messagesComponent.props.style); const flattened = StyleSheet.flatten(messagesComponent.props.style);
const backgroundColor = getChroma()( const backgroundColor = getChroma()(
flattened.backgroundColor || "black" flattened.backgroundColor || "black"
@ -53,13 +69,21 @@ export default function patchChatBackground() {
messagesComponent.props.style, messagesComponent.props.style,
{ backgroundColor } { backgroundColor }
]); ]);
} catch (e) {
logger.warn("Background patch: failed to modify style", e);
}
}
} catch (e) {
logger.warn("Background patch: render error", e);
return ret;
} }
return <ThemeBackground>{ret}</ThemeBackground>; return <ThemeBackground>{ret}</ThemeBackground>;
}) })
]; ];
return () => patches.forEach(x => x()); return () => patches.forEach(x => x?.());
} catch (e) { } catch (e) {
logger.error("Background patch: failed to initialize", e);
} }
} }

View file

@ -2,12 +2,14 @@ import { _colorRef } from "@lib/addons/themes/colors/updater";
import { NativeThemeModule } from "@lib/api/native/modules"; import { NativeThemeModule } from "@lib/api/native/modules";
import { before, instead } from "@lib/api/patcher"; import { before, instead } from "@lib/api/patcher";
import { findByProps } from "@metro"; import { findByProps } from "@metro";
import { byMutableProp } from "@metro/filters"; import chroma from "chroma-js";
import { createLazyModule } from "@metro/lazy";
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
const tokenReference = findByProps("SemanticColor"); const tokenReference = findByProps("SemanticColor");
const isThemeModule = createLazyModule(byMutableProp("isThemeDark")); const themeTypes = findByProps("ThemeTypes")?.ThemeTypes;
const origRawColor = { ...tokenReference.RawColor };
const origDarker = themeTypes.DARKER as string;
const origLight = themeTypes.LIGHT as string;
const SEMANTIC_FALLBACK_MAP: Record<string, string> = { const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
"BG_BACKDROP": "BACKGROUND_FLOATING", "BG_BACKDROP": "BACKGROUND_FLOATING",
@ -22,13 +24,19 @@ const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
"BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY" "BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY"
}; };
const origRawColor = { ...tokenReference.RawColor };
export default function patchDefinitionAndResolver() { export default function patchDefinitionAndResolver() {
const callback = ([theme]: any[]) => { const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0;
if (!_colorRef.current) return void 0;
return theme === _colorRef.key ? [_colorRef.current.reference] : void 0; Object.defineProperty(themeTypes, "DARKER", {
}; configurable: true,
enumerable: true,
get: () => _colorRef.current?.reference === "darker" ? _colorRef.key : origDarker,
});
Object.defineProperty(themeTypes, "LIGHT", {
configurable: true,
enumerable: true,
get: () => _colorRef.current?.reference === "light" ? _colorRef.key : origLight,
});
Object.keys(tokenReference.RawColor).forEach(key => { Object.keys(tokenReference.RawColor).forEach(key => {
Object.defineProperty(tokenReference.RawColor, key, { Object.defineProperty(tokenReference.RawColor, key, {
@ -37,50 +45,48 @@ export default function patchDefinitionAndResolver() {
get: () => { get: () => {
const ret = _colorRef.current?.raw[key]; const ret = _colorRef.current?.raw[key];
if (ret) return ret; if (ret) return ret;
return origRawColor[key]; return origRawColor[key];
} }
}); });
}); });
const unpatches = [ const unpatches = [
before("isThemeDark", isThemeModule, callback),
before("isThemeLight", isThemeModule, callback),
before("updateTheme", NativeThemeModule, callback), before("updateTheme", NativeThemeModule, callback),
instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => { instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => {
if (!_colorRef.current) return orig(...args); if (!_colorRef.current) return orig(...args);
const currentRef = _colorRef.current;
if (args[0] !== _colorRef.key) return orig(...args); if (args[0] !== _colorRef.key) return orig(...args);
// Use the captured reference value args[0] = _colorRef.current.reference;
args[0] = currentRef.reference;
const [name, colorDef] = extractInfo(currentRef.reference, args[1]); const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]);
// If extractInfo couldn't resolve a color definition, fall back to the original resolver let semanticDef = _colorRef.current.semantic[name];
if (!name || !colorDef) return orig(...args); if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) {
semanticDef = _colorRef.current.semantic[SEMANTIC_FALLBACK_MAP[name]];
let semanticDef = currentRef.semantic[name];
if (!semanticDef && currentRef.spec === 2 && name in SEMANTIC_FALLBACK_MAP) {
semanticDef = currentRef.semantic[SEMANTIC_FALLBACK_MAP[name]];
} }
if (semanticDef?.value) { if (semanticDef?.value) {
if (semanticDef.opacity === 1) return semanticDef.value; return semanticDef.opacity === 1
return getChroma()(semanticDef.value).alpha(semanticDef.opacity).hex(); ? semanticDef.value
: chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
} }
const rawKey = colorDef.raw; const rawValue = _colorRef.current.raw[colorDef.raw];
const rawValue = rawKey ? currentRef.raw[rawKey] : undefined;
if (rawValue) { if (rawValue) {
// Set opacity if needed // Set opacity if needed
return (colorDef.opacity === 1) ? rawValue : getChroma()(rawValue).alpha(colorDef.opacity).hex(); return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex();
} }
// Fallback to default // Fallback to default
return orig(...args); return orig(...args);
}), }),
() => { () => {
Object.defineProperty(themeTypes, "DARKER", {
configurable: true, writable: true, value: origDarker
});
Object.defineProperty(themeTypes, "LIGHT", {
configurable: true, writable: true, value: origLight
});
Object.defineProperty(tokenReference, "RawColor", { Object.defineProperty(tokenReference, "RawColor", {
configurable: true, configurable: true,
writable: true, writable: true,
@ -95,13 +101,7 @@ export default function patchDefinitionAndResolver() {
function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] { function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] {
// @ts-ignore - assigning to extractInfo._sym // @ts-ignore - assigning to extractInfo._sym
const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]]; const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]];
const entry = tokenReference.SemanticColor[propName]; const colorDef = tokenReference.SemanticColor[propName];
if (!entry || typeof entry !== "object") { return [propName, colorDef[themeName]];
return [propName, undefined];
}
const colorDef = Object.prototype.hasOwnProperty.call(entry, themeName) ? entry[themeName] : undefined;
return [propName, colorDef];
} }

View file

@ -24,46 +24,71 @@ export const _colorRef: InternalColorRef = {
current: null, current: null,
key: `bn-theme-${_inc}`, key: `bn-theme-${_inc}`,
origRaw: origRawColor, origRaw: origRawColor,
lastSetDiscordTheme: "dark" lastSetDiscordTheme: "darker"
}; };
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true } ) { export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) {
if (settings.safeMode?.enabled) return; if (settings.safeMode?.enabled) return;
const internalDef = colorManifest ? parseColorManifest(colorManifest) : null; const internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
const ref = Object.assign(_colorRef, {
const keyName = internalDef && internalDef.reference === "light" ? `bn-theme-${++_inc}` : `dark`;
const tsTheme = typeof ThemeStore.theme === "string" ? ThemeStore.theme : undefined;
const currentDiscordTheme = tsTheme === "darker" ? "dark" : (tsTheme ?? "");
const lastSet = (currentDiscordTheme && !currentDiscordTheme.startsWith("bn-theme-") && currentDiscordTheme !== keyName)
? currentDiscordTheme
: _colorRef.lastSetDiscordTheme;
var ref = Object.assign(_colorRef, {
current: internalDef, current: internalDef,
key: keyName, key: `bn-theme-${++_inc}`,
lastSetDiscordTheme: lastSet lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-")
? ThemeStore.theme
: _colorRef.lastSetDiscordTheme
}); });
if (internalDef != null) { if (internalDef != null) {
// Register the synthetic key BEFORE calling native, and verify it actually
// landed in every table native reads from. If any registration step fails,
// bail out to a known-good theme instead of handing native an unknown key.
try {
tokenRef.Theme[ref.key.toUpperCase()] = ref.key; tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
if (!FormDivider.DIVIDER_COLORS[ref.current!.reference]) {
throw new Error(`Missing divider color reference: ${ref.current!.reference}`);
}
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference]; FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
Object.keys(tokenRef.Shadow).forEach(k => tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference]); Object.keys(tokenRef.Shadow).forEach(k => {
if (!(ref.current!.reference in tokenRef.Shadow[k])) {
throw new Error(`Missing shadow reference: ${ref.current!.reference}`);
}
tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference];
});
Object.keys(tokenRef.SemanticColor).forEach(k => { Object.keys(tokenRef.SemanticColor).forEach(k => {
if (!(ref.current!.reference in tokenRef.SemanticColor[k])) {
throw new Error(`Missing semantic color reference: ${ref.current!.reference}`);
}
tokenRef.SemanticColor[k][ref.key] = { tokenRef.SemanticColor[k][ref.key] = {
...tokenRef.SemanticColor[k][ref.current!.reference] ...tokenRef.SemanticColor[k][ref.current!.reference]
}; };
}); });
} catch (e) {
console.error("Failed to register custom theme key, falling back", e);
ref.current = null; // force fallback path below
}
} }
if (update) { if (update) {
AppearanceManager.setShouldSyncAppearanceSettings(false); AppearanceManager.setShouldSyncAppearanceSettings(false);
const fallback = (_colorRef.lastSetDiscordTheme && !_colorRef.lastSetDiscordTheme.startsWith("bn-theme-"))
? _colorRef.lastSetDiscordTheme const targetTheme = ref.current != null ? ref.key : ref.lastSetDiscordTheme;
: "dark";
AppearanceManager.updateTheme(internalDef != null ? ref.key : fallback); // This is the actual crash site: a raw call into native code with no
// guard. Wrap it so a native-side rejection can't take the whole
// bridge/thread down — fall back to the last known-good Discord theme.
try {
AppearanceManager.updateTheme(targetTheme);
} catch (e) {
console.error(`Native rejected theme "${targetTheme}", reverting to "${ref.lastSetDiscordTheme}"`, e);
try {
AppearanceManager.updateTheme(ref.lastSetDiscordTheme);
} catch (e2) {
console.error("Fallback theme also failed to apply", e2);
}
}
} }
} }

View file

@ -141,7 +141,7 @@ export function getCurrentTheme() {
* @internal * @internal
*/ */
export function getThemeFromLoader(): VdThemeInfo | null { export function getThemeFromLoader(): VdThemeInfo | null {
return getStoredTheme(); return getStoredTheme() ?? getCurrentTheme();
} }
/** /**
@ -157,6 +157,7 @@ export async function initThemes() {
} }
await newAwaitStorage(colorsPref); await newAwaitStorage(colorsPref);
await awaitStorage(themes);
const currentTheme = getThemeFromLoader(); const currentTheme = getThemeFromLoader();
initColors(currentTheme?.data ?? null); initColors(currentTheme?.data ?? null);

View file

@ -8,6 +8,8 @@ export const dispatcher = FluxDispatcher;
type Intercept = (payload: Record<string, any> & { type: string; }) => any; type Intercept = (payload: Record<string, any> & { type: string; }) => any;
let intercepts: Intercept[] = []; let intercepts: Intercept[] = [];
const interceptIndex = new Map<Intercept, number>();
let nextInterceptId = 0;
/** /**
* @internal * @internal
@ -33,7 +35,14 @@ export function injectFluxInterceptor() {
(dispatcher._interceptors ??= []).unshift(cb); (dispatcher._interceptors ??= []).unshift(cb);
return () => dispatcher._interceptors &&= dispatcher._interceptors.filter(v => v !== cb); return () => {
const idx = interceptIndex.get(cb);
if (idx !== undefined) {
intercepts.splice(idx, 1);
interceptIndex.delete(cb);
intercepts.forEach((intercept, i) => interceptIndex.set(intercept, i));
}
};
} }
/** /**
@ -41,9 +50,16 @@ export function injectFluxInterceptor() {
* nullish -> nothing, falsy -> block, object -> modify * nullish -> nothing, falsy -> block, object -> modify
*/ */
export function intercept(cb: Intercept) { export function intercept(cb: Intercept) {
const id = nextInterceptId++;
interceptIndex.set(cb, id);
intercepts.push(cb); intercepts.push(cb);
return () => { return () => {
intercepts = intercepts.filter(i => i !== cb); const idx = interceptIndex.get(cb);
if (idx !== undefined) {
intercepts.splice(idx, 1);
interceptIndex.delete(cb);
intercepts.forEach((intercept, i) => interceptIndex.set(intercept, i));
}
}; };
} }

View file

@ -10,6 +10,9 @@ const rainLoaderIdentity = globalThis.__RAIN_LOADER__;
// @ts-ignore // @ts-ignore
const vendettaLoaderIdentity = globalThis.__vendetta_loader; const vendettaLoaderIdentity = globalThis.__vendetta_loader;
// @ts-ignore
const shiggyLoaderIdentity = globalThis.__SHIGGY_LOADER__;
export interface VendettaLoaderIdentity { export interface VendettaLoaderIdentity {
name: string; name: string;
features: { features: {
@ -36,6 +39,10 @@ export function isRa1nLoader() {
return rainLoaderIdentity != null; return rainLoaderIdentity != null;
} }
export function isShiggyLoader() {
return shiggyLoaderIdentity != null;
}
function polyfillVendettaLoaderIdentity() { function polyfillVendettaLoaderIdentity() {
if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null; if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null;
@ -98,6 +105,8 @@ export function getLoaderIdentity() {
return getVendettaLoaderIdentity(); return getVendettaLoaderIdentity();
} else if (isRa1nLoader()) { } else if (isRa1nLoader()) {
return rainLoaderIdentity(); return rainLoaderIdentity();
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity;
} }
return null; return null;
@ -116,6 +125,7 @@ export function getLoaderName() {
if (isPyonLoader()) return pyonLoaderIdentity.loaderName; if (isPyonLoader()) return pyonLoaderIdentity.loaderName;
else if (isRa1nLoader()) return rainLoaderIdentity.loadername; else if (isRa1nLoader()) return rainLoaderIdentity.loadername;
else if (isVendettaLoader()) return vendettaLoaderIdentity.name; else if (isVendettaLoader()) return vendettaLoaderIdentity.name;
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderName;
return "Unknown"; return "Unknown";
} }
@ -123,6 +133,7 @@ export function getLoaderName() {
export function getLoaderVersion(): string | null { export function getLoaderVersion(): string | null {
if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion; if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion;
else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion; else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion;
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderVersion;
return null; return null;
} }
@ -144,7 +155,9 @@ export function isThemeSupported() {
} else if (isVendettaLoader()) { } else if (isVendettaLoader()) {
return vendettaLoaderIdentity!!.features.themes != null; return vendettaLoaderIdentity!!.features.themes != null;
} else if (isRa1nLoader()) { } else if (isRa1nLoader()) {
return false; // Ra1n has theme support disabled, this is here just to make sure it doesnt think it does return false; // Rain has theme support disabled, this is here just to make sure it doesnt think it does
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity.hasThemeSupport;
} }
return false; return false;
@ -158,6 +171,8 @@ export function getStoredTheme(): VdThemeInfo | null {
if (!themeProp) return null; if (!themeProp) return null;
// @ts-ignore // @ts-ignore
return globalThis[themeProp] || null; return globalThis[themeProp] || null;
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity.storedTheme || null;
} }
return null; return null;
@ -168,6 +183,8 @@ export function getThemeFilePath() {
return "pyoncord/current-theme.json"; return "pyoncord/current-theme.json";
} else if (isVendettaLoader()) { } else if (isVendettaLoader()) {
return "vendetta_theme.json"; return "vendetta_theme.json";
} else if (isShiggyLoader()) {
return "ShiggyCord/current-theme.json";
} }
return null; return null;
@ -241,6 +258,7 @@ export function getLoaderConfigPath() {
export function isFontSupported() { export function isFontSupported() {
if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2; if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2;
else if (isShiggyLoader()) return shiggyLoaderIdentity.fontPatch === 2;
return false; return false;
} }

View file

@ -10,10 +10,9 @@ export interface RowConfig {
onPress?: () => any; onPress?: () => any;
render?: Parameters<typeof lazy>[0]; render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number; icon?: ImageURISource | number;
IconComponent?: React.ReactNode; IconComponent?: React.ReactNode,
usePredicate?: () => boolean; usePredicate?: () => boolean,
useTrailing?: () => string | JSX.Element; useTrailing?: () => string | JSX.Element
rawTabsConfig?: Record<string, any>;
} }
export const registeredSections = {} as { export const registeredSections = {} as {

View file

@ -2,7 +2,6 @@ import { after } from "@lib/api/patcher";
import { TableRow } from "@metro/common/components"; import { TableRow } from "@metro/common/components";
import { findByNameLazy, findByPropsLazy } from "@metro/wrappers"; import { findByNameLazy, findByPropsLazy } from "@metro/wrappers";
import { registeredSections } from "@ui/settings"; import { registeredSections } from "@ui/settings";
import { Strings } from "@core/i18n";
import { CustomPageRenderer, wrapOnPress } from "./shared"; import { CustomPageRenderer, wrapOnPress } from "./shared";
import { findInReactTree } from "@lib/utils"; import { findInReactTree } from "@lib/utils";
@ -29,9 +28,9 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
IconComponent: () => <TableRow.Icon source={row.icon} />, IconComponent: () => <TableRow.Icon source={row.icon} />,
usePredicate: row.usePredicate, usePredicate: row.usePredicate,
useTrailing: row.useTrailing, useTrailing: row.useTrailing,
useTitle: row.title,
onPress: wrapOnPress(row.onPress, null, row.render, row.title()), onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
withArrow: true, withArrow: true,
...row.rawTabsConfig,
}, },
})), })),
) )
@ -48,6 +47,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
VendettaCustomPage: { VendettaCustomPage: {
type: "route", type: "route",
title: () => "ShiggyCord", title: () => "ShiggyCord",
useTitle: () => "ShiggyCord",
screen: { screen: {
route: "VendettaCustomPage", route: "VendettaCustomPage",
getComponent: () => CustomPageRenderer, getComponent: () => CustomPageRenderer,
@ -56,6 +56,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
SHIGGYCORD_CUSTOM_PAGE: { SHIGGYCORD_CUSTOM_PAGE: {
type: "route", type: "route",
title: () => "ShiggyCord", title: () => "ShiggyCord",
useTitle: () => "ShiggyCord",
screen: { screen: {
route: "SHIGGYCORD_CUSTOM_PAGE", route: "SHIGGYCORD_CUSTOM_PAGE",
getComponent: () => CustomPageRenderer, getComponent: () => CustomPageRenderer,
@ -64,6 +65,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
BUNNY_CUSTOM_PAGE: { BUNNY_CUSTOM_PAGE: {
type: "route", type: "route",
title: () => "ShiggyCord", title: () => "ShiggyCord",
useTitle: () => "ShiggyCord",
screen: { screen: {
route: "BUNNY_CUSTOM_PAGE", route: "BUNNY_CUSTOM_PAGE",
getComponent: () => CustomPageRenderer, getComponent: () => CustomPageRenderer,

View file

@ -4,13 +4,13 @@
export type SearchTree = Record<string, any>; export type SearchTree = Record<string, any>;
export type SearchFilter = (tree: SearchTree) => boolean; export type SearchFilter = (tree: SearchTree) => boolean;
export interface FindInTreeOptions { export interface FindInTreeOptions {
walkable?: string[]; walkable?: string[] | Set<string>;
ignore?: string[]; ignore?: string[] | Set<string>;
maxDepth?: number; maxDepth?: number;
} }
function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindInTreeOptions>, depth: number): any { function treeSearch(tree: SearchTree, filter: SearchFilter, walkable: Set<string>, ignore: Set<string>, maxDepth: number, depth: number): any {
if (depth > opts.maxDepth) return; if (depth > maxDepth) return;
if (!tree) return; if (!tree) return;
try { try {
@ -22,18 +22,19 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
if (typeof item !== "object" || item === null) continue; if (typeof item !== "object" || item === null) continue;
try { try {
const found = treeSearch(item, filter, opts, depth + 1); const found = treeSearch(item, filter, walkable, ignore, maxDepth, depth + 1);
if (found) return found; if (found) return found;
} catch { } } catch { }
} }
} else if (typeof tree === "object") { } else if (typeof tree === "object") {
for (const key of Object.keys(tree)) { for (const key in tree) {
if (Object.prototype.hasOwnProperty.call(tree, key) === false) continue;
if (typeof tree[key] !== "object" || tree[key] === null) continue; if (typeof tree[key] !== "object" || tree[key] === null) continue;
if (opts.walkable.length && !opts.walkable.includes(key)) continue; if (walkable.size && !walkable.has(key)) continue;
if (opts.ignore.includes(key)) continue; if (ignore.size && ignore.has(key)) continue;
try { try {
const found = treeSearch(tree[key], filter, opts, depth + 1); const found = treeSearch(tree[key], filter, walkable, ignore, maxDepth, depth + 1);
if (found) return found; if (found) return found;
} catch { } } catch { }
} }
@ -49,5 +50,5 @@ export default function findInTree(
maxDepth = 100 maxDepth = 100
}: FindInTreeOptions = {}, }: FindInTreeOptions = {},
): any | undefined { ): any | undefined {
return treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0); return treeSearch(tree, filter, new Set(walkable), new Set(ignore), maxDepth, 0);
} }

View file

@ -16,23 +16,17 @@ let _metroCache = null as unknown as ReturnType<typeof buildInitCache>;
export const getMetroCache = () => _metroCache; export const getMetroCache = () => _metroCache;
function buildInitCache() { function buildInitCache() {
const modulesCount = Object.keys(window.modules).length;
const cache = { const cache = {
_v: CACHE_VERSION, _v: CACHE_VERSION,
_buildNumber: NativeClientInfoModule.getConstants().Build, _buildNumber: NativeClientInfoModule.getConstants().Build,
_modulesCount: Object.keys(window.modules).length, _modulesCount: modulesCount,
flagsIndex: {} as Record<string, number>, flagsIndex: {} as Record<string, number>,
findIndex: {} as Record<string, ModulesMap | undefined>, findIndex: {} as Record<string, ModulesMap | undefined>,
polyfillIndex: {} as Record<string, ModulesMap | undefined> polyfillIndex: {} as Record<string, ModulesMap | undefined>
} as const; } as const;
// Force load all modules so useful modules are pre-cached. Add a minor
// delay so the cache is initialized before the modules are loaded.
setTimeout(() => {
for (const id in window.modules) {
require("./modules").requireModule(id);
}
}, 100);
_metroCache = cache; _metroCache = cache;
return cache; return cache;
} }
@ -52,7 +46,8 @@ export async function initMetroCache() {
_metroCache = null!; _metroCache = null!;
throw "cache invalidated; version mismatch"; throw "cache invalidated; version mismatch";
} }
if (_metroCache._modulesCount !== Object.keys(window.modules).length) { let currentCount = Object.keys(window.modules).length;
if (_metroCache._modulesCount !== currentCount) {
_metroCache = null!; _metroCache = null!;
throw "cache invalidated; modules count mismatch"; throw "cache invalidated; modules count mismatch";
} }

View file

@ -117,17 +117,13 @@ for (const [id, moduleExports] of cacher.getModules()) {
} }
const exportsKeysLength = Reflect.ownKeys(actualExports).length; const exportsKeysLength = Reflect.ownKeys(actualExports).length;
if (_source[prop] && exportsKeysLength >= _source[prop]) { if (_source[prop] && exportsKeysLength <= _source[prop]) {
continue; continue;
} }
_module[prop] = actualExports[prop]; _module[prop] = actualExports[prop];
_source[prop] = Reflect.ownKeys(actualExports).length; _source[prop] = Reflect.ownKeys(actualExports).length;
cacher.cacheId(id); cacher.cacheId(id);
if (exportsKeysLength === 1) {
redesignProps.delete(prop);
}
} }
} }