Compare commits

..

75 commits

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

View file

@ -32,8 +32,10 @@ Bundle based on ShiggyCord, made just for fun
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. 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 GoonCord's bundled code from your server, rather than GitHub.
1. Make your changes, rebuild, reload, go wild!
Alternatively, you can directly *serve* the bundled code by running `bun run serve`. `gooncord.js` will be served on your local address under the port 4040. You will then insert `http://<local ip address>:4040/gooncord.js` as a custom url and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it.
![OTKUSteyler](https://count.getloli.com/@OTKUSteyler?name=OTKUSteyler&theme=miku&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=1)

1967
bun.lock

File diff suppressed because it is too large Load diff

View file

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

View file

@ -56,6 +56,8 @@ const config = {
"!ShiggyCord-deps-shim!": "./shims/depsModule.ts",
spitroast: "./node_modules/spitroast",
"react/jsx-runtime": "./shims/jsxRuntime",
"@gullerya/object-observer":
path.resolve("./node_modules/@gullerya/object-observer/dist/object-observer.min.js"),
},
plugins: [
globalPlugin({
@ -75,7 +77,7 @@ const config = {
constModules: {
globals: {
"bunny-build-info": {
version: `"1.4.1.4"`,
version: `"1.4.1.8"`,
},
},
},

View file

@ -18,16 +18,20 @@ export default () =>
},
],
execute([ephemeral], ctx) {
const info = getDebugInfo();
const content = [
"**💦GoonCord💦 Debug Info**",
`> 💦GoonCord💦: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
`> Device: ${info.device.model}`,
].join("\n");
const info = getDebugInfo();
if(info.device.codename === undefined) { info.device.codename = ""}
else{ info.device.codename = "(" + info.device.codename + ")" }
const content = [
"**💦GoonCord💦 Debug Info**",
`> GoonCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
`> Device: ${info.device.model} ${info.device.codename}`,
].join("\n");
if (ephemeral?.value) {
messageUtil.sendBotMessage(ctx.channel.id, content);

View file

@ -11,7 +11,7 @@
"IMAGE_FILES": "Image Files",
"TEXT_FILES": "Text Files",
"BRAND": "Brand",
"SHIGGYCORD": "💦GoonCord💦",
"SHIGGYCORD": "GoonCord",
"SHIGGYCORD_URL": "GoonCord URL",
"BROWSER": "Browser",
"BYTECODE": "Bytecode",
@ -94,7 +94,7 @@
"PLUGINS": "Plugins",
"REFETCH": "Refetch",
"RELOAD": "Reload",
"RELOAD_DISCORD": "💦ReGoon💦 your Discord",
"RELOAD_DISCORD": "ReGoon your Discord",
"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_SAFE_MODE": "Reload in Safe Mode",

View file

@ -156,6 +156,16 @@ export function formatString<T = void>(
val: Record<string, T>,
): FormatStringRet<T> {
const str = Strings[key];
// @ts-ignore
return new IntlMessageFormat(str).format(val);
if (!str) return str as FormatStringRet<T>;
try {
// @ts-ignore
return new IntlMessageFormat(str).format(val);
} catch (e: any) {
if (e?.name === "SyntaxError") {
logger.warn(`[i18n] Failed to parse message for key "${String(key)}":`, e.message);
return str as FormatStringRet<T>;
}
logger.warn(`[i18n] Failed to format message for key "${String(key)}":`, e?.message ?? e);
return str as FormatStringRet<T>;
}
}

View file

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

View file

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

View file

@ -39,6 +39,7 @@ export const getCorePlugins = (): CorePluginRecord => ({
"bunny.nodelete": makeLazyPlugin(() => require("./nodelete")),
"bunny.maxverstappen": makeLazyPlugin(() => require("./maxverstappen")),
});
/**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,15 +5,14 @@ import type { ImageURISource } from "react-native";
import { patchTabsUI } from "./patches/tabs";
export interface RowConfig {
key: string;
title: () => string;
onPress?: () => any;
render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number;
IconComponent?: React.ReactNode;
usePredicate?: () => boolean;
useTrailing?: () => string | JSX.Element;
rawTabsConfig?: Record<string, any>;
key: string;
title: () => string;
onPress?: () => any;
render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number;
IconComponent?: React.ReactNode,
usePredicate?: () => boolean,
useTrailing?: () => string | JSX.Element
}
export const registeredSections = {} as {

View file

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

View file

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