Compare commits
127 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c250f9081 | ||
|
|
27dc770a14 | ||
|
|
513d9b63fa | ||
|
|
f916820d9b | ||
|
|
3569666df2 | ||
|
|
8f272d57ef | ||
|
|
8d4aa61ebd | ||
|
|
0968e8b1fe | ||
|
|
380a7d8cf8 | ||
|
|
6c68f5b5d6 | ||
|
|
60f9e9e872 | ||
|
|
992d80a6d5 | ||
|
|
790157f32b | ||
|
|
c356d72b00 | ||
|
|
17409439cb | ||
|
|
c7369fe2a9 | ||
|
|
a7073e3e8a | ||
|
|
972c87ec33 | ||
|
|
e38c36aa78 | ||
|
|
a4fb30ebbf | ||
|
|
254ef099f6 | ||
|
|
3e39a3a4b4 | ||
|
|
c310ca3128 | ||
|
|
282eddf544 | ||
|
|
cbd12e8447 | ||
|
|
2697587953 | ||
|
|
01e8fe8793 | ||
|
|
dc4d0049b6 | ||
|
|
e5ae029e1c | ||
|
|
f9a8e9d493 | ||
|
|
de20126f36 | ||
|
|
de509f83b3 | ||
|
|
8bd8ab2039 | ||
|
|
3ba90b941e | ||
|
|
d21eea3e9b | ||
|
|
0ba2694f01 | ||
|
|
45b9a9c8e6 | ||
|
|
a326d43d2c | ||
|
|
e217f9c1ce | ||
|
|
f5e11a6552 | ||
|
|
2c260c153f | ||
|
|
d981f80666 | ||
|
|
452d2e67f1 | ||
|
|
b9153c23b7 | ||
|
|
1cc7a8a9d5 | ||
|
|
b04ce41839 | ||
|
|
a4e2407ae6 | ||
|
|
25ba7226ac | ||
|
|
8d0f313630 | ||
|
|
4ad9614ccb | ||
|
|
6552142c80 | ||
|
|
6cbd9a1b95 | ||
|
|
4c9fc328db | ||
|
|
3dcb0bcf00 | ||
|
|
396fd96c22 | ||
|
|
c2413969f3 | ||
|
|
86963d096c | ||
|
|
cc1153d38f | ||
|
|
f58841a2df | ||
|
|
a2cb96f3aa | ||
|
|
d7255b2d39 | ||
|
|
4894043612 | ||
|
|
82cb9823bf | ||
|
|
6709c4929c | ||
|
|
4d6a23f1f8 | ||
|
|
e599664b34 | ||
|
|
2f28882fa2 | ||
|
|
b0fbeb40c3 | ||
|
|
0ad2557275 | ||
|
|
b77421f3ce | ||
|
|
0ad0b79091 | ||
|
|
51daf66491 | ||
|
|
9d65780297 | ||
|
|
08a0f04922 | ||
|
|
fb729d429e | ||
|
|
30f9238e9d | ||
|
|
20ac74cfe7 | ||
|
|
982fba36f6 | ||
|
|
f3864b7b6b | ||
|
|
c8282b30d8 | ||
|
|
6650f6d1db | ||
|
|
d685025ce9 | ||
|
|
eeb9ec6ff1 | ||
|
|
1332fa1fc2 | ||
|
|
76d411cbe0 | ||
|
|
ff53c6176c | ||
|
|
739e6744c7 | ||
|
|
2ca955c46a | ||
|
|
f4b15f3945 | ||
|
|
cc7e9712e8 | ||
|
|
f4adb93c98 | ||
|
|
84cbee35e7 | ||
|
|
fbd5bbe72c | ||
|
|
2e9bfdfe0f | ||
|
|
4dbaddf579 | ||
|
|
4e8a302268 | ||
|
|
021e0c15fc | ||
|
|
d45e9ef61f | ||
|
|
eb6dc9ad58 | ||
|
|
5fcc13cae5 | ||
|
|
3c86ab104e | ||
|
|
c14c714745 | ||
|
|
d48a8634d6 | ||
|
|
fcd396602d | ||
|
|
2227e0d328 | ||
|
|
3aafa28a3b | ||
|
|
4854b998a2 | ||
|
|
a731776639 | ||
|
|
6c475e69be | ||
|
|
3a3d61971a | ||
|
|
b604a02fbc | ||
|
|
ba3499834e | ||
|
|
b63941bc3c | ||
|
|
9fd5d46f19 | ||
|
|
597a871bd5 | ||
|
|
49447a7be7 | ||
|
|
a7bff4c900 | ||
|
|
c7c8b800ee | ||
|
|
0885fe0c7a | ||
|
|
8e89a70e6a | ||
|
|
f477466fdf | ||
|
|
4cb42a13e0 | ||
|
|
d5143127d5 | ||
|
|
f8a1154083 | ||
|
|
4df48b31c1 | ||
|
|
42238a9421 | ||
|
|
43c667838f |
23 changed files with 792 additions and 122 deletions
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -1,39 +1,41 @@
|
||||||
# ShiggyCord [](https://discord.shiggycord.dev)
|
# GoonCord [](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.
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
13
src/core/plugins/favouriteanything/Settings.tsx
Normal file
13
src/core/plugins/favouriteanything/Settings.tsx
Normal 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>
|
||||||
|
)
|
||||||
169
src/core/plugins/favouriteanything/index.ts
Normal file
169
src/core/plugins/favouriteanything/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -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")),
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
BIN
src/core/plugins/maxverstappen/Max Verstappen.mp3
Normal file
BIN
src/core/plugins/maxverstappen/Max Verstappen.mp3
Normal file
Binary file not shown.
145
src/core/plugins/maxverstappen/index.ts
Normal file
145
src/core/plugins/maxverstappen/index.ts
Normal 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");
|
||||||
|
},
|
||||||
|
});
|
||||||
76
src/core/plugins/nodelete/index.ts
Normal file
76
src/core/plugins/nodelete/index.ts
Normal 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");
|
||||||
|
},
|
||||||
|
});
|
||||||
90
src/core/plugins/nodelete/settings.jsx
Normal file
90
src/core/plugins/nodelete/settings.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
158
src/core/plugins/nodelete/translations.js
Normal file
158
src/core/plugins/nodelete/translations.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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} />}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue