Compare commits

...

127 commits

Author SHA1 Message Date
OTKUSteyler
3c250f9081
Merge branch 'kmmiio99o:main' into main 2026-08-18 10:05:14 +02:00
OTKUSteyler
27dc770a14 Merge branch 'main' of https://github.com/OTKUSteyler/GoonCord 2026-07-31 23:34:58 +02:00
OTKUSteyler
513d9b63fa
Update README.md 2026-07-31 21:29:10 +00:00
OTKUSteyler
f916820d9b
Update release.yml 2026-07-31 21:23:56 +00:00
OTKUSteyler
3569666df2
Rename output file from shiggycord.js to gooncord.js 2026-07-31 21:23:07 +00:00
OTKUSteyler
8f272d57ef Merge remote-tracking branch 'origin/dev' 2026-07-31 23:15:37 +02:00
OTKUSteyler
8d4aa61ebd
Update index.ts 2026-07-14 17:22:13 +02:00
OTKUSteyler
0968e8b1fe
Update index.tsx 2026-07-01 16:16:42 +02:00
OTKUSteyler
380a7d8cf8
Update index.tsx 2026-07-01 16:06:48 +02:00
OTKUSteyler
6c68f5b5d6
Refactor badge data structure and fetching logic 2026-07-01 15:59:43 +02:00
OTKUSteyler
60f9e9e872
Update index.tsx 2026-07-01 14:45:36 +02:00
OTKUSteyler
992d80a6d5
Update index.tsx 2026-07-01 14:43:31 +02:00
OTKUSteyler
790157f32b
Update index.ts 2026-06-16 21:00:41 +02:00
OTKUSteyler
c356d72b00
Delete src/core/plugins/lovefemboys directory 2026-06-16 21:00:20 +02:00
OTKUSteyler
17409439cb
Update index.ts 2026-06-16 20:59:34 +02:00
OTKUSteyler
c7369fe2a9
Delete src/core/plugins/lovefemboys/utils.ts 2026-06-16 20:56:19 +02:00
OTKUSteyler
a7073e3e8a
Update index.ts 2026-06-16 20:56:11 +02:00
OTKUSteyler
972c87ec33
Update index.ts 2026-06-16 20:52:47 +02:00
OTKUSteyler
e38c36aa78
Create utils.ts 2026-06-16 20:52:36 +02:00
OTKUSteyler
a4fb30ebbf
Update index.ts 2026-06-16 20:44:29 +02:00
OTKUSteyler
254ef099f6
Delete src/core/plugins/femboy directory 2026-06-16 20:20:11 +02:00
OTKUSteyler
3e39a3a4b4
Update index.ts 2026-06-16 20:16:44 +02:00
OTKUSteyler
c310ca3128
Update index.ts 2026-06-16 20:14:07 +02:00
OTKUSteyler
282eddf544
Create index.ts 2026-06-16 20:11:42 +02:00
OTKUSteyler
cbd12e8447
Create Nyaa 2026-06-16 20:08:57 +02:00
OTKUSteyler
2697587953
Update index.tsx 2026-06-14 18:44:28 +02:00
OTKUSteyler
01e8fe8793
Delete src/core/plugins/badges/Stuff directory 2026-06-14 18:42:28 +02:00
OTKUSteyler
dc4d0049b6
Delete src/core/plugins/badges/Stuff/roles.json 2026-06-14 18:42:17 +02:00
OTKUSteyler
e5ae029e1c
Merge branch 'kmmiio99o:dev' into dev
Some checks failed
Node.js CI / CD (Bun + Forgejo) / buildAndRelease (push) Has been cancelled
2026-06-13 12:00:56 +02:00
OTKUSteyler
f9a8e9d493
Update index.tsx 2026-06-09 09:48:56 +02:00
OTKUSteyler
de20126f36
Update index.tsx 2026-06-09 08:19:29 +02:00
OTKUSteyler
de509f83b3
Update index.tsx 2026-06-09 07:53:51 +02:00
OTKUSteyler
8bd8ab2039
Update index.tsx 2026-06-09 07:35:54 +02:00
OTKUSteyler
3ba90b941e
Update index.tsx 2026-06-09 07:34:31 +02:00
OTKUSteyler
d21eea3e9b
Add files via upload 2026-06-09 07:33:13 +02:00
OTKUSteyler
0ba2694f01
Update index.tsx 2026-06-09 07:28:53 +02:00
OTKUSteyler
45b9a9c8e6
Rename badges_minified (1).json to badges_minified.json 2026-06-09 07:26:58 +02:00
OTKUSteyler
a326d43d2c
Add files via upload 2026-06-09 07:26:03 +02:00
OTKUSteyler
e217f9c1ce
Create Badges.json 2026-06-09 07:25:26 +02:00
OTKUSteyler
f5e11a6552
Update index.tsx 2026-06-09 07:10:45 +02:00
OTKUSteyler
2c260c153f
Update index.tsx 2026-06-09 07:07:06 +02:00
OTKUSteyler
d981f80666
Update index.tsx 2026-06-09 07:00:43 +02:00
OTKUSteyler
452d2e67f1
Update index.tsx 2026-06-09 04:10:57 +02:00
OTKUSteyler
b9153c23b7
Update index.tsx 2026-06-08 20:26:55 +02:00
OTKUSteyler
1cc7a8a9d5
Merge branch 'kmmiio99o:main' into main 2026-05-30 02:50:39 +02:00
OTKUSteyler
b04ce41839
Update index.ts 2026-05-20 07:15:41 +02:00
OTKUSteyler
a4e2407ae6
Update index.ts 2026-05-20 07:13:27 +02:00
OTKUSteyler
25ba7226ac
Update index.ts 2026-05-20 07:08:41 +02:00
OTKUSteyler
8d0f313630
Merge branch 'kmmiio99o:main' into main 2026-05-17 03:54:24 +02:00
OTKUSteyler
4ad9614ccb
Update README with serving instructions and image 2026-05-13 17:11:47 +02:00
OTKUSteyler
6552142c80
Update README.md 2026-05-12 20:12:31 +02:00
OTKUSteyler
6cbd9a1b95
Update index.ts 2026-05-11 12:03:15 +02:00
OTKUSteyler
4c9fc328db
Update index.ts 2026-05-11 11:58:06 +02:00
OTKUSteyler
3dcb0bcf00
Update index.ts 2026-05-11 11:49:22 +02:00
OTKUSteyler
396fd96c22
Update index.ts 2026-05-11 11:41:03 +02:00
OTKUSteyler
c2413969f3
Update index.ts 2026-05-11 11:37:10 +02:00
OTKUSteyler
86963d096c
Update index.ts 2026-05-11 11:30:17 +02:00
OTKUSteyler
cc1153d38f
Update index.ts 2026-05-11 11:24:41 +02:00
OTKUSteyler
f58841a2df
Update index.ts 2026-05-11 11:16:01 +02:00
OTKUSteyler
a2cb96f3aa
Merge branch 'kmmiio99o:main' into main 2026-05-10 17:27:08 +02:00
OTKUSteyler
d7255b2d39
Merge branch 'kmmiio99o:main' into main 2026-05-07 15:55:10 +02:00
OTKUSteyler
4894043612
Update README.md 2026-05-07 13:54:00 +02:00
OTKUSteyler
82cb9823bf
Merge branch 'kmmiio99o:main' into main 2026-04-27 18:23:13 +02:00
OTKUSteyler
6709c4929c Update loader.ts 2026-04-27 18:20:38 +02:00
OTKUSteyler
4d6a23f1f8
Update Loader.ts 2026-04-27 18:20:11 +02:00
OTKUSteyler
e599664b34
Update index.ts 2026-04-07 18:02:29 +02:00
OTKUSteyler
2f28882fa2
Update index.ts 2026-04-07 09:59:27 +02:00
OTKUSteyler
b0fbeb40c3
Update index.ts 2026-04-07 00:35:40 +02:00
OTKUSteyler
0ad2557275
Update index.ts 2026-04-07 00:29:44 +02:00
OTKUSteyler
b77421f3ce
Update index.ts 2026-04-07 00:15:49 +02:00
OTKUSteyler
0ad0b79091
Update index.ts 2026-04-07 00:15:16 +02:00
OTKUSteyler
51daf66491
Add files via upload 2026-04-07 00:14:46 +02:00
OTKUSteyler
9d65780297
Update index.ts 2026-04-07 00:14:19 +02:00
OTKUSteyler
08a0f04922
Delete src/core/plugins/max verstappen directory 2026-04-07 00:14:03 +02:00
OTKUSteyler
fb729d429e
Update index.ts 2026-04-07 00:00:43 +02:00
OTKUSteyler
30f9238e9d
Rename index.ts to index.ts 2026-04-06 23:59:28 +02:00
OTKUSteyler
20ac74cfe7
Update index.ts 2026-04-06 23:56:45 +02:00
OTKUSteyler
982fba36f6
Add files via upload 2026-04-06 23:55:49 +02:00
OTKUSteyler
f3864b7b6b
Delete src/core/plugins/max verstappen/aa 2026-04-06 23:55:26 +02:00
OTKUSteyler
c8282b30d8
Create index.ts 2026-04-06 23:54:30 +02:00
OTKUSteyler
6650f6d1db
Create aa 2026-04-06 23:53:57 +02:00
OTKUSteyler
d685025ce9
Delete src/core/plugins/max verstappen 2026-04-06 23:52:48 +02:00
OTKUSteyler
eeb9ec6ff1
Create max verstappen 2026-04-06 23:51:55 +02:00
OTKUSteyler
1332fa1fc2
Delete src/core/plugins/Max Verstappen directory 2026-04-06 23:51:15 +02:00
OTKUSteyler
76d411cbe0
Update index.ts 2026-04-06 23:49:58 +02:00
OTKUSteyler
ff53c6176c
Update index.ts 2026-04-06 23:48:24 +02:00
OTKUSteyler
739e6744c7
Add files via upload 2026-04-06 23:47:03 +02:00
OTKUSteyler
2ca955c46a
Delete src/core/plugins/Max Verstappen/tutuututu 2026-04-06 23:46:45 +02:00
OTKUSteyler
f4b15f3945
Create index.ts 2026-04-06 23:46:34 +02:00
OTKUSteyler
cc7e9712e8
Create tutuututu 2026-04-06 23:43:45 +02:00
OTKUSteyler
f4adb93c98
Rename index.js to index.ts 2026-04-02 19:43:14 +02:00
OTKUSteyler
84cbee35e7
Update index.ts 2026-04-02 19:40:49 +02:00
OTKUSteyler
fbd5bbe72c
Update index.ts 2026-04-02 19:39:49 +02:00
OTKUSteyler
2e9bfdfe0f
Update index.js 2026-04-02 19:35:57 +02:00
OTKUSteyler
4dbaddf579
Update index.js 2026-04-02 19:30:06 +02:00
OTKUSteyler
4e8a302268
Delete src/core/plugins/nodelete/a 2026-04-02 19:29:51 +02:00
OTKUSteyler
021e0c15fc
Add files via upload 2026-04-02 19:29:07 +02:00
OTKUSteyler
d45e9ef61f
Create a 2026-04-02 19:28:45 +02:00
OTKUSteyler
eb6dc9ad58
Update index.ts 2026-04-02 19:03:39 +02:00
OTKUSteyler
5fcc13cae5
Delete src/core/plugins/favouriteanything/a 2026-04-02 19:01:54 +02:00
OTKUSteyler
3c86ab104e
Update index.ts 2026-04-02 19:00:13 +02:00
OTKUSteyler
c14c714745
Add files via upload 2026-04-02 18:55:02 +02:00
OTKUSteyler
d48a8634d6
Create a 2026-04-02 18:54:26 +02:00
OTKUSteyler
fcd396602d
Delete src/core/plugins/favouriteanything 2026-04-02 18:53:59 +02:00
OTKUSteyler
2227e0d328
Create 2026-04-02 18:52:46 +02:00
OTKUSteyler
3aafa28a3b
Update index.ts 2026-04-02 17:26:36 +02:00
OTKUSteyler
4854b998a2
Delete src/core/plugins/NoDelete directory 2026-04-02 17:26:06 +02:00
OTKUSteyler
a731776639
Update index.js 2026-04-02 17:22:53 +02:00
OTKUSteyler
6c475e69be
Update index.ts 2026-04-02 17:16:22 +02:00
OTKUSteyler
3a3d61971a
Update index.ts 2026-04-02 17:12:51 +02:00
OTKUSteyler
b604a02fbc
Delete src/core/plugins/NoDelete/a 2026-04-02 17:09:00 +02:00
OTKUSteyler
ba3499834e
Add files via upload 2026-04-02 17:05:54 +02:00
OTKUSteyler
b63941bc3c
Create a 2026-04-02 17:05:34 +02:00
OTKUSteyler
9fd5d46f19
Update debug.ts 2026-03-23 14:46:35 +01:00
OTKUSteyler
597a871bd5
Update debug.ts 2026-03-23 14:43:23 +01:00
OTKUSteyler
49447a7be7
Update reload.ts 2026-03-23 14:42:03 +01:00
OTKUSteyler
a7bff4c900
Update index.ts 2026-03-23 06:43:38 +01:00
OTKUSteyler
c7c8b800ee
Update index.ts 2026-03-21 21:43:51 +01:00
OTKUSteyler
0885fe0c7a
Update index.tsx 2026-03-21 21:34:36 +01:00
OTKUSteyler
8e89a70e6a
Update text in About component 2026-03-21 21:32:30 +01:00
OTKUSteyler
f477466fdf
Add files via upload 2026-03-21 21:30:57 +01:00
OTKUSteyler
4cb42a13e0
Update build.mjs 2026-03-21 21:20:52 +01:00
OTKUSteyler
d5143127d5
Update index.tsx 2026-03-21 21:18:37 +01:00
OTKUSteyler
f8a1154083
Update release.yml 2026-03-21 21:14:52 +01:00
OTKUSteyler
4df48b31c1
Update default.json 2026-03-21 21:14:16 +01:00
OTKUSteyler
42238a9421
Update README for GoonCord installation instructions 2026-03-21 21:09:29 +01:00
OTKUSteyler
43c667838f
Update README.md 2026-03-21 21:04:31 +01:00
23 changed files with 792 additions and 122 deletions

