Compare commits
195 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc1c315089 | ||
|
|
bdb8216baa | ||
|
|
f6972bb964 | ||
|
|
ec908963bf | ||
|
|
0a901de7ef | ||
|
|
0ae6b217c3 | ||
|
|
30e325d1e6 | ||
|
|
b17686afdf | ||
|
|
eb72692e57 | ||
|
|
ee83314fb6 | ||
|
|
feb03a25f6 | ||
|
|
add94265ae | ||
|
|
dbb0b4bd9e | ||
|
|
1f3a5262b7 | ||
|
|
1450dc20af | ||
|
|
9fd141a158 | ||
|
|
68660a2e06 | ||
|
|
9984b28e45 | ||
|
|
2afe338c87 | ||
|
|
ef3fd12199 | ||
|
|
f111ad4cea | ||
|
|
d85f28012d | ||
|
|
e49f1f9f06 | ||
|
|
1ef5817cdb | ||
|
|
075a83c41c | ||
|
|
7758595fbd | ||
|
|
e10cd63ea0 | ||
|
|
08f2eba392 | ||
|
|
e201bc5839 | ||
|
|
110ecaf19a | ||
|
|
68281d0b6c | ||
|
|
02ebd331de | ||
|
|
eeb9c9ddcb | ||
|
|
ac9fef7e03 | ||
|
|
ac5caa747d | ||
|
|
4f7df6de63 | ||
|
|
90d4d47303 | ||
|
|
dcd8811652 | ||
|
|
225c1e13a0 | ||
|
|
f910478079 | ||
|
|
69c02e1fb5 | ||
|
|
abde8e96a7 | ||
|
|
98e5509bd5 | ||
|
|
a7844b86e3 | ||
|
|
262562f050 | ||
|
|
b12d44d10e | ||
|
|
e9e8efbb66 | ||
|
|
e0a85bb003 | ||
|
|
77bfd4ef15 | ||
|
|
0931d85c74 | ||
|
|
1d0aa92b62 | ||
|
|
f6b285ff22 | ||
|
|
64a4df26fc | ||
|
|
e38c785aae | ||
|
|
2d96fad40e | ||
|
|
0990faa41d | ||
|
|
dc93b81ae1 | ||
|
|
2448c87def | ||
|
|
5205d8559f | ||
|
|
6bbf0a3c07 | ||
|
|
af23275735 | ||
|
|
0e25a20ba0 | ||
|
|
b0e1180182 | ||
|
|
68dc8238c3 | ||
|
|
7f99346971 | ||
|
|
401165eb34 | ||
|
|
19d3493322 | ||
|
|
a56b432fba | ||
|
|
414cf25ae8 | ||
|
|
1523b51108 | ||
|
|
20d6ff15ea | ||
|
|
036de69acd | ||
|
|
945acc1738 | ||
|
|
5fa10f606a | ||
|
|
36336f738f | ||
|
|
5cdd217017 | ||
|
|
cd4679913a | ||
|
|
a4009897c2 | ||
|
|
1352abed03 | ||
|
|
47be786461 | ||
|
|
aa90f479e8 | ||
|
|
87cd9ab347 | ||
|
|
5b4cc43637 | ||
|
|
8b9fa8d964 | ||
|
|
01a8686542 | ||
|
|
52bc899440 | ||
|
|
0512796945 | ||
|
|
0a92bc4363 | ||
|
|
7273bd52fa | ||
|
|
985355976e | ||
|
|
3f92812707 | ||
|
|
57fc0b2ab2 | ||
|
|
86ca221428 | ||
|
|
0d7a5b8612 | ||
|
|
f31e0d0587 | ||
|
|
4427217cb2 | ||
|
|
c297b946f4 | ||
|
|
077616dca1 | ||
|
|
c007af6500 | ||
|
|
36febf010c | ||
|
|
f6b18053d9 | ||
|
|
9a1ab563b7 | ||
|
|
a0766a8ddc | ||
|
|
08a6017103 | ||
|
|
4c10ace879 | ||
|
|
2216045a50 | ||
|
|
9894d8e8c4 | ||
|
|
0dfa1e0a0a | ||
|
|
926b393c0f | ||
|
|
f688e4dfc6 | ||
|
|
9f1f3cf5f3 | ||
|
|
c7f2c1fad8 | ||
|
|
b1df00ac39 | ||
|
|
60af217859 | ||
|
|
6e34da9d91 | ||
|
|
07f7566c13 | ||
|
|
66ae261fe3 | ||
|
|
e8b9a337b5 | ||
|
|
c965aa5907 | ||
|
|
3f709f82cc | ||
|
|
972d7e7836 | ||
|
|
335bb4ab5e | ||
|
|
a4dd0601b0 | ||
|
|
d2e9499d6f | ||
|
|
a0bc249936 | ||
|
|
46eeaa27c4 | ||
|
|
6a34ad83ef | ||
|
|
a628bf2850 | ||
|
|
ff2e341a1a | ||
|
|
4f6f5b636b | ||
|
|
fdcc756281 | ||
|
|
5750fff8d7 | ||
|
|
923039adb2 | ||
|
|
153e459665 | ||
|
|
2e75085b72 | ||
|
|
6347f7d86e | ||
|
|
829cb397a8 | ||
|
|
b791dfe045 | ||
|
|
6a20f5cdf3 | ||
|
|
b5279af2c5 | ||
|
|
f56b894e4a | ||
|
|
f18029cfc2 | ||
|
|
78c9999e97 | ||
|
|
bd177b9bf1 | ||
|
|
21bdd39803 | ||
|
|
f8047bb853 | ||
|
|
ba344ac3fb | ||
|
|
b474285180 | ||
|
|
0ac9ee6dd3 | ||
|
|
55233fda91 | ||
|
|
ac17136640 | ||
|
|
b99e851af9 | ||
|
|
5e5d635a09 | ||
|
|
dac035fb5f | ||
|
|
16cd982554 | ||
|
|
7396fca9fc | ||
|
|
0b6d648615 | ||
|
|
3449b76df3 | ||
|
|
9b416830ab | ||
|
|
9922386c18 | ||
|
|
1ebb81bf96 | ||
|
|
c3f619dc79 | ||
|
|
8f9444ab54 | ||
|
|
19d0419a2f | ||
|
|
23083003e9 | ||
|
|
0861a24c0c | ||
|
|
13593d8f41 | ||
|
|
c0c910a2b3 | ||
|
|
2a1eccff80 | ||
|
|
3ca0f5443a | ||
|
|
bed19ed24b | ||
|
|
8909be4543 | ||
|
|
c079a9ecae | ||
|
|
de3194fecc | ||
|
|
92f22ba309 | ||
|
|
75625016a9 | ||
|
|
ec673af07b | ||
|
|
867810ecf9 | ||
|
|
80ce7006ba | ||
|
|
9beb2576a2 | ||
|
|
016a4589ea | ||
|
|
7d068dcf17 | ||
|
|
1ec1ac535a | ||
|
|
18b5e4d66a | ||
|
|
af881aaf07 | ||
|
|
daaeceed91 | ||
|
|
d8a6d14088 | ||
|
|
29d35c42e2 | ||
|
|
a982c7cd1d | ||
|
|
1232995cd9 | ||
|
|
c95e5062d0 | ||
|
|
d1380a5772 | ||
|
|
3339e7ce02 | ||
|
|
81f0f5c2c5 | ||
|
|
cc31d09118 |
46 changed files with 2493 additions and 1788 deletions
|
|
@ -53,4 +53,4 @@ jobs:
|
|||
git fetch origin
|
||||
git checkout github || git checkout -b github
|
||||
git merge origin/main --no-edit --allow-unrelated-histories
|
||||
git push --force origin github
|
||||
git push --force origin github
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -1,6 +1,7 @@
|
|||
# GoonCord [](https://discord.shiggycord.dev)
|
||||
# GoonCord [](https://discord.gg/nQykFF9Ud6)
|
||||
|
||||
Bundle based on ShiggyCord, made just for fun
|
||||
An unofficial fork of ShiggyGord, made just for fun
|
||||
Idea taken from me
|
||||
|
||||
## Installing
|
||||
|
||||
|
|
@ -8,7 +9,7 @@ Bundle based on ShiggyCord, made just for fun
|
|||
|
||||
- Rooted: [Goon Xposed](https://github.com/OTKUSteyler/GoonXposed)
|
||||
|
||||
- Non-Rooted: [Goon Manager](https://github.com/OTKUSteyler/GoonManager)
|
||||
- Non-Rooted: [Goon Manger](https://github.com/OTKUSteyler/GoonManager)
|
||||
|
||||
### Android/iOS
|
||||
- **Injecting bundle:**
|
||||
|
|
@ -21,7 +22,7 @@ Bundle based on ShiggyCord, made just for fun
|
|||
1. Go to Settings > General and enable Developer Settings.
|
||||
1. Clone the repo:
|
||||
```
|
||||
git clone https://github.com/OTKUSteyler/gooncord.git
|
||||
git clone https://github.com/OTKUSteyler/GoonCord.git
|
||||
```
|
||||
1. Install dependencies:
|
||||
```
|
||||
|
|
@ -37,5 +38,3 @@ Bundle based on ShiggyCord, made just for fun
|
|||
1. Make your changes, rebuild, reload, go wild!
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
|
|
|||
47
package.json
47
package.json
|
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"author": {
|
||||
"name": "OTKUSteyler",
|
||||
"url": "https://github.com/OTKUSteyler"
|
||||
"url": "https://github.com/OTKUSteyler/GoonCord"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/build.mjs",
|
||||
|
|
@ -13,40 +13,45 @@
|
|||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.15.47",
|
||||
"@swc/helpers": "^0.5.23",
|
||||
"@types/node": "^26.1.2",
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/exec": "^7.1.0",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "^8.65.0",
|
||||
"chalk": "^6.0.0",
|
||||
"esbuild": "^0.28.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "^8.56.0",
|
||||
"chalk": "^5.6.2",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-plugin-globals": "^0.2.0",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint": "^10.0.1",
|
||||
"eslint-plugin-import-alias": "^1.2.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-simple-import-sort": "^14.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"typescript": "^7.0.2",
|
||||
"typescript": "^5.9.3",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gullerya/object-observer": "^7.0.0",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@gullerya/object-observer": "^6.1.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@types/chroma-js": "~3.1.2",
|
||||
"@types/lodash": "~4.17.24",
|
||||
"@types/react": "19.2.17",
|
||||
"es-toolkit": "^1.50.0",
|
||||
"@types/react": "19.2.14",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"intl-messageformat": "^11.2.12",
|
||||
"intl-messageformat": "^11.1.2",
|
||||
"moment": "2.30.1",
|
||||
"react-devtools": "^7.0.1",
|
||||
"react-native": "0.86.2",
|
||||
"react-native-reanimated": "^4.5.3",
|
||||
"react-native-webview": "^14.0.1",
|
||||
"react-native": "0.84.0",
|
||||
"react-native-reanimated": "^4.2.2",
|
||||
"react-native-webview": "^13.16.0",
|
||||
"spitroast": "^2.1.6",
|
||||
"type-fest": "^5.8.0"
|
||||
"type-fest": "^5.4.4"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@swc/core",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const config = {
|
|||
splitting: false,
|
||||
// Enable minification by default for release builds (when a release-branch is provided).
|
||||
// This keeps development builds unminified for easier debugging.
|
||||
minify: true,
|
||||
minify: false,
|
||||
external: [],
|
||||
supported: {
|
||||
// Hermes does not actually support const and let, even though it syntactically
|
||||
|
|
@ -56,8 +56,6 @@ const config = {
|
|||
"!ShiggyCord-deps-shim!": "./shims/depsModule.ts",
|
||||
spitroast: "./node_modules/spitroast",
|
||||
"react/jsx-runtime": "./shims/jsxRuntime",
|
||||
"@gullerya/object-observer":
|
||||
path.resolve("./node_modules/@gullerya/object-observer/dist/object-observer.min.js"),
|
||||
},
|
||||
plugins: [
|
||||
globalPlugin({
|
||||
|
|
@ -77,7 +75,7 @@ const config = {
|
|||
constModules: {
|
||||
globals: {
|
||||
"bunny-build-info": {
|
||||
version: `"1.4.1.8"`,
|
||||
version: `"1.4.1.2"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -143,7 +141,7 @@ if (isThisFileBeingRunViaCLI) {
|
|||
printBuildSuccess(context.hash, releaseBranch, timeTook);
|
||||
|
||||
const { timeTook: minifiedTimeTook } = await buildBundle({
|
||||
minify: true,
|
||||
minify: false,
|
||||
outfile: config.outfile.replace(/\.js$/, ".min.js"),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function serve(options) {
|
|||
if (pathname?.endsWith(".js")) {
|
||||
try {
|
||||
let fileToServe;
|
||||
if (pathname === "/shiggycord.min.js") {
|
||||
if (pathname === "/gooncord.min.js") {
|
||||
const { config, context, timeTook } = await buildBundle({
|
||||
minify: true,
|
||||
});
|
||||
|
|
@ -30,7 +30,7 @@ export function serve(options) {
|
|||
true,
|
||||
);
|
||||
fileToServe = config.outfile.replace(/\.js$/, ".min.js");
|
||||
} else if (pathname === "/shiggycord.js") {
|
||||
} else if (pathname === "/gooncord.js") {
|
||||
const { config, context, timeTook } = await buildBundle();
|
||||
printBuildSuccess(context.hash, args["release-branch"], timeTook);
|
||||
fileToServe = config.outfile;
|
||||
|
|
@ -55,7 +55,7 @@ export function serve(options) {
|
|||
|
||||
server.listen(args.port ?? 4040);
|
||||
|
||||
console.info(chalk.bold.yellowBright("Serving ShiggyCord on:"));
|
||||
console.info(chalk.bold.yellowBright("Serving GoonCord on:"));
|
||||
|
||||
const netInterfaces = os.networkInterfaces();
|
||||
for (const netinterfaces of Object.values(netInterfaces)) {
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 45 KiB |
|
|
@ -18,26 +18,24 @@ export default () =>
|
|||
},
|
||||
],
|
||||
execute([ephemeral], ctx) {
|
||||
const info = getDebugInfo();
|
||||
const info = getDebugInfo();
|
||||
const content = [
|
||||
"**💦GoonCord Goon Info💦**",
|
||||
`> GoonCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
|
||||
`> Discord: ${info.discord.version} (${info.discord.build})`,
|
||||
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
|
||||
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
|
||||
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
|
||||
`> Device: ${info.device.model}`,
|
||||
].join("\n");
|
||||
|
||||
if(info.device.codename === undefined) { info.device.codename = ""}
|
||||
else{ info.device.codename = "(" + info.device.codename + ")" }
|
||||
|
||||
const content = [
|
||||
"**💦GoonCord💦 Debug Info**",
|
||||
`> GoonCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
|
||||
`> Discord: ${info.discord.version} (${info.discord.build})`,
|
||||
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
|
||||
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
|
||||
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
|
||||
`> Device: ${info.device.model} ${info.device.codename}`,
|
||||
].join("\n");
|
||||
|
||||
if (ephemeral?.value) {
|
||||
messageUtil.sendBotMessage(ctx.channel.id, content);
|
||||
} else {
|
||||
const fixNonce = (BigInt(Date.now() - 1420070400000) << 22n).toString();
|
||||
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {nonce:fixNonce});
|
||||
}
|
||||
}
|
||||
};
|
||||
if (ephemeral?.value) {
|
||||
messageUtil.sendBotMessage(ctx.channel.id, content);
|
||||
} else {
|
||||
const fixNonce = Date.now().toString();
|
||||
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {
|
||||
nonce: fixNonce,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export default () =>
|
|||
if (ephemeral?.value) {
|
||||
messageUtil.sendBotMessage(ctx.channel.id, content);
|
||||
} else {
|
||||
const fixNonce = (BigInt(Date.now() - 1420070400000) << 22n).toString();
|
||||
const fixNonce = Date.now().toString();
|
||||
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {
|
||||
nonce: fixNonce,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@
|
|||
"ARE_YOU_SURE_TO_CLEAR_DATA": "Are you sure you wish to clear the data of {name}?",
|
||||
"ARE_YOU_SURE_TO_DELETE_PLUGIN": "Are you sure you wish to delete {name}? This will clear all of the plugin's data.",
|
||||
"ARE_YOU_SURE_TO_DELETE_THEME": "Are you sure you wish to delete {name}?",
|
||||
"AUTO_DEBUGGER": "Automatically connect to Debugger",
|
||||
"AUTO_DEVTOOLS": "Automatically connect to Devtools",
|
||||
"ASSET_BROWSER": "Asset Browser",
|
||||
"ASSET_TYPES": "Asset Types",
|
||||
"IMAGE_FILES": "Image Files",
|
||||
"TEXT_FILES": "Text Files",
|
||||
"BRAND": "Brand",
|
||||
"SHIGGYCORD": "GoonCord",
|
||||
"SHIGGYCORD": "💦GoonCord💦",
|
||||
"SHIGGYCORD_URL": "GoonCord URL",
|
||||
"BROWSER": "Browser",
|
||||
"BYTECODE": "Bytecode",
|
||||
|
|
@ -24,14 +22,13 @@
|
|||
"CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}",
|
||||
"CODEBERG": "Codeberg",
|
||||
"CODENAME": "Codename",
|
||||
"COMMAND_DEBUG_DESC": "Send 💦GoonCord💦 debug info.",
|
||||
"COMMAND_DEBUG_DESC": "Send GoonCord debug info.",
|
||||
"COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.",
|
||||
"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_CODE": "The code to evaluate.",
|
||||
"COMMAND_PLUGINS_DESC": "Send list of installed plugins.",
|
||||
"COMMAND_PLUGINS_OPT_EPHEMERALLY": "Send plugins list ephemerally.",
|
||||
"COMMAND_RELOAD_DESC": "Reload Discord.",
|
||||
"COMPONENT": "Component",
|
||||
"CONFIRMATION_LINK_IS_A_TYPE": "This link is a **{urlType, select, plugin {Plugin} theme {Theme} other {Add-on}}**, would you like to install it?",
|
||||
"CONNECT_TO_DEBUG_WEBSOCKET": "Connect to debug WebSocket",
|
||||
|
|
@ -42,7 +39,6 @@
|
|||
"DEBUG": "Debug",
|
||||
"DEBUGGER_URL": "Debugger URL",
|
||||
"DELETE": "Delete",
|
||||
"DEVTOOLS_URL": "DevTools URL",
|
||||
"DESC_EXTRACT_FONTS_FROM_THEME": "Looks out for \"fonts\" field in your currently applied theme and install it.",
|
||||
"DEVELOPER": "Developer",
|
||||
"DEVELOPER_SETTINGS": "Developer Settings",
|
||||
|
|
@ -72,12 +68,12 @@
|
|||
"LOAD_FROM_CUSTOM_URL": "Load from custom URL",
|
||||
"LOAD_FROM_CUSTOM_URL_DEC": "Load GoonCord from a custom endpoint.",
|
||||
"LOAD_REACT_DEVTOOLS": "Load React DevTools",
|
||||
"SHIGGY_URL": "ShiggyCord URL",
|
||||
"LOADER": "Loader",
|
||||
"MACHINE_ID": "Machine ID",
|
||||
"MANUFACTURER": "Manufacturer",
|
||||
"MESSAGE": "Message",
|
||||
"MISCELLANEOUS": "Miscellaneous",
|
||||
"MODAL_RELOAD_THEME_CHANGE": "Reload the app to apply the theme change?",
|
||||
"MODAL_RELOAD_REQUIRED": "Reload app?",
|
||||
"MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?",
|
||||
"MODAL_THEME_REFETCHED": "Theme refetched",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { logger } from "@lib/utils/logger";
|
||||
import { FluxDispatcher } from "@metro/common";
|
||||
import { findByNameLazy, findByStoreName } from "@metro/wrappers";
|
||||
import { findByNameLazy } from "@metro/wrappers";
|
||||
import { PrimitiveType } from "intl-messageformat";
|
||||
|
||||
import langDefault from "./default.json";
|
||||
|
||||
const IntlMessageFormat = findByNameLazy(
|
||||
"MessageFormat",
|
||||
"MessageFormat",
|
||||
) as typeof import("intl-messageformat").default;
|
||||
|
||||
type I18nKey = keyof typeof langDefault;
|
||||
|
|
@ -18,133 +17,61 @@ const _loadedLocale = new Set<string>();
|
|||
const _loadedStrings = {} as Record<string, typeof langDefault>;
|
||||
|
||||
export const Strings = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_t, prop: keyof typeof langDefault) => {
|
||||
if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) {
|
||||
return _loadedStrings[_currentLocale]?.[prop];
|
||||
}
|
||||
return langDefault[prop];
|
||||
},
|
||||
{},
|
||||
{
|
||||
get: (_t, prop: keyof typeof langDefault) => {
|
||||
if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) {
|
||||
return _loadedStrings[_currentLocale]?.[prop];
|
||||
}
|
||||
return langDefault[prop];
|
||||
},
|
||||
},
|
||||
) as Record<I18nKey, string>;
|
||||
|
||||
const languageMap: Record<string, string> = {
|
||||
"ar-SA": "ar",
|
||||
"bn-BD": "bn",
|
||||
"ca-ES": "ca",
|
||||
"de-DE": "de",
|
||||
"es-ES": "es",
|
||||
"es-419": "es",
|
||||
"fa-IR": "fa",
|
||||
"fi-FI": "fi",
|
||||
"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",
|
||||
"ru-RU": "ru",
|
||||
"sk-SK": "sk",
|
||||
"sv-SE": "sv",
|
||||
"tr-TR": "tr",
|
||||
"vi-VN": "vi",
|
||||
};
|
||||
export function initFetchI18nStrings() {
|
||||
const cb = ({ locale }: { locale: string }) => {
|
||||
const languageMap = {
|
||||
"es-ES": "es",
|
||||
"es-419": "es_419",
|
||||
"zh-TW": "zh-Hant",
|
||||
"zh-CN": "zh-Hans",
|
||||
"pt-PT": "pt",
|
||||
"pt-BR": "pt_BR",
|
||||
"sv-SE": "sv",
|
||||
} as Record<string, string>;
|
||||
|
||||
function fetchLocale(locale: string) {
|
||||
const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale);
|
||||
|
||||
logger.log("[i18n] fetchLocale called:", locale, "->", resolvedLocale);
|
||||
if (resolvedLocale.startsWith("en-")) {
|
||||
_currentLocale = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_loadedLocale.has(resolvedLocale)) {
|
||||
_loadedLocale.add(resolvedLocale);
|
||||
_loadedLocale.add(resolvedLocale);
|
||||
|
||||
if (resolvedLocale.toLowerCase().startsWith("en")) {
|
||||
// use local default.json for english
|
||||
logger.log("[i18n] Using local default.json for English locale");
|
||||
_loadedStrings[resolvedLocale] = langDefault;
|
||||
_currentLocale = resolvedLocale;
|
||||
} else {
|
||||
fetch(`https://codeberg.org/cocobo1/kettu-i18n/raw/branch/main/base/${resolvedLocale}.json`)
|
||||
.then(r => r.json())
|
||||
.then(strings => {
|
||||
logger.log("[i18n] Loaded strings for:", resolvedLocale);
|
||||
_loadedStrings[resolvedLocale] = strings;
|
||||
_currentLocale = resolvedLocale;
|
||||
})
|
||||
.catch(e =>
|
||||
logger.error(`[i18n] Error fetching strings for ${resolvedLocale}: ${e}`),
|
||||
);
|
||||
}
|
||||
fetch(
|
||||
`https://raw.githubusercontent.com/OTKUSteyler/i18n/main/resources/${resolvedLocale}/gooncord.json`,
|
||||
)
|
||||
.then((r) => r.json())
|
||||
.then((strings) => (_loadedStrings[resolvedLocale] = strings))
|
||||
.then(
|
||||
() =>
|
||||
resolvedLocale === _lastSetLocale &&
|
||||
(_currentLocale = resolvedLocale),
|
||||
)
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
`An error occured while fetching strings for ${resolvedLocale}: ${e}`,
|
||||
),
|
||||
);
|
||||
} 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;
|
||||
};
|
||||
|
||||
const tryTimes = () => {
|
||||
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);
|
||||
};
|
||||
FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb);
|
||||
return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb);
|
||||
}
|
||||
|
||||
type FormatStringRet<T> = T extends PrimitiveType
|
||||
|
|
@ -152,20 +79,10 @@ type FormatStringRet<T> = T extends PrimitiveType
|
|||
: string | T | (string | T)[];
|
||||
|
||||
export function formatString<T = void>(
|
||||
key: I18nKey,
|
||||
val: Record<string, T>,
|
||||
key: I18nKey,
|
||||
val: Record<string, T>,
|
||||
): FormatStringRet<T> {
|
||||
const str = Strings[key];
|
||||
if (!str) return str as FormatStringRet<T>;
|
||||
try {
|
||||
// @ts-ignore
|
||||
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>;
|
||||
}
|
||||
const str = Strings[key];
|
||||
// @ts-ignore
|
||||
return new IntlMessageFormat(str).format(val);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,201 +1,94 @@
|
|||
import { after } from "@lib/api/patcher";
|
||||
import { onJsxCreate } from "@lib/api/react/jsx";
|
||||
import { findByNameLazy } from "@metro";
|
||||
import { findByName } from "@metro";
|
||||
import { useEffect, useState } from "react";
|
||||
import { defineCorePlugin } from "..";
|
||||
import { FluxDispatcher } from "@metro/common";
|
||||
|
||||
interface Badge {
|
||||
label: string;
|
||||
url: string;
|
||||
interface BunnyBadge {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
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>();
|
||||
const useBadgesModule = findByName("useBadges", false);
|
||||
|
||||
export default defineCorePlugin({
|
||||
manifest: {
|
||||
id: "bunny.badges",
|
||||
version: "1.2.0",
|
||||
type: "plugin",
|
||||
spec: 3,
|
||||
main: "",
|
||||
display: {
|
||||
name: "Badges",
|
||||
description: "Adds badges to user's profile",
|
||||
authors: [{ name: "cocobo1" }, { name: "pylixonly" }],
|
||||
},
|
||||
manifest: {
|
||||
id: "bunny.badges",
|
||||
version: "1.1.0",
|
||||
type: "plugin",
|
||||
spec: 3,
|
||||
main: "",
|
||||
display: {
|
||||
name: "Badges",
|
||||
description: "Adds badges to user's profile",
|
||||
authors: [{ name: "cocobo1" }, { name: "pylixonly" }],
|
||||
},
|
||||
},
|
||||
|
||||
start() {
|
||||
onJsxCreate("ProfileBadge", (component, ret) => {
|
||||
if (ret.props.id?.startsWith("rain-")) {
|
||||
const cachedProps = badgeProps.get(ret.props.id);
|
||||
if (cachedProps) {
|
||||
ret.props.source = cachedProps.source;
|
||||
ret.props.label = cachedProps.label;
|
||||
ret.props.id = cachedProps.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
start() {
|
||||
let allBadges: { [x: string]: any } | null = null;
|
||||
const badgeProps = {} as Record<string, any>;
|
||||
|
||||
onJsxCreate("RenderedBadge", (component, ret) => {
|
||||
if (ret.props.id?.startsWith("rain-")) {
|
||||
const cachedProps = badgeProps.get(ret.props.id);
|
||||
if (cachedProps) {
|
||||
Object.assign(ret.props, cachedProps);
|
||||
}
|
||||
}
|
||||
});
|
||||
onJsxCreate("ProfileBadge", (component, ret) => {
|
||||
if (ret.props.id?.startsWith("bunny-")) {
|
||||
const cachedProps = badgeProps[ret.props.id];
|
||||
if (cachedProps) {
|
||||
ret.props.source = cachedProps.source;
|
||||
ret.props.label = cachedProps.label;
|
||||
ret.props.id = cachedProps.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Small helper so one failing/optional source can't take down the others.
|
||||
const safeFetchJson = async <T,>(url: string, fallback: T): Promise<T> => {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return fallback;
|
||||
return (await res.json()) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
onJsxCreate("RenderedBadge", (component, ret) => {
|
||||
if (ret.props.id?.startsWith("bunny-")) {
|
||||
const cachedProps = badgeProps[ret.props.id];
|
||||
if (cachedProps) {
|
||||
Object.assign(ret.props, cachedProps);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fetchAndProcessBadges = async (userId: string) => {
|
||||
if (pendingRequests.has(userId)) return;
|
||||
pendingRequests.add(userId);
|
||||
after("default", useBadgesModule, ([user], result) => {
|
||||
const [badges, setBadges] = useState<BunnyBadge[]>([]);
|
||||
|
||||
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",
|
||||
{}
|
||||
),
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
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,
|
||||
source: { uri: badge.url },
|
||||
label: badge.label,
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
description: badge.label,
|
||||
icon: " _",
|
||||
});
|
||||
if (!allBadges) {
|
||||
fetch(
|
||||
"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) {
|
||||
badges.forEach((badge, i) => {
|
||||
const badgeId = `bunny-${user.userId}-${i}`;
|
||||
|
||||
badgeProps[badgeId] = {
|
||||
id: badgeId,
|
||||
source: { uri: badge.url },
|
||||
label: badge.label,
|
||||
userId: user.userId,
|
||||
};
|
||||
|
||||
result.push({
|
||||
id: badgeId,
|
||||
description: badge.label,
|
||||
icon: " _",
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
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>
|
||||
)
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
// 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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -34,12 +34,8 @@ export const getCorePlugins = (): CorePluginRecord => ({
|
|||
"bunny.quickinstall": makeLazyPlugin(() => require("./quickinstall"), true),
|
||||
"bunny.badges": makeLazyPlugin(() => require("./badges")),
|
||||
"bunny.notrack": makeLazyPlugin(() => require("./notrack")),
|
||||
"bunny.messagefix": makeLazyPlugin(() => require("./messagefix")),
|
||||
"bunny.fixembed": makeLazyPlugin(() => require("./fixembed")),
|
||||
"bunny.favouriteanything": makeLazyPlugin(() => require("./favouriteanything")),
|
||||
"bunny.nodelete": makeLazyPlugin(() => require("./nodelete")),
|
||||
"bunny.maxverstappen": makeLazyPlugin(() => require("./maxverstappen")),
|
||||
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,145 +0,0 @@
|
|||
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");
|
||||
},
|
||||
});
|
||||
56
src/core/plugins/messagefix/index.ts
Normal file
56
src/core/plugins/messagefix/index.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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 || Date.now().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");
|
||||
},
|
||||
});
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
// 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");
|
||||
},
|
||||
});
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
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;
|
||||
}
|
||||
301
src/core/plugins/shiggyenhancements/index.tsx
Normal file
301
src/core/plugins/shiggyenhancements/index.tsx
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
import { defineCorePlugin } from "..";
|
||||
import { findByProps } from "@metro";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { React } from "@metro/common";
|
||||
|
||||
const { TableRowGroup, TableRow, TableSwitchRow, Stack } = findByProps(
|
||||
"TableRowGroup",
|
||||
"TableRow",
|
||||
"TableSwitchRow",
|
||||
"Stack",
|
||||
);
|
||||
const { ScrollView } = require("react-native");
|
||||
|
||||
// Get ReactNative for NativeModules
|
||||
const { ReactNative } = window as any;
|
||||
const { DCDSoundManager } = ReactNative?.NativeModules || {};
|
||||
|
||||
// Sound settings type
|
||||
type SoundSettings = {
|
||||
enabled: boolean;
|
||||
probability: number;
|
||||
};
|
||||
|
||||
declare module "@lib/api/settings" {
|
||||
interface Settings {
|
||||
startupSound?: SoundSettings;
|
||||
}
|
||||
}
|
||||
|
||||
const soundUrl =
|
||||
"https://github.com/OTKUSteyler/GoonCord/raw/refs/heads/main/Stuff/All%20I%20Want%20For%20Christmas%20(Is%20Fark).mp3";
|
||||
const soundId = 6971;
|
||||
let soundDuration = -1;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
let isPlaying = false;
|
||||
let isPrepared = false;
|
||||
|
||||
// Function to prepare the sound
|
||||
const prepareSound = function (): Promise<any> {
|
||||
return new Promise(function (resolve) {
|
||||
if (!DCDSoundManager) {
|
||||
logger.error("DCDSoundManager not available");
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
DCDSoundManager.prepare(
|
||||
soundUrl,
|
||||
"music",
|
||||
soundId,
|
||||
function (error: any, sound: any) {
|
||||
if (error) {
|
||||
logger.error("Failed to prepare sound:", error);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(sound);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Function to play the sound
|
||||
async function playSound() {
|
||||
if (!DCDSoundManager) {
|
||||
logger.error("DCDSoundManager not available for playback");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
DCDSoundManager.stop(soundId);
|
||||
isPlaying = false;
|
||||
}
|
||||
|
||||
isPlaying = true;
|
||||
|
||||
try {
|
||||
await DCDSoundManager.play(soundId);
|
||||
|
||||
if (soundDuration > 0) {
|
||||
timeoutId = setTimeout(function () {
|
||||
isPlaying = false;
|
||||
DCDSoundManager.stop(soundId);
|
||||
timeoutId = null;
|
||||
}, soundDuration);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error playing sound:", error);
|
||||
isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should play based on probability
|
||||
function shouldPlaySound(): boolean {
|
||||
const probability = settings.startupSound?.probability ?? 2;
|
||||
const random = Math.random() * 100;
|
||||
return random <= probability;
|
||||
}
|
||||
|
||||
export default defineCorePlugin({
|
||||
manifest: {
|
||||
id: "bunny.enhancements",
|
||||
version: "0.1.0",
|
||||
type: "plugin",
|
||||
spec: 3,
|
||||
main: "",
|
||||
display: {
|
||||
name: "Shiggy Enhancements",
|
||||
description:
|
||||
"Fixes common discord bugs because discord wont, and adds some features. (Originally Kettu Enhancements)",
|
||||
authors: [{ name: "cocobo1" }, { name: "Shiggy Team" }],
|
||||
},
|
||||
},
|
||||
|
||||
SettingsComponent() {
|
||||
const { useState, useEffect } = React;
|
||||
const [config, setConfig] = useState<SoundSettings>({
|
||||
enabled: settings.startupSound?.enabled ?? true,
|
||||
probability: settings.startupSound?.probability ?? 2,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
settings.startupSound = config;
|
||||
}, [config]);
|
||||
|
||||
const updateConfig = (
|
||||
key: keyof SoundSettings,
|
||||
value: boolean | number,
|
||||
) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
// Get UI components including TextInput
|
||||
const {
|
||||
TableRowGroup: _TRG,
|
||||
TableRow: _TR,
|
||||
TableSwitchRow: _TSR,
|
||||
Stack: _S,
|
||||
TextInput: _TextInput,
|
||||
} = findByProps(
|
||||
"TableRowGroup",
|
||||
"TableRow",
|
||||
"TableSwitchRow",
|
||||
"Stack",
|
||||
"TextInput",
|
||||
);
|
||||
|
||||
// Fallback if table components aren't available
|
||||
if (!_TRG || !_TSR || !_TR || !_S) {
|
||||
const FallbackText =
|
||||
"Startup Sound UI unavailable (missing TableRow components).";
|
||||
return React.createElement(
|
||||
ScrollView,
|
||||
{ style: { flex: 1, padding: 12 } },
|
||||
FallbackText,
|
||||
);
|
||||
}
|
||||
|
||||
const TableRowGroup = _TRG;
|
||||
const TableRow = _TR;
|
||||
const TableSwitchRow = _TSR;
|
||||
const Stack = _S;
|
||||
const TextInput = _TextInput;
|
||||
|
||||
// Handle probability input change
|
||||
const handleProbabilityChange = (v: string) => {
|
||||
const num = parseInt(v);
|
||||
if (!isNaN(num) && num >= 0 && num <= 100) {
|
||||
updateConfig("probability", num);
|
||||
} else if (v === "") {
|
||||
updateConfig("probability", 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Create settings elements
|
||||
const settingsElements = [];
|
||||
|
||||
// Startup Sound toggle with probability display
|
||||
settingsElements.push(
|
||||
React.createElement(
|
||||
TableRowGroup,
|
||||
{ title: "Startup Sound", key: "startup-sound" },
|
||||
React.createElement(TableSwitchRow, {
|
||||
label: "Enable Startup Sound",
|
||||
value: config.enabled,
|
||||
onValueChange: (v: boolean) => updateConfig("enabled", v),
|
||||
}),
|
||||
React.createElement(TableRow, {
|
||||
label: "Play Chance",
|
||||
subLabel: `Current: ${config.probability}% chance to play on startup`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Probability input in its own invisible group
|
||||
if (TextInput) {
|
||||
settingsElements.push(
|
||||
React.createElement(
|
||||
TableRowGroup,
|
||||
{ title: "", key: "probability-input-group" },
|
||||
React.createElement(TextInput, {
|
||||
placeholder: "Enter probability (0-100)",
|
||||
value: config.probability.toString(),
|
||||
onChange: handleProbabilityChange,
|
||||
key: "probability-input",
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// About section
|
||||
settingsElements.push(
|
||||
React.createElement(
|
||||
TableRowGroup,
|
||||
{ title: "About", key: "about" },
|
||||
React.createElement(TableRow, {
|
||||
label: "Description",
|
||||
subLabel:
|
||||
"Shiggy Enhancements, more options, more features. Originally Kettu Enhancements with additinal features.",
|
||||
disabled: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return React.createElement(
|
||||
ScrollView,
|
||||
{ style: { flex: 1 } },
|
||||
React.createElement(
|
||||
Stack,
|
||||
{ spacing: 8, style: { padding: 10 } },
|
||||
...settingsElements,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
start() {
|
||||
logger.log("Shiggy Enhancements: Starting plugin");
|
||||
|
||||
// Initialize sound settings
|
||||
settings.startupSound = settings.startupSound || {
|
||||
enabled: false,
|
||||
probability: 2,
|
||||
};
|
||||
|
||||
// Check if DCDSoundManager is available
|
||||
if (!DCDSoundManager) {
|
||||
logger.error("DCDSoundManager not found - startup sound disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Play startup sound if enabled and probability check passes
|
||||
if (settings.startupSound.enabled && shouldPlaySound()) {
|
||||
logger.log(
|
||||
`Attempting to play startup sound (${settings.startupSound.probability}% chance)`,
|
||||
);
|
||||
|
||||
// Always prepare the sound fresh each time
|
||||
prepareSound()
|
||||
.then(function (sound) {
|
||||
if (sound) {
|
||||
isPrepared = true;
|
||||
soundDuration = sound.duration || 5000;
|
||||
logger.log(`Sound prepared, duration: ${soundDuration}ms`);
|
||||
playSound();
|
||||
logger.log(
|
||||
`Startup sound played (${settings.startupSound?.probability}% chance)`,
|
||||
);
|
||||
} else {
|
||||
logger.error("Failed to prepare sound");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Error in sound preparation:", error);
|
||||
});
|
||||
} else if (settings.startupSound.enabled) {
|
||||
logger.log(
|
||||
`Startup sound skipped due to probability (${settings.startupSound?.probability}% chance)`,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
logger.log("Shiggy Enhancements: Stopping plugin");
|
||||
|
||||
// Stop and cleanup audio
|
||||
if (DCDSoundManager && isPlaying) {
|
||||
DCDSoundManager.stop(soundId);
|
||||
isPlaying = false;
|
||||
}
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
// Reset preparation state so it can play again next time
|
||||
isPrepared = false;
|
||||
},
|
||||
});
|
||||
|
|
@ -23,54 +23,51 @@ const { showSimpleActionSheet } = lazyDestructure(() =>
|
|||
|
||||
// TODO: These styles work weirdly. Low DPI Has cramped text. Fix?
|
||||
const useStyles = createStyles({
|
||||
card: {
|
||||
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
|
||||
borderRadius: 12,
|
||||
overflow: "hidden"
|
||||
},
|
||||
header: {
|
||||
padding: 0,
|
||||
},
|
||||
headerLeading: {
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
scale: 1.2
|
||||
},
|
||||
headerTrailing: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 15,
|
||||
alignItems: "center"
|
||||
},
|
||||
headerLabel: {
|
||||
...TextStyleSheet["heading-lg/semibold"],
|
||||
},
|
||||
headerLabelCompact: {
|
||||
...TextStyleSheet["text-md/semibold"],
|
||||
},
|
||||
headerSubtitle: {
|
||||
...TextStyleSheet["text-sm/semibold"],
|
||||
color: semanticColors.TEXT_MUTED,
|
||||
},
|
||||
descriptionLabel: {
|
||||
...TextStyleSheet["text-md/semibold"],
|
||||
color: "text-strong",
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row-reverse",
|
||||
alignItems: "center",
|
||||
gap: 5
|
||||
},
|
||||
iconStyle: {
|
||||
tintColor: semanticColors.LOGO_PRIMARY,
|
||||
opacity: 0.2,
|
||||
height: 64,
|
||||
width: 64,
|
||||
left: void 0,
|
||||
right: "30%",
|
||||
top: "-10%"
|
||||
}
|
||||
card: {
|
||||
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
},
|
||||
header: {
|
||||
padding: 0,
|
||||
},
|
||||
headerLeading: {
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
scale: 1.2,
|
||||
},
|
||||
headerTrailing: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 15,
|
||||
alignItems: "center",
|
||||
},
|
||||
headerLabel: {
|
||||
...TextStyleSheet["heading-md/semibold"],
|
||||
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
|
||||
},
|
||||
headerSubtitle: {
|
||||
...TextStyleSheet["text-md/semibold"],
|
||||
color: semanticColors.TEXT_MUTED,
|
||||
},
|
||||
descriptionLabel: {
|
||||
...TextStyleSheet["text-md/semibold"],
|
||||
color: semanticColors.TEXT_NORMAL,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row-reverse",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
},
|
||||
iconStyle: {
|
||||
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
|
||||
opacity: 0.2,
|
||||
height: 64,
|
||||
width: 64,
|
||||
left: void 0,
|
||||
right: "30%",
|
||||
top: "-10%",
|
||||
},
|
||||
});
|
||||
|
||||
interface Action {
|
||||
|
|
@ -111,9 +108,9 @@ export default function AddonCard(props: CardProps) {
|
|||
<Stack spacing={16}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<View style={styles.headerLeading}>
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerLabel}>{props.headerLabel}</Text>
|
||||
<Text style={styles.headerLabel}>{props.headerLabel}</Text>
|
||||
{props.headerSublabel && (
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerSubtitle}>{props.headerSublabel}</Text>
|
||||
<Text style={styles.headerSubtitle}>{props.headerSublabel}</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={[styles.headerTrailing, { marginLeft: "auto" }]}>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
TableSwitchRow,
|
||||
Stack,
|
||||
} from "@metro/common/components";
|
||||
import { semanticColors } from "@ui/color";
|
||||
import { ScrollView, View } from "react-native";
|
||||
|
||||
import ErrorComponentStackCard from "./ErrorComponentStackCard";
|
||||
|
|
@ -73,8 +74,8 @@ export default function ErrorBoundaryScreen(props: {
|
|||
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
|
||||
<Text
|
||||
variant="heading-md/extrabold"
|
||||
color="text-default"
|
||||
style={{
|
||||
color: semanticColors.HEADER_PRIMARY,
|
||||
marginBottom: 8,
|
||||
textAlign: "center",
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export { shiggycordIcon };
|
|||
|
||||
export default function initSettings() {
|
||||
registerSection({
|
||||
name: "GoonCord",
|
||||
name: "💦GoonCord💦",
|
||||
items: [
|
||||
{
|
||||
key: "SHIGGYCORD",
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export default function About() {
|
|||
variant="text-md/medium"
|
||||
style={{ textAlign: "center", marginTop: 4 }}
|
||||
>
|
||||
💦Goon💦 your discord client!
|
||||
Goon your discord client!
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
|
|
|||
|
|
@ -32,19 +32,7 @@ export default function General() {
|
|||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
// Custom Community Card Button
|
||||
const CommunityCardButton = ({
|
||||
icon,
|
||||
label,
|
||||
subLabel,
|
||||
color,
|
||||
onPress,
|
||||
}: {
|
||||
icon?: number | { uri: string };
|
||||
label: string;
|
||||
subLabel?: string | null;
|
||||
color?: string;
|
||||
onPress?: () => void;
|
||||
}) => (
|
||||
const CommunityCardButton = ({ icon, label, subLabel, color, onPress }) => (
|
||||
<TouchableOpacity style={{ flex: 1 }} onPress={onPress} activeOpacity={0.7}>
|
||||
<Card
|
||||
style={{
|
||||
|
|
@ -72,7 +60,7 @@ export default function General() {
|
|||
}}
|
||||
>
|
||||
<TableRow.Icon
|
||||
source={icon as any}
|
||||
source={icon}
|
||||
style={{
|
||||
tintColor: "#FFFFFF",
|
||||
width: 24,
|
||||
|
|
@ -125,8 +113,8 @@ export default function General() {
|
|||
>
|
||||
<TableRowGroup title="App Information">
|
||||
<TableRow
|
||||
label="GoonCord"
|
||||
icon={<TableRow.Icon source={{ uri: shiggycordIcon ?? "" }} />}
|
||||
label="💦GoonCord💦"
|
||||
icon={<TableRow.Icon source={{ uri: shiggycordIcon }} />}
|
||||
trailing={<TableRow.TrailingText text={debugInfo.bunny.version} />}
|
||||
/>
|
||||
<TableRow
|
||||
|
|
@ -144,7 +132,7 @@ export default function General() {
|
|||
subLabel={`${debugInfo.bunny.loader.name} loader`}
|
||||
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
|
||||
trailing={
|
||||
<TableRow.TrailingText text={debugInfo.bunny.loader.version ?? ""} />
|
||||
<TableRow.TrailingText text={debugInfo.bunny.loader.version} />
|
||||
}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
|
|
@ -200,7 +188,7 @@ export default function General() {
|
|||
<TableSwitchRow
|
||||
label={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS}
|
||||
subLabel={Strings.SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC}
|
||||
icon={<TableRow.Icon source={findAssetId("StaffBadgeIcon")} />}
|
||||
icon={<TableRow.Icon source={findAssetId("FlaskIcon")} />}
|
||||
value={settings.enableDiscordDeveloperSettings}
|
||||
onValueChange={(v: boolean) => {
|
||||
settings.enableDiscordDeveloperSettings = v;
|
||||
|
|
|
|||
|
|
@ -702,7 +702,7 @@ export default function Plugins() {
|
|||
// Use the insertion order of pluginSettings so newly-installed plugins
|
||||
// (which add entries to pluginSettings) appear first.
|
||||
const installedBnIds = Object.keys(pluginSettings || {})
|
||||
.filter((id) => registeredPlugins.has(id))
|
||||
.filter((id) => registeredPlugins.has(id) && !isCorePlugin(id))
|
||||
.slice()
|
||||
.reverse();
|
||||
|
||||
|
|
@ -710,12 +710,12 @@ export default function Plugins() {
|
|||
.map((id) => registeredPlugins.get(id)!)
|
||||
.map(unifyBunnyPlugin);
|
||||
|
||||
// Merge lists: show Vendetta-managed plugins first,
|
||||
// Merge lists: show Vendetta-managed plugins first (recent first),
|
||||
// then Bunny-installed externals (recent first).
|
||||
const allPlugins = [...vdPlugins, ...bnPlugins];
|
||||
|
||||
// Include core plugins in the list
|
||||
return allPlugins;
|
||||
// Filter out core plugins from the list
|
||||
return allPlugins.filter((plugin) => !isCorePlugin(plugin.id));
|
||||
}}
|
||||
ListHeaderComponent={() => null}
|
||||
installAction={{
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import {
|
|||
Text,
|
||||
} from "@metro/common/components";
|
||||
import { View, Image } from "react-native";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { BundleUpdaterManager } from "@lib/api/native/modules";
|
||||
|
||||
const { useReducer } = React;
|
||||
|
||||
|
|
@ -350,12 +352,58 @@ 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() {
|
||||
useProxy(settings);
|
||||
useProxy(themes);
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
|
||||
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 [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ async function initializeShiggyCord() {
|
|||
Object.freeze = Object.seal = Object;
|
||||
|
||||
await require("@metro/internals/caches").initMetroCache();
|
||||
require(".").default();
|
||||
await require(".").default();
|
||||
} catch (e) {
|
||||
const { ClientInfoManager } = require("@lib/api/native/modules");
|
||||
const stack = e instanceof Error ? e.stack : undefined;
|
||||
|
|
|
|||
58
src/index.ts
58
src/index.ts
|
|
@ -11,8 +11,13 @@ import { injectFluxInterceptor } from "@lib/api/flux";
|
|||
import { patchJsx } from "@lib/api/react/jsx";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
import { patchSettings } from "@ui/settings";
|
||||
import { updaterSettings } from "@lib/api/settings";
|
||||
import { InteractionManager } from "react-native";
|
||||
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 { timings } from "@lib/utils/timings";
|
||||
|
||||
|
|
@ -61,19 +66,52 @@ export default async () => {
|
|||
const { VdPluginManager } = await import("@core/vendetta/plugins");
|
||||
const { initPlugins, updatePlugins } = await import("@lib/addons/plugins");
|
||||
|
||||
await Promise.all([
|
||||
VdPluginManager.initPlugins()
|
||||
.then((u) => u && lib.unload.push(u))
|
||||
.catch((e) => logger.log("Vendetta init failed:", e)),
|
||||
initPlugins()
|
||||
.catch((e) => logger.log("initPlugins failed:", e)),
|
||||
]);
|
||||
// Initialize Vendetta plugins (may start many plugins) — do not block UI.
|
||||
VdPluginManager.initPlugins()
|
||||
.then((u) => lib.unload.push(u))
|
||||
.catch((e) => logger.log("Vendetta init failed:", e));
|
||||
|
||||
updatePlugins()
|
||||
.catch((e) => logger.log("updatePlugins failed:", e));
|
||||
// Start ShiggyCord (Bunny) plugins now without forcing repository updates.
|
||||
// Plugin repository fetching is deferred so the app can finish launching first.
|
||||
// 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
|
||||
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).
|
||||
|
|
@ -88,5 +126,5 @@ export default async () => {
|
|||
}
|
||||
|
||||
// Final ready log for basic UI availability.
|
||||
logger.log("ShiggyCord is ready.");
|
||||
logger.log("GoonCord is ready.");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -752,54 +752,27 @@ 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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Initialize plugins but stagger startup to avoid CPU spikes during app start.
|
||||
*
|
||||
* Safe to call multiple times — skips already-registered plugins.
|
||||
* This replaces the original "start all enabled plugins in parallel" approach
|
||||
* 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 registerInstalledPlugins() {
|
||||
await awaitStorage(pluginRepositories, pluginSettings);
|
||||
export async function initPlugins(
|
||||
options: { staggerInterval?: number; batchSize?: number } = {},
|
||||
) {
|
||||
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
|
||||
// settings pages and UI that read registered/core plugins show them.
|
||||
// registerCorePlugins is synchronous and idempotent, but call it here as a
|
||||
|
|
@ -811,7 +784,6 @@ export async function initPlugins() {
|
|||
}
|
||||
|
||||
await awaitStorage(pluginRepositories, pluginSettings);
|
||||
await registerInstalledPlugins();
|
||||
|
||||
// Collect enabled plugin ids
|
||||
const enabledIds = [...registeredPlugins.keys()].filter((id) =>
|
||||
|
|
@ -820,18 +792,32 @@ export async function initPlugins() {
|
|||
|
||||
if (enabledIds.length === 0) return;
|
||||
|
||||
// Start all enabled plugins in parallel
|
||||
await Promise.allSettled(
|
||||
enabledIds.map(async (id) => {
|
||||
try {
|
||||
// Only attempt to start if still enabled and not already started
|
||||
if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
|
||||
await startPlugin(id).catch((e) => {
|
||||
console.error(`Failed to start plugin ${id}:`, e);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error while starting plugin ${id}:`, error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
// Helper sleep
|
||||
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(
|
||||
batch.map(async (id) => {
|
||||
try {
|
||||
// Only attempt to start if still enabled and not already started
|
||||
if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
|
||||
await startPlugin(id).catch((e) => {
|
||||
// startPlugin may throw; ensure it doesn't crash the whole batch
|
||||
console.error(`Failed to start plugin ${id}:`, e);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error while starting plugin ${id}:`, error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// If there are more plugins to start, wait before starting the next batch.
|
||||
if (i + batchSize < enabledIds.length) {
|
||||
await sleep(staggerInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,77 +13,53 @@ const Messages = findByFilePathLazy("modules/messages/native/Messages.tsx", true
|
|||
function ThemeBackground({ children }: { children: React.ReactNode; }) {
|
||||
useObservable([colorsPref]);
|
||||
|
||||
const background = _colorRef.current?.background;
|
||||
const url = background?.url;
|
||||
const blur = background?.blur;
|
||||
|
||||
if (
|
||||
!_colorRef.current ||
|
||||
colorsPref.customBackground === "hidden" ||
|
||||
!url
|
||||
if (!_colorRef.current
|
||||
|| colorsPref.customBackground === "hidden"
|
||||
|| !_colorRef.current.background?.url
|
||||
|| _colorRef.current.background?.blur && (typeof _colorRef.current.background?.blur !== "number")
|
||||
) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (blur !== undefined && typeof blur !== "number") {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
style={{ flex: 1, height: "100%" }}
|
||||
source={{ uri: url }}
|
||||
blurRadius={typeof blur === "number" ? blur : 0}
|
||||
>
|
||||
{children}
|
||||
</ImageBackground>
|
||||
);
|
||||
return <ImageBackground
|
||||
style={{ flex: 1, height: "100%" }}
|
||||
source={{ uri: _colorRef.current.background?.url }}
|
||||
blurRadius={_colorRef.current.background?.blur}
|
||||
>
|
||||
{children}
|
||||
</ImageBackground>;
|
||||
}
|
||||
|
||||
export default function patchChatBackground() {
|
||||
try {
|
||||
if (!Messages) return;
|
||||
|
||||
const patches = [
|
||||
after("render", Messages, (_, ret) => {
|
||||
if (!ret) return ret;
|
||||
if (!_colorRef.current || !_colorRef.current.background?.url) return;
|
||||
|
||||
try {
|
||||
if (!_colorRef.current || !_colorRef.current.background?.url) return ret;
|
||||
const messagesComponent = findInReactTree(
|
||||
ret,
|
||||
x => x && "HACK_fixModalInteraction" in x.props && x?.props?.style
|
||||
);
|
||||
|
||||
const messagesComponent = findInReactTree(
|
||||
ret,
|
||||
x => x && "HACK_fixModalInteraction" in (x.props ?? {}) && x?.props?.style
|
||||
);
|
||||
if (messagesComponent) {
|
||||
const flattened = StyleSheet.flatten(messagesComponent.props.style);
|
||||
const backgroundColor = getChroma()(
|
||||
flattened.backgroundColor || "black"
|
||||
).alpha(
|
||||
1 - (_colorRef.current.background?.opacity ?? 1)
|
||||
).hex();
|
||||
|
||||
if (messagesComponent) {
|
||||
try {
|
||||
const flattened = StyleSheet.flatten(messagesComponent.props.style);
|
||||
const backgroundColor = getChroma()(
|
||||
flattened.backgroundColor || "black"
|
||||
).alpha(
|
||||
1 - (_colorRef.current.background?.opacity ?? 1)
|
||||
).hex();
|
||||
|
||||
messagesComponent.props.style = StyleSheet.flatten([
|
||||
messagesComponent.props.style,
|
||||
{ backgroundColor }
|
||||
]);
|
||||
} catch (e) {
|
||||
logger.warn("Background patch: failed to modify style", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Background patch: render error", e);
|
||||
return ret;
|
||||
messagesComponent.props.style = StyleSheet.flatten([
|
||||
messagesComponent.props.style,
|
||||
{ backgroundColor }
|
||||
]);
|
||||
}
|
||||
|
||||
return <ThemeBackground>{ret}</ThemeBackground>;
|
||||
})
|
||||
];
|
||||
|
||||
return () => patches.forEach(x => x?.());
|
||||
return () => patches.forEach(x => x());
|
||||
} catch (e) {
|
||||
logger.error("Background patch: failed to initialize", e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ import { _colorRef } from "@lib/addons/themes/colors/updater";
|
|||
import { NativeThemeModule } from "@lib/api/native/modules";
|
||||
import { before, instead } from "@lib/api/patcher";
|
||||
import { findByProps } from "@metro";
|
||||
import chroma from "chroma-js";
|
||||
import { byMutableProp } from "@metro/filters";
|
||||
import { createLazyModule } from "@metro/lazy";
|
||||
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
|
||||
|
||||
const tokenReference = findByProps("SemanticColor");
|
||||
const themeTypes = findByProps("ThemeTypes")?.ThemeTypes;
|
||||
|
||||
const origRawColor = { ...tokenReference.RawColor };
|
||||
const origDarker = themeTypes.DARKER as string;
|
||||
const origLight = themeTypes.LIGHT as string;
|
||||
const isThemeModule = createLazyModule(byMutableProp("isThemeDark"));
|
||||
|
||||
const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
||||
"BG_BACKDROP": "BACKGROUND_FLOATING",
|
||||
|
|
@ -24,19 +22,13 @@ const SEMANTIC_FALLBACK_MAP: Record<string, string> = {
|
|||
"BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY"
|
||||
};
|
||||
|
||||
export default function patchDefinitionAndResolver() {
|
||||
const callback = ([theme]: any[]) => theme === _colorRef.key ? [_colorRef.current!.reference] : void 0;
|
||||
const origRawColor = { ...tokenReference.RawColor };
|
||||
|
||||
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,
|
||||
});
|
||||
export default function patchDefinitionAndResolver() {
|
||||
const callback = ([theme]: any[]) => {
|
||||
if (!_colorRef.current) return void 0;
|
||||
return theme === _colorRef.key ? [_colorRef.current.reference] : void 0;
|
||||
};
|
||||
|
||||
Object.keys(tokenReference.RawColor).forEach(key => {
|
||||
Object.defineProperty(tokenReference.RawColor, key, {
|
||||
|
|
@ -45,48 +37,50 @@ export default function patchDefinitionAndResolver() {
|
|||
get: () => {
|
||||
const ret = _colorRef.current?.raw[key];
|
||||
if (ret) return ret;
|
||||
|
||||
return origRawColor[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const unpatches = [
|
||||
before("isThemeDark", isThemeModule, callback),
|
||||
before("isThemeLight", isThemeModule, callback),
|
||||
before("updateTheme", NativeThemeModule, callback),
|
||||
instead("resolveSemanticColor", tokenReference.default.meta ?? tokenReference.default.internal, (args: any[], orig: any) => {
|
||||
if (!_colorRef.current) return orig(...args);
|
||||
const currentRef = _colorRef.current;
|
||||
if (args[0] !== _colorRef.key) return orig(...args);
|
||||
|
||||
args[0] = _colorRef.current.reference;
|
||||
// Use the captured reference value
|
||||
args[0] = currentRef.reference;
|
||||
|
||||
const [name, colorDef] = extractInfo(_colorRef.current!.reference, args[1]);
|
||||
const [name, colorDef] = extractInfo(currentRef.reference, args[1]);
|
||||
|
||||
let semanticDef = _colorRef.current.semantic[name];
|
||||
if (!semanticDef && _colorRef.current.spec === 2 && name in SEMANTIC_FALLBACK_MAP) {
|
||||
semanticDef = _colorRef.current.semantic[SEMANTIC_FALLBACK_MAP[name]];
|
||||
// If extractInfo couldn't resolve a color definition, fall back to the original resolver
|
||||
if (!name || !colorDef) return orig(...args);
|
||||
|
||||
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) {
|
||||
return semanticDef.opacity === 1
|
||||
? semanticDef.value
|
||||
: chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
|
||||
if (semanticDef.opacity === 1) return semanticDef.value;
|
||||
return getChroma()(semanticDef.value).alpha(semanticDef.opacity).hex();
|
||||
}
|
||||
|
||||
const rawValue = _colorRef.current.raw[colorDef.raw];
|
||||
const rawKey = colorDef.raw;
|
||||
const rawValue = rawKey ? currentRef.raw[rawKey] : undefined;
|
||||
if (rawValue) {
|
||||
// Set opacity if needed
|
||||
return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex();
|
||||
return (colorDef.opacity === 1) ? rawValue : getChroma()(rawValue).alpha(colorDef.opacity).hex();
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
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", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
|
|
@ -101,7 +95,13 @@ export default function patchDefinitionAndResolver() {
|
|||
function extractInfo(themeName: string, colorObj: any): [name: string, colorDef: any] {
|
||||
// @ts-ignore - assigning to extractInfo._sym
|
||||
const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]];
|
||||
const colorDef = tokenReference.SemanticColor[propName];
|
||||
const entry = tokenReference.SemanticColor[propName];
|
||||
|
||||
return [propName, colorDef[themeName]];
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return [propName, undefined];
|
||||
}
|
||||
|
||||
const colorDef = Object.prototype.hasOwnProperty.call(entry, themeName) ? entry[themeName] : undefined;
|
||||
|
||||
return [propName, colorDef];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,39 @@ const mmkvStorage = proxyLazy(() => {
|
|||
|
||||
export default function patchStorage() {
|
||||
const patchedKeys = new Set(["ThemeStore", "SelectivelySyncedUserSettingsStore"]);
|
||||
const sanitizedKeys = new Set<string>();
|
||||
|
||||
const patches = [
|
||||
after("get", mmkvStorage, ([key], ret) => {
|
||||
if (!_colorRef.current || !patchedKeys.has(key)) return;
|
||||
if (!patchedKeys.has(key)) return;
|
||||
|
||||
const state = findInTree(ret._state, s => typeof s.theme === "string");
|
||||
if (state) state.theme = _colorRef.key;
|
||||
if (!state) return;
|
||||
|
||||
const lastSetDiscordTheme = _colorRef.lastSetDiscordTheme ?? "darker";
|
||||
|
||||
if (/^bn-theme-\d+$/.test(state.theme) && !/^bn-theme-\d+$/.test(lastSetDiscordTheme) && !sanitizedKeys.has(key)) {
|
||||
try {
|
||||
const newVal = { ...ret._state, theme: lastSetDiscordTheme };
|
||||
mmkvStorage.set(key, newVal);
|
||||
sanitizedKeys.add(key);
|
||||
} catch (e) {
|
||||
}
|
||||
state.theme = lastSetDiscordTheme;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_colorRef.current) state.theme = _colorRef.key;
|
||||
}),
|
||||
before("set", mmkvStorage, ([key, value]) => {
|
||||
if (!patchedKeys.has(key)) return;
|
||||
|
||||
const json = JSON.stringify(value);
|
||||
const lastSetDiscordTheme = _colorRef.lastSetDiscordTheme ?? "darker";
|
||||
const replacementTheme = /^bn-theme-\d+$/.test(lastSetDiscordTheme) ? "darker" : lastSetDiscordTheme;
|
||||
const replaced = json.replace(
|
||||
/"theme":"bn-theme-\d+"/,
|
||||
`"theme":${JSON.stringify(lastSetDiscordTheme)}`
|
||||
`"theme":${JSON.stringify(replacementTheme)}`
|
||||
);
|
||||
|
||||
return [key, JSON.parse(replaced)];
|
||||
|
|
|
|||
|
|
@ -24,71 +24,46 @@ export const _colorRef: InternalColorRef = {
|
|||
current: null,
|
||||
key: `bn-theme-${_inc}`,
|
||||
origRaw: origRawColor,
|
||||
lastSetDiscordTheme: "darker"
|
||||
lastSetDiscordTheme: "dark"
|
||||
};
|
||||
|
||||
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true }) {
|
||||
export function updateBunnyColor(colorManifest: ColorManifest | null, { update = true } ) {
|
||||
if (settings.safeMode?.enabled) return;
|
||||
|
||||
const internalDef = colorManifest ? parseColorManifest(colorManifest) : null;
|
||||
const ref = Object.assign(_colorRef, {
|
||||
current: internalDef,
|
||||
key: `bn-theme-${++_inc}`,
|
||||
lastSetDiscordTheme: !ThemeStore.theme.startsWith("bn-theme-")
|
||||
? ThemeStore.theme
|
||||
: _colorRef.lastSetDiscordTheme
|
||||
});
|
||||
|
||||
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 {
|
||||
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,
|
||||
key: keyName,
|
||||
lastSetDiscordTheme: lastSet
|
||||
});
|
||||
|
||||
if (internalDef != null) {
|
||||
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];
|
||||
|
||||
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.Shadow).forEach(k => tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference]);
|
||||
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.current!.reference]
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to register custom theme key, falling back", e);
|
||||
ref.current = null; // force fallback path below
|
||||
}
|
||||
}
|
||||
|
||||
if (update) {
|
||||
AppearanceManager.setShouldSyncAppearanceSettings(false);
|
||||
|
||||
const targetTheme = ref.current != null ? ref.key : ref.lastSetDiscordTheme;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
const fallback = (_colorRef.lastSetDiscordTheme && !_colorRef.lastSetDiscordTheme.startsWith("bn-theme-"))
|
||||
? _colorRef.lastSetDiscordTheme
|
||||
: "dark";
|
||||
AppearanceManager.updateTheme(internalDef != null ? ref.key : fallback);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export function getCurrentTheme() {
|
|||
* @internal
|
||||
*/
|
||||
export function getThemeFromLoader(): VdThemeInfo | null {
|
||||
return getStoredTheme() ?? getCurrentTheme();
|
||||
return getStoredTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,7 +157,6 @@ export async function initThemes() {
|
|||
}
|
||||
|
||||
await newAwaitStorage(colorsPref);
|
||||
await awaitStorage(themes);
|
||||
|
||||
const currentTheme = getThemeFromLoader();
|
||||
initColors(currentTheme?.data ?? null);
|
||||
|
|
|
|||
|
|
@ -401,4 +401,4 @@ export function initDebugger() {
|
|||
logger.error("Failed to connect to ReactDevTools during startup:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ export const dispatcher = FluxDispatcher;
|
|||
|
||||
type Intercept = (payload: Record<string, any> & { type: string; }) => any;
|
||||
let intercepts: Intercept[] = [];
|
||||
const interceptIndex = new Map<Intercept, number>();
|
||||
let nextInterceptId = 0;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
@ -35,14 +33,7 @@ export function injectFluxInterceptor() {
|
|||
|
||||
(dispatcher._interceptors ??= []).unshift(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));
|
||||
}
|
||||
};
|
||||
return () => dispatcher._interceptors &&= dispatcher._interceptors.filter(v => v !== cb);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,16 +41,9 @@ export function injectFluxInterceptor() {
|
|||
* nullish -> nothing, falsy -> block, object -> modify
|
||||
*/
|
||||
export function intercept(cb: Intercept) {
|
||||
const id = nextInterceptId++;
|
||||
interceptIndex.set(cb, id);
|
||||
intercepts.push(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));
|
||||
}
|
||||
intercepts = intercepts.filter(i => i !== cb);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ const rainLoaderIdentity = globalThis.__RAIN_LOADER__;
|
|||
// @ts-ignore
|
||||
const vendettaLoaderIdentity = globalThis.__vendetta_loader;
|
||||
|
||||
// @ts-ignore
|
||||
const shiggyLoaderIdentity = globalThis.__SHIGGY_LOADER__;
|
||||
|
||||
export interface VendettaLoaderIdentity {
|
||||
name: string;
|
||||
features: {
|
||||
|
|
@ -39,10 +36,6 @@ export function isRa1nLoader() {
|
|||
return rainLoaderIdentity != null;
|
||||
}
|
||||
|
||||
export function isShiggyLoader() {
|
||||
return shiggyLoaderIdentity != null;
|
||||
}
|
||||
|
||||
function polyfillVendettaLoaderIdentity() {
|
||||
if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null;
|
||||
|
||||
|
|
@ -105,8 +98,6 @@ export function getLoaderIdentity() {
|
|||
return getVendettaLoaderIdentity();
|
||||
} else if (isRa1nLoader()) {
|
||||
return rainLoaderIdentity();
|
||||
} else if (isShiggyLoader()) {
|
||||
return shiggyLoaderIdentity;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -125,7 +116,6 @@ export function getLoaderName() {
|
|||
if (isPyonLoader()) return pyonLoaderIdentity.loaderName;
|
||||
else if (isRa1nLoader()) return rainLoaderIdentity.loadername;
|
||||
else if (isVendettaLoader()) return vendettaLoaderIdentity.name;
|
||||
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderName;
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
|
@ -133,7 +123,6 @@ export function getLoaderName() {
|
|||
export function getLoaderVersion(): string | null {
|
||||
if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion;
|
||||
else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion;
|
||||
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderVersion;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -155,9 +144,7 @@ export function isThemeSupported() {
|
|||
} else if (isVendettaLoader()) {
|
||||
return vendettaLoaderIdentity!!.features.themes != null;
|
||||
} else if (isRa1nLoader()) {
|
||||
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; // Ra1n has theme support disabled, this is here just to make sure it doesnt think it does
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -171,8 +158,6 @@ export function getStoredTheme(): VdThemeInfo | null {
|
|||
if (!themeProp) return null;
|
||||
// @ts-ignore
|
||||
return globalThis[themeProp] || null;
|
||||
} else if (isShiggyLoader()) {
|
||||
return shiggyLoaderIdentity.storedTheme || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -183,8 +168,6 @@ export function getThemeFilePath() {
|
|||
return "pyoncord/current-theme.json";
|
||||
} else if (isVendettaLoader()) {
|
||||
return "vendetta_theme.json";
|
||||
} else if (isShiggyLoader()) {
|
||||
return "ShiggyCord/current-theme.json";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -258,7 +241,6 @@ export function getLoaderConfigPath() {
|
|||
|
||||
export function isFontSupported() {
|
||||
if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2;
|
||||
else if (isShiggyLoader()) return shiggyLoaderIdentity.fontPatch === 2;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -266,4 +248,4 @@ export function isFontSupported() {
|
|||
export async function clearBundle() {
|
||||
// TODO: This should be not be hardcoded, maybe put in loader.json?
|
||||
return void await removeCacheFile("bundle.js");
|
||||
}
|
||||
}
|
||||
|
|
@ -5,14 +5,15 @@ import type { ImageURISource } from "react-native";
|
|||
import { patchTabsUI } from "./patches/tabs";
|
||||
|
||||
export interface RowConfig {
|
||||
key: string;
|
||||
title: () => string;
|
||||
onPress?: () => any;
|
||||
render?: Parameters<typeof lazy>[0];
|
||||
icon?: ImageURISource | number;
|
||||
IconComponent?: React.ReactNode,
|
||||
usePredicate?: () => boolean,
|
||||
useTrailing?: () => string | JSX.Element
|
||||
key: string;
|
||||
title: () => string;
|
||||
onPress?: () => any;
|
||||
render?: Parameters<typeof lazy>[0];
|
||||
icon?: ImageURISource | number;
|
||||
IconComponent?: React.ReactNode;
|
||||
usePredicate?: () => boolean;
|
||||
useTrailing?: () => string | JSX.Element;
|
||||
rawTabsConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const registeredSections = {} as {
|
||||
|
|
|
|||
|
|
@ -57,15 +57,15 @@ export function patchPanelUI(unpatches: (() => void | boolean)[]) {
|
|||
after("default", findByNameLazy("getScreens", false), (_a, screens) => ({
|
||||
...screens,
|
||||
VendettaCustomPage: {
|
||||
title: "ShiggyCord",
|
||||
title: "GoonCord",
|
||||
render: () => <CustomPageRenderer />,
|
||||
},
|
||||
BUNNY_CUSTOM_PAGE: {
|
||||
title: "ShiggyCord",
|
||||
title: "GoonCord",
|
||||
render: () => <CustomPageRenderer />,
|
||||
},
|
||||
SHIGGYCORD_CUSTOM_PAGE: {
|
||||
title: "ShiggyCord",
|
||||
title: "GoonCord",
|
||||
render: () => <CustomPageRenderer />,
|
||||
},
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { after } from "@lib/api/patcher";
|
|||
import { TableRow } from "@metro/common/components";
|
||||
import { findByNameLazy, findByPropsLazy } from "@metro/wrappers";
|
||||
import { registeredSections } from "@ui/settings";
|
||||
import { Strings } from "@core/i18n";
|
||||
|
||||
import { CustomPageRenderer, wrapOnPress } from "./shared";
|
||||
import { findInReactTree } from "@lib/utils";
|
||||
|
|
@ -28,9 +29,9 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
|||
IconComponent: () => <TableRow.Icon source={row.icon} />,
|
||||
usePredicate: row.usePredicate,
|
||||
useTrailing: row.useTrailing,
|
||||
useTitle: row.title,
|
||||
onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
|
||||
withArrow: true,
|
||||
...row.rawTabsConfig,
|
||||
},
|
||||
})),
|
||||
)
|
||||
|
|
@ -47,7 +48,6 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
|||
VendettaCustomPage: {
|
||||
type: "route",
|
||||
title: () => "ShiggyCord",
|
||||
useTitle: () => "ShiggyCord",
|
||||
screen: {
|
||||
route: "VendettaCustomPage",
|
||||
getComponent: () => CustomPageRenderer,
|
||||
|
|
@ -56,7 +56,6 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
|||
SHIGGYCORD_CUSTOM_PAGE: {
|
||||
type: "route",
|
||||
title: () => "ShiggyCord",
|
||||
useTitle: () => "ShiggyCord",
|
||||
screen: {
|
||||
route: "SHIGGYCORD_CUSTOM_PAGE",
|
||||
getComponent: () => CustomPageRenderer,
|
||||
|
|
@ -65,7 +64,6 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
|
|||
BUNNY_CUSTOM_PAGE: {
|
||||
type: "route",
|
||||
title: () => "ShiggyCord",
|
||||
useTitle: () => "ShiggyCord",
|
||||
screen: {
|
||||
route: "BUNNY_CUSTOM_PAGE",
|
||||
getComponent: () => CustomPageRenderer,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
export type SearchTree = Record<string, any>;
|
||||
export type SearchFilter = (tree: SearchTree) => boolean;
|
||||
export interface FindInTreeOptions {
|
||||
walkable?: string[] | Set<string>;
|
||||
ignore?: string[] | Set<string>;
|
||||
walkable?: string[];
|
||||
ignore?: string[];
|
||||
maxDepth?: number;
|
||||
}
|
||||
|
||||
function treeSearch(tree: SearchTree, filter: SearchFilter, walkable: Set<string>, ignore: Set<string>, maxDepth: number, depth: number): any {
|
||||
if (depth > maxDepth) return;
|
||||
function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindInTreeOptions>, depth: number): any {
|
||||
if (depth > opts.maxDepth) return;
|
||||
if (!tree) return;
|
||||
|
||||
try {
|
||||
|
|
@ -22,19 +22,18 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, walkable: Set<string
|
|||
if (typeof item !== "object" || item === null) continue;
|
||||
|
||||
try {
|
||||
const found = treeSearch(item, filter, walkable, ignore, maxDepth, depth + 1);
|
||||
const found = treeSearch(item, filter, opts, depth + 1);
|
||||
if (found) return found;
|
||||
} catch { }
|
||||
}
|
||||
} else if (typeof tree === "object") {
|
||||
for (const key in tree) {
|
||||
if (Object.prototype.hasOwnProperty.call(tree, key) === false) continue;
|
||||
for (const key of Object.keys(tree)) {
|
||||
if (typeof tree[key] !== "object" || tree[key] === null) continue;
|
||||
if (walkable.size && !walkable.has(key)) continue;
|
||||
if (ignore.size && ignore.has(key)) continue;
|
||||
if (opts.walkable.length && !opts.walkable.includes(key)) continue;
|
||||
if (opts.ignore.includes(key)) continue;
|
||||
|
||||
try {
|
||||
const found = treeSearch(tree[key], filter, walkable, ignore, maxDepth, depth + 1);
|
||||
const found = treeSearch(tree[key], filter, opts, depth + 1);
|
||||
if (found) return found;
|
||||
} catch { }
|
||||
}
|
||||
|
|
@ -50,5 +49,5 @@ export default function findInTree(
|
|||
maxDepth = 100
|
||||
}: FindInTreeOptions = {},
|
||||
): any | undefined {
|
||||
return treeSearch(tree, filter, new Set(walkable), new Set(ignore), maxDepth, 0);
|
||||
return treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,23 @@ let _metroCache = null as unknown as ReturnType<typeof buildInitCache>;
|
|||
export const getMetroCache = () => _metroCache;
|
||||
|
||||
function buildInitCache() {
|
||||
const modulesCount = Object.keys(window.modules).length;
|
||||
|
||||
const cache = {
|
||||
_v: CACHE_VERSION,
|
||||
_buildNumber: NativeClientInfoModule.getConstants().Build,
|
||||
_modulesCount: modulesCount,
|
||||
_modulesCount: Object.keys(window.modules).length,
|
||||
flagsIndex: {} as Record<string, number>,
|
||||
findIndex: {} as Record<string, ModulesMap | undefined>,
|
||||
polyfillIndex: {} as Record<string, ModulesMap | undefined>
|
||||
} 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;
|
||||
return cache;
|
||||
}
|
||||
|
|
@ -46,8 +52,7 @@ export async function initMetroCache() {
|
|||
_metroCache = null!;
|
||||
throw "cache invalidated; version mismatch";
|
||||
}
|
||||
let currentCount = Object.keys(window.modules).length;
|
||||
if (_metroCache._modulesCount !== currentCount) {
|
||||
if (_metroCache._modulesCount !== Object.keys(window.modules).length) {
|
||||
_metroCache = null!;
|
||||
throw "cache invalidated; modules count mismatch";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,13 +117,17 @@ for (const [id, moduleExports] of cacher.getModules()) {
|
|||
}
|
||||
|
||||
const exportsKeysLength = Reflect.ownKeys(actualExports).length;
|
||||
if (_source[prop] && exportsKeysLength <= _source[prop]) {
|
||||
if (_source[prop] && exportsKeysLength >= _source[prop]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_module[prop] = actualExports[prop];
|
||||
_source[prop] = Reflect.ownKeys(actualExports).length;
|
||||
cacher.cacheId(id);
|
||||
|
||||
if (exportsKeysLength === 1) {
|
||||
redesignProps.delete(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue