Compare commits

...
Sign in to create a new pull request.

66 commits
main ... dev

Author SHA1 Message Date
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
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
22 changed files with 685 additions and 64 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,4 +1,4 @@
# 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 Kettu, made just for fun
@ -6,34 +6,34 @@ Bundle based on Kettu, made just for fun
### 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/shiggycord.js`) in the new input box labelled `ShiggyCord 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.

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: [
{ {
@ -20,8 +20,8 @@ export default () =>
execute([ephemeral], ctx) { execute([ephemeral], ctx) {
const info = getDebugInfo(); const info = getDebugInfo();
const content = [ const content = [
"**ShiggyCord Debug Info**", "**💦GoonCord💦 Debug Info**",
`> ShiggyCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`, `> 💦GoonCord💦: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`, `> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`, `> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`, `> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,

View file

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

View file

@ -11,8 +11,8 @@
"IMAGE_FILES": "Image Files", "IMAGE_FILES": "Image Files",
"TEXT_FILES": "Text Files", "TEXT_FILES": "Text Files",
"BRAND": "Brand", "BRAND": "Brand",
"SHIGGYCORD": "ShiggyCord", "SHIGGYCORD": "💦GoonCord💦",
"SHIGGYCORD_URL": "ShiggyCord URL", "SHIGGYCORD_URL": "GoonCord URL",
"BROWSER": "Browser", "BROWSER": "Browser",
"BYTECODE": "Bytecode", "BYTECODE": "Bytecode",
"CANCEL": "Cancel", "CANCEL": "Cancel",
@ -24,7 +24,7 @@
"CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}", "CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}",
"CODEBERG": "Codeberg", "CODEBERG": "Codeberg",
"CODENAME": "Codename", "CODENAME": "Codename",
"COMMAND_DEBUG_DESC": "Send ShiggyCord debug info.", "COMMAND_DEBUG_DESC": "Send 💦GoonCord💦 debug info.",
"COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.", "COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.",
"COMMAND_EVAL_DESC": "Evaluate JavaScript code.", "COMMAND_EVAL_DESC": "Evaluate JavaScript code.",
"COMMAND_EVAL_OPT_ASYNC": "Whether to support 'await' in code. Must explicitly return for result (default: false)", "COMMAND_EVAL_OPT_ASYNC": "Whether to support 'await' in code. Must explicitly return for result (default: false)",
@ -70,7 +70,7 @@
"LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme", "LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme",
"LINKS": "Links", "LINKS": "Links",
"LOAD_FROM_CUSTOM_URL": "Load from custom URL", "LOAD_FROM_CUSTOM_URL": "Load from custom URL",
"LOAD_FROM_CUSTOM_URL_DEC": "Load ShiggyCord from a custom endpoint.", "LOAD_FROM_CUSTOM_URL_DEC": "Load GoonCord from a custom endpoint.",
"LOAD_REACT_DEVTOOLS": "Load React DevTools", "LOAD_REACT_DEVTOOLS": "Load React DevTools",
"SHIGGY_URL": "ShiggyCord URL", "SHIGGY_URL": "ShiggyCord URL",
"LOADER": "Loader", "LOADER": "Loader",
@ -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

@ -80,8 +80,8 @@ export default defineCorePlugin({
try { try {
const [badgesRes, rolesRes] = await Promise.all([ const [badgesRes, rolesRes] = await Promise.all([
fetch("https://codeberg.org/raincord/badges/raw/branch/main/badges.json"), fetch("https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/badges.json"),
fetch("https://codeberg.org/raincord/badges/raw/branch/main/assets/roles/roles.json"), fetch("https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/assets/roles/roles.json"),
]); ]);
const badgesData: BadgeData = await badgesRes.json(); const badgesData: BadgeData = await badgesRes.json();

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

@ -35,6 +35,10 @@ 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

@ -126,5 +126,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.