View file

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

View file

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

View file

@ -30,7 +30,7 @@ let context = null;
const config = { const config = {
entryPoints: ["src/entry.ts"], entryPoints: ["src/entry.ts"],
bundle: true, bundle: true,
outfile: "dist/shiggycord.js", outfile: "dist/gooncord.js",
format: "iife", format: "iife",
splitting: false, splitting: false,
// Enable minification by default for release builds (when a release-branch is provided). // Enable minification by default for release builds (when a release-branch is provided).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import { after } from "@lib/api/patcher"; import { after } from "@lib/api/patcher";
import { onJsxCreate } from "@lib/api/react/jsx"; import { onJsxCreate } from "@lib/api/react/jsx";
import { findByName, findByNameLazy } from "@metro"; import { findByNameLazy } from "@metro";
import { useEffect, useState } from "react";
import { defineCorePlugin } from ".."; import { defineCorePlugin } from "..";
import { FluxDispatcher } from "@metro/common"; import { FluxDispatcher } from "@metro/common";
@ -10,14 +9,9 @@ interface Badge {
url: string; url: string;
} }
interface CustomBadge {
label: string;
url: string;
}
interface UserBadgeData { interface UserBadgeData {
roles?: string[]; roles?: string[];
custom?: CustomBadge[]; custom?: Badge[];
} }
interface BadgeData { interface BadgeData {
@ -33,6 +27,11 @@ interface RolesData {
[roleName: string]: RoleData; [roleName: string]: RoleData;
} }
interface EquicordBadge {
tooltip: string;
badge: string;
}
const useBadgesModule = findByNameLazy("useBadges", false); const useBadgesModule = findByNameLazy("useBadges", false);
const badgesCache = new Map<string, Badge[]>(); const badgesCache = new Map<string, Badge[]>();
@ -40,20 +39,20 @@ const badgeProps = new Map<string, Record<string, any>>();
const pendingRequests = new Set<string>(); const pendingRequests = new Set<string>();
export default defineCorePlugin({ export default defineCorePlugin({
manifest: { manifest: {
id: "bunny.badges", id: "bunny.badges",
version: "1.1.0", version: "1.2.0",
type: "plugin", type: "plugin",
spec: 3, spec: 3,
main: "", main: "",
display: { display: {
name: "Badges", name: "Badges",
description: "Adds badges to user's profile", description: "Adds badges to user's profile",
authors: [{ name: "cocobo1" }, { name: "pylixonly" }], authors: [{ name: "cocobo1" }, { name: "pylixonly" }],
},
}, },
},
start() { start() {
onJsxCreate("ProfileBadge", (component, ret) => { onJsxCreate("ProfileBadge", (component, ret) => {
if (ret.props.id?.startsWith("rain-")) { if (ret.props.id?.startsWith("rain-")) {
const cachedProps = badgeProps.get(ret.props.id); const cachedProps = badgeProps.get(ret.props.id);
@ -74,27 +73,67 @@ export default defineCorePlugin({
} }
}); });
// 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;
}
};
const fetchAndProcessBadges = async (userId: string) => { const fetchAndProcessBadges = async (userId: string) => {
if (pendingRequests.has(userId)) return; if (pendingRequests.has(userId)) return;
pendingRequests.add(userId); pendingRequests.add(userId);
try { try {
const [badgesRes, rolesRes] = await Promise.all([ const [
fetch("https://codeberg.org/raincord/badges/raw/branch/main/badges.json"), goonBadgesData,
fetch("https://codeberg.org/raincord/badges/raw/branch/main/assets/roles/roles.json"), goonRolesData,
rainBadgesData,
rainRolesData,
equicordData,
] = await Promise.all([
safeFetchJson<BadgeData>(
"https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/badges.json",
{}
),
safeFetchJson<RolesData>(
"https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/assets/roles/roles.json",
{}
),
safeFetchJson<BadgeData>(
"https://codeberg.org/raincord/badges/raw/branch/main/badges.json",
{}
),
safeFetchJson<RolesData>(
"https://codeberg.org/raincord/badges/raw/branch/main/assets/roles/roles.json",
{}
),
safeFetchJson<Record<string, EquicordBadge[]>>(
"https://badge.equicord.org/badges.json",
{}
),
]); ]);
const badgesData: BadgeData = await badgesRes.json();
const rolesData: RolesData = await rolesRes.json();
const userBadgeData = badgesData[userId] || { roles: [], custom: [] };
const allBadges: Badge[] = []; const allBadges: Badge[] = [];
// process role badges // GoonCord and raincord badge/role sets are keyed independently,
if (userBadgeData.roles) { // so each source is resolved against its own roles file.
userBadgeData.roles.forEach(roleName => { const badgeSources: Array<{ badges: BadgeData; roles: RolesData }> = [
const roleData = rolesData[roleName]; { 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) { if (roleData) {
allBadges.push({ allBadges.push({
label: roleData.label, label: roleData.label,
@ -102,12 +141,18 @@ export default defineCorePlugin({
}); });
} }
}); });
}
// process custom badges // process custom badges
if (userBadgeData.custom) { if (userBadgeData.custom) {
allBadges.push(...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); badgesCache.set(userId, allBadges);
@ -122,12 +167,14 @@ export default defineCorePlugin({
}); });
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: { id: userId } }); FluxDispatcher.dispatch({ type: "USER_UPDATE", user: { id: userId } });
} catch (err) {
console.error("[bunny.badges] Failed to fetch/process badges:", err);
} finally { } finally {
pendingRequests.delete(userId); pendingRequests.delete(userId);
} }
}; };
after("default", useBadgesModule, ([user], result) => { return after("default", useBadgesModule, ([user], result) => {
if (!user) return; if (!user) return;
const userId = user.userId; const userId = user.userId;

View file

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

View file

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

View file

@ -11,7 +11,7 @@ const { TableRowGroup, TableRow, TableSwitchRow, Stack } = findByProps(
"TableSwitchRow", "TableSwitchRow",
"Stack", "Stack",
); );
const { ScrollView, Text } = require("react-native"); const { ScrollView, Text, Linking } = require("react-native");
type FixEmbedSettings = { type FixEmbedSettings = {
twitter: boolean; twitter: boolean;
@ -42,19 +42,19 @@ function transformLinks(content: string, config: FixEmbedSettings): string {
if (config.instagram) { if (config.instagram) {
result = result.replace( result = result.replace(
/https?:\/\/(?:www\.)?instagram\.com(\/[^\s]+)/gi, /https?:\/\/(?:www\.)?instagram\.com(\/[^\s]+)/gi,
"https://uuinstagram.com$1", "https://kkinstagram.com$1",
); );
} }
// TikTok: Replace domain, keep path // TikTok: Replace domain, keep path (use embedez.com for all TikTok links)
if (config.tiktok) { if (config.tiktok) {
result = result.replace( result = result.replace(
/https?:\/\/(?:www\.)?tiktok\.com(\/[^\s]+)/gi, /https?:\/\/(?:www\.)?tiktok\.com(\/[^\s]+)/gi,
"https://tnktok.com$1", "https://embedez.com$1",
); );
// TikTok short links (vm.tiktok.com) // TikTok short links (vm.tiktok.com)
result = result.replace( result = result.replace(
/https?:\/\/vm\.tiktok\.com\/([A-Za-z0-9]+)\/?/gi, /https?:\/\/vm\.tiktok\.com\/([A-Za-z0-9]+)\/?/gi,
"https://vm.tnktok.com/$1", "https://vm.tiktokez.com/$1",
); );
} }
// Reddit: Replace domain, keep path // Reddit: Replace domain, keep path
@ -99,7 +99,6 @@ export default defineCorePlugin({
setConfig((prev) => ({ ...prev, [key]: value })); setConfig((prev) => ({ ...prev, [key]: value }));
}; };
// Prefer table-style rows (TableRowGroup / TableSwitchRow) and Stack layout similar to other core plugins.
const { const {
TableRowGroup: _TRG, TableRowGroup: _TRG,
TableRow: _TR, TableRow: _TR,
@ -107,7 +106,6 @@ export default defineCorePlugin({
Stack: _S, Stack: _S,
} = findByProps("TableRowGroup", "TableRow", "TableSwitchRow", "Stack"); } = findByProps("TableRowGroup", "TableRow", "TableSwitchRow", "Stack");
// Fallback if the table-style components are not available in the host environment
if (!_TRG || !_TSR || !_TR || !_S) { if (!_TRG || !_TSR || !_TR || !_S) {
return React.createElement( return React.createElement(
ScrollView, ScrollView,
@ -120,7 +118,6 @@ export default defineCorePlugin({
); );
} }
// Use the resolved components
const TableRowGroup = _TRG; const TableRowGroup = _TRG;
const TableRow = _TR; const TableRow = _TR;
const TableSwitchRow = _TSR; const TableSwitchRow = _TSR;
@ -164,6 +161,12 @@ export default defineCorePlugin({
"This plugin automatically converts social media links to privacy-friendly alternative frontends for better embeds.", "This plugin automatically converts social media links to privacy-friendly alternative frontends for better embeds.",
disabled: true, disabled: true,
}), }),
React.createElement(TableRow, {
label: "EmbedEZ",
subLabel: "TikTok embed fix powered by embedez.com — tap to open",
onPress: () => Linking.openURL("https://embedez.com/"),
arrow: true,
}),
), ),
), ),
]); ]);

View file

@ -35,6 +35,11 @@ export const getCorePlugins = (): CorePluginRecord => ({
"bunny.badges": makeLazyPlugin(() => require("./badges")), "bunny.badges": makeLazyPlugin(() => require("./badges")),
"bunny.notrack": makeLazyPlugin(() => require("./notrack")), "bunny.notrack": makeLazyPlugin(() => require("./notrack")),
"bunny.fixembed": makeLazyPlugin(() => require("./fixembed")), "bunny.fixembed": makeLazyPlugin(() => require("./fixembed")),
"bunny.favouriteanything": makeLazyPlugin(() => require("./favouriteanything")),
"bunny.nodelete": makeLazyPlugin(() => require("./nodelete")),
"bunny.maxverstappen": makeLazyPlugin(() => require("./maxverstappen")),
}); });
/** /**

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -125,7 +125,7 @@ export default function General() {
> >
<TableRowGroup title="App Information"> <TableRowGroup title="App Information">
<TableRow <TableRow
label="ShiggyCord" label="💦GoonCord💦"
icon={<TableRow.Icon source={{ uri: shiggycordIcon ?? "" }} />} icon={<TableRow.Icon source={{ uri: shiggycordIcon ?? "" }} />}
trailing={<TableRow.TrailingText text={debugInfo.bunny.version} />} trailing={<TableRow.TrailingText text={debugInfo.bunny.version} />}
/> />

View file

@ -92,5 +92,5 @@ export default async () => {
} }
// Final ready log for basic UI availability. // Final ready log for basic UI availability.
logger.log("ShiggyCord is ready."); logger.log("GoonCord is ready.");
}; };

View file

@ -1,64 +1,49 @@
import { ImageLoader } from "./"; import { ImageLoader } from "./";
import type { LoaderPayload } from "./types"; import type { LoaderPayload } from "./types";
type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>; type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>;
export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload; export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload;
export function isModuleRegistered(module: string) { export function isModuleRegistered(module: string) {
return loaderPayload.loader.modules[module] !== undefined; return loaderPayload.loader.modules[module] !== undefined;
} }
export function isFunctionRegistered(module: string, function_: string) { export function isFunctionRegistered(module: string, function_: string) {
return loaderPayload.loader.modules[module]?.functions[function_] !== undefined; return loaderPayload.loader.modules[module]?.functions[function_] !== undefined;
} }
export async function callFunction(module: string, functionName: string, args: unknown[]) { export async function callFunction(module: string, functionName: string, args: unknown[]) {
if (!isFunctionRegistered(module, functionName)) { if (!isFunctionRegistered(module, functionName)) {
throw new Error(`Function ${module}.${functionName} is not registered`); throw new Error(`Function ${module}.${functionName} is not registered`);
} }
const promise: InteropReturn = ImageLoader.queryCache([ const promise: InteropReturn = ImageLoader.queryCache([
"__pyon_bridge", "__pyon_bridge",
JSON.stringify({ m: module, f: functionName, a: args }), JSON.stringify({ m: module, f: functionName, a: args }),
]); ]);
const result = await promise; const result = await promise;
if ("ret" in result) { if ("ret" in result) {
return JSON.parse(result.ret); return JSON.parse(result.ret);
} }
if ("cancelled" in result) { if ("cancelled" in result) {
throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`); throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`);
} }
if ("err" in result) { if ("err" in result) {
throw new Error(result.err); throw new Error(result.err);
} }
} }
export interface LoaderModule { export interface LoaderModule {
hasFunction(functionName: string): boolean; hasFunction(functionName: string): boolean;
callFunction(functionName: string, args: unknown[]): Promise<unknown>; callFunction(functionName: string, args: unknown[]): Promise<unknown>;
} }
type ModuleProps<T extends LoaderModule> = { type ModuleProps<T extends LoaderModule> = {
name: string; name: string;
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>; argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
}; };
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) { export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
const module = loaderPayload.loader.modules[name]; const module = loaderPayload.loader.modules[name];
if (!module) { if (!module) {
throw new Error(`Module ${name} is not registered`); throw new Error(`Module ${name} is not registered`);
} }
const baseObject: Record<string, any> = { const baseObject: Record<string, any> = {
hasFunction: (functionName: string) => isFunctionRegistered(name, functionName), hasFunction: (functionName: string) => isFunctionRegistered(name, functionName),
callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args), callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args),
}; };
for (const [functionName] of Object.entries(module.functions)) { for (const [functionName] of Object.entries(module.functions)) {
baseObject[functionName] = (...args: unknown[]) => baseObject[functionName] = (...args: unknown[]) =>
callFunction( callFunction(
@ -68,10 +53,8 @@ export function getModule<T extends LoaderModule>({ name, argumentProcessors: pr
processArgs && functionName in processArgs ? processArgs[functionName](args) : args, processArgs && functionName in processArgs ? processArgs[functionName](args) : args,
); );
} }
return baseObject as T; return baseObject as T;
} }
/** /**
* Determines if safe mode is currently enabled. The result remains consistent over time. * Determines if safe mode is currently enabled. The result remains consistent over time.
* This will not change until the app is restarted. For draft changes, use the InitConfigStore. * This will not change until the app is restarted. For draft changes, use the InitConfigStore.

View file

@ -1,64 +1,49 @@
import { ImageLoader } from "./"; import { ImageLoader } from "./";
import type { LoaderPayload } from "./types"; import type { LoaderPayload } from "./types";
type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>; type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>;
export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload; export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload;
export function isModuleRegistered(module: string) { export function isModuleRegistered(module: string) {
return loaderPayload.loader.modules[module] !== undefined; return loaderPayload.loader.modules[module] !== undefined;
} }
export function isFunctionRegistered(module: string, function_: string) { export function isFunctionRegistered(module: string, function_: string) {
return loaderPayload.loader.modules[module]?.functions[function_] !== undefined; return loaderPayload.loader.modules[module]?.functions[function_] !== undefined;
} }
export async function callFunction(module: string, functionName: string, args: unknown[]) { export async function callFunction(module: string, functionName: string, args: unknown[]) {
if (!isFunctionRegistered(module, functionName)) { if (!isFunctionRegistered(module, functionName)) {
throw new Error(`Function ${module}.${functionName} is not registered`); throw new Error(`Function ${module}.${functionName} is not registered`);
} }
const promise: InteropReturn = ImageLoader.queryCache([ const promise: InteropReturn = ImageLoader.queryCache([
"__pyon_bridge", "__pyon_bridge",
JSON.stringify({ m: module, f: functionName, a: args }), JSON.stringify({ m: module, f: functionName, a: args }),
]); ]);
const result = await promise; const result = await promise;
if ("ret" in result) { if ("ret" in result) {
return JSON.parse(result.ret); return JSON.parse(result.ret);
} }
if ("cancelled" in result) { if ("cancelled" in result) {
throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`); throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`);
} }
if ("err" in result) { if ("err" in result) {
throw new Error(result.err); throw new Error(result.err);
} }
} }
export interface LoaderModule { export interface LoaderModule {
hasFunction(functionName: string): boolean; hasFunction(functionName: string): boolean;
callFunction(functionName: string, args: unknown[]): Promise<unknown>; callFunction(functionName: string, args: unknown[]): Promise<unknown>;
} }
type ModuleProps<T extends LoaderModule> = { type ModuleProps<T extends LoaderModule> = {
name: string; name: string;
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>; argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
}; };
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) { export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
const module = loaderPayload.loader.modules[name]; const module = loaderPayload.loader.modules[name];
if (!module) { if (!module) {
throw new Error(`Module ${name} is not registered`); throw new Error(`Module ${name} is not registered`);
} }
const baseObject: Record<string, any> = { const baseObject: Record<string, any> = {
hasFunction: (functionName: string) => isFunctionRegistered(name, functionName), hasFunction: (functionName: string) => isFunctionRegistered(name, functionName),
callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args), callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args),
}; };
for (const [functionName] of Object.entries(module.functions)) { for (const [functionName] of Object.entries(module.functions)) {
baseObject[functionName] = (...args: unknown[]) => baseObject[functionName] = (...args: unknown[]) =>
callFunction( callFunction(
@ -68,10 +53,8 @@ export function getModule<T extends LoaderModule>({ name, argumentProcessors: pr
processArgs && functionName in processArgs ? processArgs[functionName](args) : args, processArgs && functionName in processArgs ? processArgs[functionName](args) : args,
); );
} }
return baseObject as T; return baseObject as T;
} }
/** /**
* Determines if safe mode is currently enabled. The result remains consistent over time. * Determines if safe mode is currently enabled. The result remains consistent over time.
* This will not change until the app is restarted. For draft changes, use the InitConfigStore. * This will not change until the app is restarted. For draft changes, use the InitConfigStore.
@ -79,4 +62,4 @@ export function getModule<T extends LoaderModule>({ name, argumentProcessors: pr
*/ */
export function isSafeModeEnabled() { export function isSafeModeEnabled() {
return Boolean(loaderPayload.loader.initConfig.safeMode); return Boolean(loaderPayload.loader.initConfig.safeMode);
} }